Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ let package = Package(
.library(name: "CallableKit", targets: ["CallableKit"]),
.library(name: "CallableKitVaporTransport", targets: ["CallableKitVaporTransport"]),
.library(name: "CallableKitHummingbirdTransport", targets: ["CallableKitHummingbirdTransport"]),
.library(name: "CallableKitURLSessionStub", targets: ["CallableKitURLSessionStub"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.2"),
Expand Down Expand Up @@ -65,6 +66,12 @@ let package = Package(
.product(name: "Hummingbird", package: "hummingbird"),
"CallableKit",
]
),
.target(
name: "CallableKitURLSessionStub",
dependencies: [
"CallableKit",
]
)
]
)
3 changes: 3 additions & 0 deletions Sources/CallableKit/CallableKitEmpty.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@usableFromInline internal struct CallableKitEmpty: Codable {
@usableFromInline init() {}
}
2 changes: 1 addition & 1 deletion Sources/CallableKit/Macros.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@attached(
peer,
names: prefixed(configure)
names: prefixed(configure), suffixed(Stub)
)
public macro Callable() = #externalMacro(module: "CallableKitMacros", type: "CallableMacro")
19 changes: 8 additions & 11 deletions Sources/CallableKit/ServiceTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,46 +22,43 @@ public protocol ServiceTransport<Service> {
)
}

fileprivate struct _Empty: Codable, Sendable {
}

extension ServiceTransport {
public func register<Request: Decodable>(
@inlinable public func register<Request: Decodable>(
path: String,
methodSelector: @escaping @Sendable (Service.Type) -> (Service) -> (Request) async throws -> Void
) {
register(path: path) { (serviceType) in
{ (service: Service) in
{ (request: Request) -> _Empty in
{ (request: Request) -> CallableKitEmpty in
try await methodSelector(serviceType)(service)(request)
return _Empty()
return CallableKitEmpty()
}
}
}
}

public func register<Response: Encodable>(
@inlinable public func register<Response: Encodable>(
path: String,
methodSelector: @escaping @Sendable (Service.Type) -> (Service) -> () async throws -> Response
) {
register(path: path) { (serviceType) in
{ (service: Service) in
{ (_: _Empty) -> Response in
{ (_: CallableKitEmpty) -> Response in
return try await methodSelector(serviceType)(service)()
}
}
}
}

public func register(
@inlinable public func register(
path: String,
methodSelector: @escaping @Sendable (Service.Type) -> (Service) -> () async throws -> Void
) {
register(path: path) { (serviceType) in
{ (service: Service) in
{ (_: _Empty) -> _Empty in
{ (_: CallableKitEmpty) -> CallableKitEmpty in
try await methodSelector(serviceType)(service)()
return _Empty()
return CallableKitEmpty()
}
}
}
Expand Down
47 changes: 47 additions & 0 deletions Sources/CallableKit/StubClientProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
public protocol StubClientProtocol: Sendable {
func send<Request: Encodable & Sendable, Response: Decodable & Sendable>(
path: String,
request: Request
) async throws -> Response

func send<Request: Encodable & Sendable>(
path: String,
request: Request
) async throws

func send<Response: Decodable & Sendable>(
path: String
) async throws -> Response

func send(
path: String
) async throws
}

extension StubClientProtocol {
@inlinable public func send<Request: Encodable & Sendable, Response: Decodable & Sendable>(
path: String,
request: Request
) async throws -> Response {
try await send(path: path, request: request)
}

@inlinable public func send<Request: Encodable & Sendable>(
path: String,
request: Request
) async throws {
_ = try await send(path: path, request: request) as CallableKitEmpty
}

@inlinable public func send<Response: Decodable & Sendable>(
path: String
) async throws -> Response {
try await send(path: path, request: CallableKitEmpty())
}

@inlinable public func send(
path: String
) async throws {
_ = try await send(path: path, request: CallableKitEmpty())
}
}
27 changes: 26 additions & 1 deletion Sources/CallableKitMacros/CallableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,32 @@ public struct CallableMacro: PeerMacro {
}
}

return [DeclSyntax(configureFunc)]
let stubStruct = try StructDeclSyntax("public struct \(raw: protocolName)Stub<C: StubClientProtocol>: \(raw: protocolName), Sendable") {
VariableDeclSyntax(
modifiers: [.init(name: .keyword(.private))],
.let,
name: "client" as PatternSyntax,
type: TypeAnnotationSyntax(type: "C" as TypeSyntax)
)
try InitializerDeclSyntax("public init(client: C)") {
"self.client = client"
}
for function in functions {
function
.with(\.leadingTrivia, [])
.with(\.modifiers, [.init(name: .keyword(.public))])
.with(\.body, CodeBlockSyntax {
if let param = function.signature.parameterClause.parameters.first {
let argName = param.secondName ?? param.firstName
#"return try await client.send(path: "\#(raw: serviceName)/\#(function.name)", request: \#(argName))"#
} else {
#"return try await client.send(path: "\#(raw: serviceName)/\#(function.name)")"#
}
})
}
}

return [DeclSyntax(configureFunc), DeclSyntax(stubStruct)]
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,43 @@
#if !DISABLE_FOUNDATION_NETWORKING
import CallableKit
import Foundation
#if canImport(FoundationNetworking)
@preconcurrency import FoundationNetworking
#endif

enum FoundationHTTPStubClientError: Error {
case unexpectedState
case unexpectedStatusCode(_ code: Int)
}
public struct URLSessionStubClient: StubClientProtocol {
public enum UnexpectedError: Error {
case state
case statusCode(_ code: Int)
}

struct FoundationHTTPStubResponseError: Error, CustomStringConvertible {
var path: String
var body: Data
var request: URLRequest
var response: HTTPURLResponse
var description: String {
"ResponseError. path=\(path), status=\(response.statusCode)"
public struct ResponseError: Error, CustomStringConvertible {
public var path: String
public var body: Data
public var request: URLRequest
public var response: HTTPURLResponse
public var description: String {
"ResponseError. path=\(path), status=\(response.statusCode)"
}
}
}

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

init(
public init(
baseURL: URL,
session: URLSession = .init(configuration: .ephemeral),
onWillSendRequest: (@Sendable (inout URLRequest) async throws -> Void)? = nil,
mapResponseError: (@Sendable (FoundationHTTPStubResponseError) throws -> Never)? = nil
mapResponseError: (@Sendable (ResponseError) throws -> Never)? = nil
) {
self.baseURL = baseURL
session = .init(configuration: .ephemeral)
self.session = session
self.onWillSendRequest = onWillSendRequest
self.mapResponseError = mapResponseError
}

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

if let onWillSendRequest = onWillSendRequest {
if let onWillSendRequest {
try await onWillSendRequest(&q)
}
let (data, urlResponse) = try await session.data(for: q)
Expand All @@ -62,25 +63,25 @@ final class FoundationHTTPStubClient: StubClientProtocol {
request: URLRequest
) throws -> Res {
guard let urlResponse = response as? HTTPURLResponse else {
throw FoundationHTTPStubClientError.unexpectedState
throw UnexpectedError.state
}

if 200...299 ~= urlResponse.statusCode {
return try makeDecoder().decode(Res.self, from: data)
} else if 400...599 ~= urlResponse.statusCode {
let error = FoundationHTTPStubResponseError(
let error = ResponseError(
path: path,
body: data,
request: request,
response: urlResponse
)
if let mapResponseError = mapResponseError {
if let mapResponseError {
try mapResponseError(error)
} else {
throw error
}
} else {
throw FoundationHTTPStubClientError.unexpectedStatusCode(urlResponse.statusCode)
throw UnexpectedError.statusCode(urlResponse.statusCode)
}
}
}
Expand All @@ -98,13 +99,9 @@ private func makeEncoder() -> JSONEncoder {
}

#if canImport(FoundationNetworking)
private class TaskBox: @unchecked Sendable {
var task: URLSessionTask?
}

extension URLSession {
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
let taskBox = TaskBox()
nonisolated(unsafe) var taskBox: URLSessionTask?
return try await withTaskCancellationHandler(operation: {
try await withCheckedThrowingContinuation { continuation in
let task = dataTask(with: request) { data, response, error in
Expand All @@ -115,13 +112,12 @@ extension URLSession {
}
return continuation.resume(returning: (data, response))
}
taskBox.task = task
taskBox = task
task.resume()
}
}, onCancel: {
taskBox.task?.cancel()
taskBox?.cancel()
})
}
}
#endif
#endif
4 changes: 0 additions & 4 deletions Sources/Codegen/Codegen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ import CodegenImpl
import Foundation

@main struct Codegen: ParsableCommand {
@Option(help: "generate client stub", completion: .directory)
var client_out: URL?

@Option(help: "generate client stub for typescript", completion: .directory)
var ts_out: URL?

Expand All @@ -24,7 +21,6 @@ import Foundation
mutating func run() throws {
try Runner(
definitionDirectory: definitionDirectory,
clientOut: client_out,
tsOut: ts_out,
module: module,
dependencies: dependency,
Expand Down
Loading
Loading