Skip to content

Commit fa377ef

Browse files
authored
Merge pull request #37 from sidepelican/client_macro
Use macro for Client Side Swift code
2 parents c4d9567 + 56954ea commit fa377ef

File tree

16 files changed

+143
-462
lines changed

16 files changed

+143
-462
lines changed

Package.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ let package = Package(
1111
.library(name: "CallableKit", targets: ["CallableKit"]),
1212
.library(name: "CallableKitVaporTransport", targets: ["CallableKitVaporTransport"]),
1313
.library(name: "CallableKitHummingbirdTransport", targets: ["CallableKitHummingbirdTransport"]),
14+
.library(name: "CallableKitURLSessionStub", targets: ["CallableKitURLSessionStub"]),
1415
],
1516
dependencies: [
1617
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.2"),
@@ -65,6 +66,12 @@ let package = Package(
6566
.product(name: "Hummingbird", package: "hummingbird"),
6667
"CallableKit",
6768
]
69+
),
70+
.target(
71+
name: "CallableKitURLSessionStub",
72+
dependencies: [
73+
"CallableKit",
74+
]
6875
)
6976
]
7077
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@usableFromInline internal struct CallableKitEmpty: Codable {
2+
@usableFromInline init() {}
3+
}

Sources/CallableKit/Macros.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
@attached(
22
peer,
3-
names: prefixed(configure)
3+
names: prefixed(configure), suffixed(Stub)
44
)
55
public macro Callable() = #externalMacro(module: "CallableKitMacros", type: "CallableMacro")

Sources/CallableKit/ServiceTransport.swift

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,46 +22,43 @@ public protocol ServiceTransport<Service> {
2222
)
2323
}
2424

25-
fileprivate struct _Empty: Codable, Sendable {
26-
}
27-
2825
extension ServiceTransport {
29-
public func register<Request: Decodable>(
26+
@inlinable public func register<Request: Decodable>(
3027
path: String,
3128
methodSelector: @escaping @Sendable (Service.Type) -> (Service) -> (Request) async throws -> Void
3229
) {
3330
register(path: path) { (serviceType) in
3431
{ (service: Service) in
35-
{ (request: Request) -> _Empty in
32+
{ (request: Request) -> CallableKitEmpty in
3633
try await methodSelector(serviceType)(service)(request)
37-
return _Empty()
34+
return CallableKitEmpty()
3835
}
3936
}
4037
}
4138
}
4239

43-
public func register<Response: Encodable>(
40+
@inlinable public func register<Response: Encodable>(
4441
path: String,
4542
methodSelector: @escaping @Sendable (Service.Type) -> (Service) -> () async throws -> Response
4643
) {
4744
register(path: path) { (serviceType) in
4845
{ (service: Service) in
49-
{ (_: _Empty) -> Response in
46+
{ (_: CallableKitEmpty) -> Response in
5047
return try await methodSelector(serviceType)(service)()
5148
}
5249
}
5350
}
5451
}
5552

56-
public func register(
53+
@inlinable public func register(
5754
path: String,
5855
methodSelector: @escaping @Sendable (Service.Type) -> (Service) -> () async throws -> Void
5956
) {
6057
register(path: path) { (serviceType) in
6158
{ (service: Service) in
62-
{ (_: _Empty) -> _Empty in
59+
{ (_: CallableKitEmpty) -> CallableKitEmpty in
6360
try await methodSelector(serviceType)(service)()
64-
return _Empty()
61+
return CallableKitEmpty()
6562
}
6663
}
6764
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
public protocol StubClientProtocol: Sendable {
2+
func send<Request: Encodable & Sendable, Response: Decodable & Sendable>(
3+
path: String,
4+
request: Request
5+
) async throws -> Response
6+
7+
func send<Request: Encodable & Sendable>(
8+
path: String,
9+
request: Request
10+
) async throws
11+
12+
func send<Response: Decodable & Sendable>(
13+
path: String
14+
) async throws -> Response
15+
16+
func send(
17+
path: String
18+
) async throws
19+
}
20+
21+
extension StubClientProtocol {
22+
@inlinable public func send<Request: Encodable & Sendable, Response: Decodable & Sendable>(
23+
path: String,
24+
request: Request
25+
) async throws -> Response {
26+
try await send(path: path, request: request)
27+
}
28+
29+
@inlinable public func send<Request: Encodable & Sendable>(
30+
path: String,
31+
request: Request
32+
) async throws {
33+
_ = try await send(path: path, request: request) as CallableKitEmpty
34+
}
35+
36+
@inlinable public func send<Response: Decodable & Sendable>(
37+
path: String
38+
) async throws -> Response {
39+
try await send(path: path, request: CallableKitEmpty())
40+
}
41+
42+
@inlinable public func send(
43+
path: String
44+
) async throws {
45+
_ = try await send(path: path, request: CallableKitEmpty())
46+
}
47+
}

Sources/CallableKitMacros/CallableMacro.swift

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,32 @@ public struct CallableMacro: PeerMacro {
3939
}
4040
}
4141

42-
return [DeclSyntax(configureFunc)]
42+
let stubStruct = try StructDeclSyntax("public struct \(raw: protocolName)Stub<C: StubClientProtocol>: \(raw: protocolName), Sendable") {
43+
VariableDeclSyntax(
44+
modifiers: [.init(name: .keyword(.private))],
45+
.let,
46+
name: "client" as PatternSyntax,
47+
type: TypeAnnotationSyntax(type: "C" as TypeSyntax)
48+
)
49+
try InitializerDeclSyntax("public init(client: C)") {
50+
"self.client = client"
51+
}
52+
for function in functions {
53+
function
54+
.with(\.leadingTrivia, [])
55+
.with(\.modifiers, [.init(name: .keyword(.public))])
56+
.with(\.body, CodeBlockSyntax {
57+
if let param = function.signature.parameterClause.parameters.first {
58+
let argName = param.secondName ?? param.firstName
59+
#"return try await client.send(path: "\#(raw: serviceName)/\#(function.name)", request: \#(argName))"#
60+
} else {
61+
#"return try await client.send(path: "\#(raw: serviceName)/\#(function.name)")"#
62+
}
63+
})
64+
}
65+
}
66+
67+
return [DeclSyntax(configureFunc), DeclSyntax(stubStruct)]
4368
}
4469
}
4570

example/Sources/Client/Gen/FoundationHTTPStubClient.gen.swift renamed to Sources/CallableKitURLSessionStub/URLSessionStubClient.swift

Lines changed: 31 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,43 @@
1-
#if !DISABLE_FOUNDATION_NETWORKING
1+
import CallableKit
22
import Foundation
33
#if canImport(FoundationNetworking)
44
@preconcurrency import FoundationNetworking
55
#endif
66

7-
enum FoundationHTTPStubClientError: Error {
8-
case unexpectedState
9-
case unexpectedStatusCode(_ code: Int)
10-
}
7+
public struct URLSessionStubClient: StubClientProtocol {
8+
public enum UnexpectedError: Error {
9+
case state
10+
case statusCode(_ code: Int)
11+
}
1112

12-
struct FoundationHTTPStubResponseError: Error, CustomStringConvertible {
13-
var path: String
14-
var body: Data
15-
var request: URLRequest
16-
var response: HTTPURLResponse
17-
var description: String {
18-
"ResponseError. path=\(path), status=\(response.statusCode)"
13+
public struct ResponseError: Error, CustomStringConvertible {
14+
public var path: String
15+
public var body: Data
16+
public var request: URLRequest
17+
public var response: HTTPURLResponse
18+
public var description: String {
19+
"ResponseError. path=\(path), status=\(response.statusCode)"
20+
}
1921
}
20-
}
2122

22-
final class FoundationHTTPStubClient: StubClientProtocol {
23-
private let baseURL: URL
24-
private let session: URLSession
25-
private let onWillSendRequest: (@Sendable (inout URLRequest) async throws -> Void)?
26-
private let mapResponseError: (@Sendable (FoundationHTTPStubResponseError) throws -> Never)?
23+
public var baseURL: URL
24+
public var session: URLSession
25+
public var onWillSendRequest: (@Sendable (inout URLRequest) async throws -> Void)?
26+
public var mapResponseError: (@Sendable (ResponseError) throws -> Never)?
2727

28-
init(
28+
public init(
2929
baseURL: URL,
30+
session: URLSession = .init(configuration: .ephemeral),
3031
onWillSendRequest: (@Sendable (inout URLRequest) async throws -> Void)? = nil,
31-
mapResponseError: (@Sendable (FoundationHTTPStubResponseError) throws -> Never)? = nil
32+
mapResponseError: (@Sendable (ResponseError) throws -> Never)? = nil
3233
) {
3334
self.baseURL = baseURL
34-
session = .init(configuration: .ephemeral)
35+
self.session = session
3536
self.onWillSendRequest = onWillSendRequest
3637
self.mapResponseError = mapResponseError
3738
}
3839

39-
func send<Req: Encodable, Res: Decodable>(
40+
public func send<Req: Encodable, Res: Decodable>(
4041
path: String,
4142
request: Req
4243
) async throws -> Res {
@@ -48,7 +49,7 @@ final class FoundationHTTPStubClient: StubClientProtocol {
4849
q.addValue("\(body.count)", forHTTPHeaderField: "Content-Length")
4950
q.httpBody = body
5051

51-
if let onWillSendRequest = onWillSendRequest {
52+
if let onWillSendRequest {
5253
try await onWillSendRequest(&q)
5354
}
5455
let (data, urlResponse) = try await session.data(for: q)
@@ -62,25 +63,25 @@ final class FoundationHTTPStubClient: StubClientProtocol {
6263
request: URLRequest
6364
) throws -> Res {
6465
guard let urlResponse = response as? HTTPURLResponse else {
65-
throw FoundationHTTPStubClientError.unexpectedState
66+
throw UnexpectedError.state
6667
}
6768

6869
if 200...299 ~= urlResponse.statusCode {
6970
return try makeDecoder().decode(Res.self, from: data)
7071
} else if 400...599 ~= urlResponse.statusCode {
71-
let error = FoundationHTTPStubResponseError(
72+
let error = ResponseError(
7273
path: path,
7374
body: data,
7475
request: request,
7576
response: urlResponse
7677
)
77-
if let mapResponseError = mapResponseError {
78+
if let mapResponseError {
7879
try mapResponseError(error)
7980
} else {
8081
throw error
8182
}
8283
} else {
83-
throw FoundationHTTPStubClientError.unexpectedStatusCode(urlResponse.statusCode)
84+
throw UnexpectedError.statusCode(urlResponse.statusCode)
8485
}
8586
}
8687
}
@@ -98,13 +99,9 @@ private func makeEncoder() -> JSONEncoder {
9899
}
99100

100101
#if canImport(FoundationNetworking)
101-
private class TaskBox: @unchecked Sendable {
102-
var task: URLSessionTask?
103-
}
104-
105102
extension URLSession {
106103
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
107-
let taskBox = TaskBox()
104+
nonisolated(unsafe) var taskBox: URLSessionTask?
108105
return try await withTaskCancellationHandler(operation: {
109106
try await withCheckedThrowingContinuation { continuation in
110107
let task = dataTask(with: request) { data, response, error in
@@ -115,13 +112,12 @@ extension URLSession {
115112
}
116113
return continuation.resume(returning: (data, response))
117114
}
118-
taskBox.task = task
115+
taskBox = task
119116
task.resume()
120117
}
121118
}, onCancel: {
122-
taskBox.task?.cancel()
119+
taskBox?.cancel()
123120
})
124121
}
125122
}
126123
#endif
127-
#endif

Sources/Codegen/Codegen.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ import CodegenImpl
33
import Foundation
44

55
@main struct Codegen: ParsableCommand {
6-
@Option(help: "generate client stub", completion: .directory)
7-
var client_out: URL?
8-
96
@Option(help: "generate client stub for typescript", completion: .directory)
107
var ts_out: URL?
118

@@ -24,7 +21,6 @@ import Foundation
2421
mutating func run() throws {
2522
try Runner(
2623
definitionDirectory: definitionDirectory,
27-
clientOut: client_out,
2824
tsOut: ts_out,
2925
module: module,
3026
dependencies: dependency,

0 commit comments

Comments
 (0)