diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 54c63f7..912c776 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,12 +13,10 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - defaults: - run: - working-directory: example steps: - uses: actions/checkout@v4 - - run: npm ci - working-directory: example/TSClient - - run: ./test.sh vapor - - run: ./test.sh hummingbird + - run: swift test + # - run: npm ci + # working-directory: example/TSClient + # - run: ./test.sh vapor + # - run: ./test.sh hummingbird diff --git a/Package.resolved b/Package.resolved index d98639b..eeb1ed4 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,240 +1,6 @@ { - "originHash" : "49e633b3248491775b3e78a39e2300b207ade2ef8c7fa57307d0e1c93efa8d02", + "originHash" : "75dbd2eff3c024141932ffa9d2095080c971b2835fefd5df9969ee3626d3229f", "pins" : [ - { - "identity" : "async-http-client", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swift-server/async-http-client.git", - "state" : { - "revision" : "2119f0d9cc1b334e25447fe43d3693c0e60e6234", - "version" : "1.24.0" - } - }, - { - "identity" : "async-kit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/vapor/async-kit.git", - "state" : { - "revision" : "e048c8ee94967e8d8a1c2ec0e1156d6f7fa34d31", - "version" : "1.20.0" - } - }, - { - "identity" : "codabletotypescript", - "kind" : "remoteSourceControl", - "location" : "https://github.com/omochi/CodableToTypeScript", - "state" : { - "revision" : "e5a24cd47970a97700223fbfa6ba5d52113a0273", - "version" : "3.0.1" - } - }, - { - "identity" : "console-kit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/vapor/console-kit.git", - "state" : { - "revision" : "966d89ae64cd71c652a1e981bc971de59d64f13d", - "version" : "4.15.1" - } - }, - { - "identity" : "hummingbird", - "kind" : "remoteSourceControl", - "location" : "https://github.com/hummingbird-project/hummingbird.git", - "state" : { - "revision" : "70b0714b4c4a192a51a9ddcc691fcf2ab3bc9141", - "version" : "2.5.0" - } - }, - { - "identity" : "multipart-kit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/vapor/multipart-kit.git", - "state" : { - "revision" : "a31236f24bfd2ea2f520a74575881f6731d7ae68", - "version" : "4.7.0" - } - }, - { - "identity" : "routing-kit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/vapor/routing-kit.git", - "state" : { - "revision" : "8c9a227476555c55837e569be71944e02a056b72", - "version" : "4.9.1" - } - }, - { - "identity" : "swift-algorithms", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-algorithms.git", - "state" : { - "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", - "version" : "1.2.0" - } - }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", - "state" : { - "revision" : "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a", - "version" : "1.2.2" - } - }, - { - "identity" : "swift-asn1", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-asn1.git", - "state" : { - "revision" : "7faebca1ea4f9aaf0cda1cef7c43aecd2311ddf6", - "version" : "1.3.0" - } - }, - { - "identity" : "swift-async-algorithms", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-async-algorithms.git", - "state" : { - "revision" : "5c8bd186f48c16af0775972700626f0b74588278", - "version" : "1.0.2" - } - }, - { - "identity" : "swift-atomics", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-atomics.git", - "state" : { - "revision" : "cd142fd2f64be2100422d658e7411e39489da985", - "version" : "1.2.0" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", - "version" : "1.1.4" - } - }, - { - "identity" : "swift-crypto", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-crypto.git", - "state" : { - "revision" : "ff0f781cf7c6a22d52957e50b104f5768b50c779", - "version" : "3.10.0" - } - }, - { - "identity" : "swift-distributed-tracing", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-distributed-tracing.git", - "state" : { - "revision" : "6483d340853a944c96dbcc28b27dd10b6c581703", - "version" : "1.1.2" - } - }, - { - "identity" : "swift-http-types", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-http-types.git", - "state" : { - "revision" : "ef18d829e8b92d731ad27bb81583edd2094d1ce3", - "version" : "1.3.1" - } - }, - { - "identity" : "swift-log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log.git", - "state" : { - "revision" : "96a2f8a0fa41e9e09af4585e2724c4e825410b91", - "version" : "1.6.2" - } - }, - { - "identity" : "swift-metrics", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-metrics.git", - "state" : { - "revision" : "e0165b53d49b413dd987526b641e05e246782685", - "version" : "2.5.0" - } - }, - { - "identity" : "swift-nio", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio.git", - "state" : { - "revision" : "dca6594f65308c761a9c409e09fbf35f48d50d34", - "version" : "2.77.0" - } - }, - { - "identity" : "swift-nio-extras", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio-extras.git", - "state" : { - "revision" : "2e9746cfc57554f70b650b021b6ae4738abef3e6", - "version" : "1.24.1" - } - }, - { - "identity" : "swift-nio-http2", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio-http2.git", - "state" : { - "revision" : "eaa71bb6ae082eee5a07407b1ad0cbd8f48f9dca", - "version" : "1.34.1" - } - }, - { - "identity" : "swift-nio-ssl", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio-ssl.git", - "state" : { - "revision" : "c7e95421334b1068490b5d41314a50e70bab23d1", - "version" : "2.29.0" - } - }, - { - "identity" : "swift-nio-transport-services", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio-transport-services.git", - "state" : { - "revision" : "bbd5e63cf949b7db0c9edaf7a21e141c52afe214", - "version" : "1.23.0" - } - }, - { - "identity" : "swift-numerics", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-numerics.git", - "state" : { - "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", - "version" : "1.0.2" - } - }, - { - "identity" : "swift-service-context", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-service-context.git", - "state" : { - "revision" : "0c62c5b4601d6c125050b5c3a97f20cce881d32b", - "version" : "1.1.0" - } - }, - { - "identity" : "swift-service-lifecycle", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swift-server/swift-service-lifecycle.git", - "state" : { - "revision" : "f70b838872863396a25694d8b19fe58bcd0b7903", - "version" : "2.6.2" - } - }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", @@ -243,51 +9,6 @@ "revision" : "0687f71944021d616d34d922343dcef086855920", "version" : "600.0.1" } - }, - { - "identity" : "swift-system", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-system.git", - "state" : { - "revision" : "c8a44d836fe7913603e246acab7c528c2e780168", - "version" : "1.4.0" - } - }, - { - "identity" : "swifttypereader", - "kind" : "remoteSourceControl", - "location" : "https://github.com/omochi/SwiftTypeReader.git", - "state" : { - "revision" : "be6697207cd9ed660499ea1a180919be8c857b8e", - "version" : "3.1.0" - } - }, - { - "identity" : "typescriptast", - "kind" : "remoteSourceControl", - "location" : "https://github.com/omochi/TypeScriptAST", - "state" : { - "revision" : "4f6420d45d6dabc79b30954ea3aa524b7810a68b", - "version" : "2.0.1" - } - }, - { - "identity" : "vapor", - "kind" : "remoteSourceControl", - "location" : "https://github.com/vapor/vapor.git", - "state" : { - "revision" : "3aeeb6fab5112fb10ce699b5a80207e5cf571c32", - "version" : "4.106.7" - } - }, - { - "identity" : "websocket-kit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/vapor/websocket-kit.git", - "state" : { - "revision" : "4232d34efa49f633ba61afde365d3896fc7f8740", - "version" : "2.15.0" - } } ], "version" : 3 diff --git a/Package.swift b/Package.swift index ebf54d5..9ba3923 100644 --- a/Package.swift +++ b/Package.swift @@ -7,38 +7,13 @@ let package = Package( name: "CallableKit", platforms: [.macOS(.v14)], products: [ - .executable(name: "codegen", targets: ["Codegen"]), .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"), - .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.1"), - .package(url: "https://github.com/omochi/CodableToTypeScript.git", from: "3.0.1"), - .package(url: "https://github.com/omochi/SwiftTypeReader.git", from: "3.1.0"), - .package(url: "https://github.com/vapor/vapor.git", from: "4.106.7"), - .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.5.0"), + .package(url: "https://github.com/swiftlang/swift-syntax.git", "600.0.0"..<"999.0.0"), ], targets: [ - .executableTarget( - name: "Codegen", - dependencies: [ - .product(name: "ArgumentParser", package: "swift-argument-parser"), - "CodegenImpl", - ] - ), - .target( - name: "CodegenImpl", - dependencies: [ - "CodableToTypeScript", - "SwiftTypeReader", - ], - swiftSettings: [ - .enableUpcomingFeature("BareSlashRegexLiterals"), - ] - ), .target( name: "CallableKit", dependencies: [ @@ -53,18 +28,11 @@ let package = Package( .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), ] ), - .target( - name: "CallableKitVaporTransport", - dependencies: [ - .product(name: "Vapor", package: "vapor"), - "CallableKit", - ] - ), - .target( - name: "CallableKitHummingbirdTransport", + .testTarget( + name: "CallableKitMacrosTests", dependencies: [ - .product(name: "Hummingbird", package: "hummingbird"), - "CallableKit", + .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), + "CallableKitMacros", ] ), .target( @@ -72,6 +40,6 @@ let package = Package( dependencies: [ "CallableKit", ] - ) + ), ] ) diff --git a/Sources/CallableKit/StubClientProtocol.swift b/Sources/CallableKit/StubClientProtocol.swift index 22b42c3..5956687 100644 --- a/Sources/CallableKit/StubClientProtocol.swift +++ b/Sources/CallableKit/StubClientProtocol.swift @@ -42,6 +42,6 @@ extension StubClientProtocol { @inlinable public func send( path: String ) async throws { - _ = try await send(path: path, request: CallableKitEmpty()) + _ = try await send(path: path, request: CallableKitEmpty()) as CallableKitEmpty } } diff --git a/Sources/CallableKitHummingbirdTransport/HummingbirdTransport.swift b/Sources/CallableKitHummingbirdTransport/HummingbirdTransport.swift deleted file mode 100644 index e20ca56..0000000 --- a/Sources/CallableKitHummingbirdTransport/HummingbirdTransport.swift +++ /dev/null @@ -1,39 +0,0 @@ -import CallableKit -import Foundation -import Hummingbird - -public struct HummingbirdTransport: ServiceTransport { - public init(router: Router, serviceBuilder: @escaping @Sendable (Request, Router.Context) async throws -> Service) { - self.router = router - self.serviceBuilder = serviceBuilder - } - - public var router: Router - public var serviceBuilder: @Sendable (Request, Router.Context) async throws -> Service - - public func register( - path: String, - methodSelector: @escaping @Sendable (Service.Type) -> (Service) -> (Request) async throws -> Response - ) { - router.post(RouterPath(path)) { [serviceBuilder] request, context in - let serviceRequest = try await makeDecoder().decode(Request.self, from: request, context: context) - let service = try await serviceBuilder(request, context) - let serviceResponse = try await methodSelector(Service.self)(service)(serviceRequest) - var response = try makeEncoder().encode(serviceResponse, from: request, context: context) - response.headers[.cacheControl] = "no-store" - return response - } - } -} - -private func makeDecoder() -> JSONDecoder { - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .millisecondsSince1970 - return decoder -} - -private func makeEncoder() -> JSONEncoder { - let encoder = JSONEncoder() - encoder.dateEncodingStrategy = .millisecondsSince1970 - return encoder -} diff --git a/Sources/CallableKitMacrosTests/CallableTests.swift b/Sources/CallableKitMacrosTests/CallableTests.swift new file mode 100644 index 0000000..55c3ee0 --- /dev/null +++ b/Sources/CallableKitMacrosTests/CallableTests.swift @@ -0,0 +1,90 @@ +import CallableKitMacros +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +final class CallableTests: XCTestCase { + let macros: [String: any Macro.Type] = [ + "Callable": CallableMacro.self, + ] + + func testBasic() { + assertMacroExpansion(""" +@Callable +public protocol EchoServiceProtocol { + func hello(request: EchoHelloRequest) async throws -> EchoHelloResponse + func testComplexType(request: TestComplexType.Request) async throws -> TestComplexType.Response + func emptyRequestAndResponse() async throws +} +""", expandedSource: """ +public protocol EchoServiceProtocol { + func hello(request: EchoHelloRequest) async throws -> EchoHelloResponse + func testComplexType(request: TestComplexType.Request) async throws -> TestComplexType.Response + func emptyRequestAndResponse() async throws +} + +public func configureEchoServiceProtocol( + transport: some ServiceTransport +) { + transport.register(path: "Echo/hello") { + $0.hello + } + transport.register(path: "Echo/testComplexType") { + $0.testComplexType + } + transport.register(path: "Echo/emptyRequestAndResponse") { + $0.emptyRequestAndResponse + } +} + +public struct EchoServiceProtocolStub: EchoServiceProtocol, Sendable { + private let client: C + public init(client: C) { + self.client = client + } + public func hello(request: EchoHelloRequest) async throws -> EchoHelloResponse { + return try await client.send(path: "Echo/hello", request: request) + } + public func testComplexType(request: TestComplexType.Request) async throws -> TestComplexType.Response { + return try await client.send(path: "Echo/testComplexType", request: request) + } + public func emptyRequestAndResponse() async throws { + return try await client.send(path: "Echo/emptyRequestAndResponse") + } +} +""", macros: macros + ) + } + + func testServiceNameSuffixTrimmed() { + assertMacroExpansion(""" +@Callable +public protocol GreetingService { + func hello() async throws -> String +} +""", expandedSource: """ +public protocol GreetingService { + func hello() async throws -> String +} + +public func configureGreetingService( + transport: some ServiceTransport +) { + transport.register(path: "Greeting/hello") { + $0.hello + } +} + +public struct GreetingServiceStub: GreetingService, Sendable { + private let client: C + public init(client: C) { + self.client = client + } + public func hello() async throws -> String { + return try await client.send(path: "Greeting/hello") + } +} +""", macros: macros + ) + } +} diff --git a/Sources/CallableKitVaporTransport/VaporTransport.swift b/Sources/CallableKitVaporTransport/VaporTransport.swift deleted file mode 100644 index 39ce1b2..0000000 --- a/Sources/CallableKitVaporTransport/VaporTransport.swift +++ /dev/null @@ -1,45 +0,0 @@ -import CallableKit -import Vapor - -public struct VaporTransport: ServiceTransport { - public init(router: any RoutesBuilder, serviceBuilder: @escaping @Sendable (Request) async throws -> Service) { - self.router = router - self.serviceBuilder = serviceBuilder - } - - public var router: any RoutesBuilder - public var serviceBuilder: @Sendable (Request) async throws -> Service - - public func register( - path: String, - methodSelector: @escaping @Sendable (Service.Type) -> (Service) -> (Request) async throws -> Response - ) { - router.post(path.pathComponents) { [serviceBuilder] request in - guard let body = request.body.data else { - throw Abort(.badRequest, reason: "no body") - } - let serviceRequest = try makeDecoder().decode(Request.self, from: body) - let service = try await serviceBuilder(request) - let serviceResponse = try await methodSelector(Service.self)(service)(serviceRequest) - - var headers = HTTPHeaders() - headers.cacheControl = .init(noStore: true) - var buffer = request.byteBufferAllocator.buffer(capacity: 0) - try makeEncoder().encode(serviceResponse, to: &buffer, headers: &headers, userInfo: [:]) - - return Vapor.Response(status: .ok, headers: headers, body: .init(buffer: buffer, byteBufferAllocator: request.byteBufferAllocator)) - } - } -} - -private func makeDecoder() -> JSONDecoder { - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .millisecondsSince1970 - return decoder -} - -private func makeEncoder() -> JSONEncoder { - let encoder = JSONEncoder() - encoder.dateEncodingStrategy = .millisecondsSince1970 - return encoder -} diff --git a/Sources/Codegen/Codegen.swift b/Sources/Codegen/Codegen.swift deleted file mode 100644 index fa8aa3b..0000000 --- a/Sources/Codegen/Codegen.swift +++ /dev/null @@ -1,36 +0,0 @@ -import ArgumentParser -import CodegenImpl -import Foundation - -@main struct Codegen: ParsableCommand { - @Option(help: "generate client stub for typescript", completion: .directory) - var ts_out: URL? - - @Option(name: .shortAndLong, help: "module name of definition") - var module: String? - - @Option(name: .shortAndLong, parsing: .singleValue, help: "directory path of dependency module source", completion: .directory) - var dependency: [URL] = [] - - @Argument(help: "directory path of definition codes", completion: .directory) - var definitionDirectory: URL - - @Flag(help: "use import decl of Next.js style") - var nextjs: Bool = false - - mutating func run() throws { - try Runner( - definitionDirectory: definitionDirectory, - tsOut: ts_out, - module: module, - dependencies: dependency, - nextjs: nextjs - ).run() - } -} - -extension URL: ArgumentParser.ExpressibleByArgument { - public init?(argument: String) { - self = URL(fileURLWithPath: argument) - } -} diff --git a/Sources/CodegenImpl/GenerateTS/GenerateTS+commonlib.swift b/Sources/CodegenImpl/GenerateTS/GenerateTS+commonlib.swift deleted file mode 100644 index d676d03..0000000 --- a/Sources/CodegenImpl/GenerateTS/GenerateTS+commonlib.swift +++ /dev/null @@ -1,126 +0,0 @@ -import Foundation -import TypeScriptAST - -extension GenerateTSClient { - func generateCommon() -> TSSourceFile { - let string = TSIdentType.string - var elements: [any ASTNode] = [ - TSInterfaceDecl(modifiers: [.export], name: "IStubClient", body: TSBlockStmt([ - TSMethodDecl( - name: "send", params: [ - .init(name: "request", type: TSIdentType.unknown), - .init(name: "servicePath", type: TSIdentType.string) - ], - result: TSIdentType.promise(TSIdentType.unknown) - ) - ])), - TSTypeDecl(modifiers: [.export], name: "Headers", type: TSIdentType("Record", genericArgs: [string, string])), - TSTypeDecl(modifiers: [.export], name: "StubClientOptions", type: TSObjectType([ - .field(TSFieldDecl(name: "headers", isOptional: true, type: TSFunctionType(params: [], result: TSUnionType([ - TSIdentType("Headers"), - TSIdentType("Promise", genericArgs: [TSIdentType("Headers")]), - ])))), - .field(TSFieldDecl(name: "mapResponseError", isOptional: true, type: TSFunctionType(params: [.init(name: "e", type: TSIdentType("FetchHTTPStubResponseError"))], result: TSIdentType.error))), - ])), - TSClassDecl(modifiers: [.export], name: "FetchHTTPStubResponseError", extends: TSIdentType("Error"), body: TSBlockStmt([ - TSFieldDecl(modifiers: [.readonly], name: "path", type: string), - TSFieldDecl(modifiers: [.readonly], name: "response", type: TSIdentType("Response")), - TSMethodDecl(name: "constructor", params: [.init(name: "path", type: string), .init(name: "response", type: TSIdentType("Response"))], body: TSBlockStmt([ - TSCallExpr(callee: TSIdentExpr("super"), args: [TSTemplateLiteralExpr("ResponseError. path=\(ident: "path"), status=\(TSMemberExpr(base: TSIdentExpr("response"), name: "status"))")]), - TSAssignExpr(TSMemberExpr(base: TSIdentExpr.this, name: "path"), TSIdentExpr("path")), - TSAssignExpr(TSMemberExpr(base: TSIdentExpr.this, name: "response"), TSIdentExpr("response")), - ])), - ])), - TSVarDecl( - modifiers: [.export], - kind: .const, - name: "createStubClient", - initializer: TSClosureExpr( - params: [ - .init(name: "baseURL", type: string), - .init(name: "options", isOptional: true, type: TSIdentType("StubClientOptions")), - ], - result: TSIdentType("IStubClient"), - body: TSBlockStmt([ - TSReturnStmt(TSObjectExpr([ - .method(TSMethodDecl(modifiers: [.async], name: "send", params: [.init(name: "request"), .init(name: "servicePath")], body: TSBlockStmt([ - TSVarDecl(kind: .let, name: "optionHeaders", type: TSIdentType("Headers"), initializer: TSObjectExpr([])), - TSIfStmt(condition: TSMemberExpr(base: TSIdentExpr("options"), isOptional: true, name: "headers"), then: TSBlockStmt([ - TSAssignExpr( - TSIdentExpr("optionHeaders"), - TSAwaitExpr(TSCallExpr(callee: TSMemberExpr(base: TSIdentExpr("options"), name: "headers"), args: [])) - ), - ])), - - TSVarDecl(kind: .const, name: "res", initializer: TSAwaitExpr(TSCallExpr(callee: TSIdentExpr("fetch"), args: [ - TSCallExpr(callee: TSMemberExpr( - base: TSNewExpr(callee: TSIdentType("URL"), args: [ - TSIdentExpr("servicePath"), - TSIdentExpr("baseURL"), - ]), - name: "toString" - ), args: []), - TSObjectExpr([ - .named(name: "method", value: TSStringLiteralExpr("POST")), - .named(name: "headers", value: TSObjectExpr([ - .named(name: "Content-Type", value: TSStringLiteralExpr("application/json")), - .destructuring(value: TSIdentExpr("optionHeaders")), - ])), - .named(name: "body", value: TSCallExpr(callee: TSMemberExpr(base: TSIdentExpr("JSON"), name: "stringify"), args: [TSIdentExpr("request")])), - ]), - ]))), - TSIfStmt(condition: TSPrefixOperatorExpr("!", TSMemberExpr(base: TSIdentExpr("res"), name: "ok")), then: TSBlockStmt([ - TSVarDecl(kind: .const, name: "e", initializer: TSNewExpr(callee: TSIdentType("FetchHTTPStubResponseError"), args: [ - TSIdentExpr("servicePath"), - TSIdentExpr("res"), - ])), - TSIfStmt( - condition: TSMemberExpr(base: TSIdentExpr("options"), isOptional: true, name: "mapResponseError"), - then: TSBlockStmt([ - TSThrowStmt(TSCallExpr(callee: TSMemberExpr(base: TSIdentExpr("options"), name: "mapResponseError"), args: [TSIdentExpr("e")])), - ]), - else: TSBlockStmt([ - TSThrowStmt(TSIdentExpr("e")), - ]) - ), - ])), - TSReturnStmt(TSAwaitExpr(TSCallExpr(callee: TSMemberExpr(base: TSIdentExpr("res"), name: "json"), args: []))), - ]))) - ])) - ]) - ) - ) - ] - - elements += [ - DateConvertDecls.encodeDecl(), - DateConvertDecls.decodeDecl(), - ] - - return TSSourceFile(elements) - } -} - -fileprivate enum DateConvertDecls { - static func decodeDecl() -> TSFunctionDecl { - TSFunctionDecl( - modifiers: [.export], - name: "Date_decode", - params: [ .init(name: "unixMilli", type: TSIdentType("number"))], - body: TSBlockStmt([ - TSReturnStmt(TSNewExpr(callee: TSIdentType("Date"), args: [TSIdentExpr("unixMilli")])) - ]) - ) - } - - static func encodeDecl() -> TSFunctionDecl { - TSFunctionDecl( - modifiers: [.export], - name: "Date_encode", - params: [.init(name: "d", type: TSIdentType("Date"))], - body: TSBlockStmt([ - TSReturnStmt(TSCallExpr(callee: TSMemberExpr(base: TSIdentExpr("d"), name: "getTime"), args: [])) - ]) - ) - } -} diff --git a/Sources/CodegenImpl/GenerateTSClient.swift b/Sources/CodegenImpl/GenerateTSClient.swift deleted file mode 100644 index 90b4d85..0000000 --- a/Sources/CodegenImpl/GenerateTSClient.swift +++ /dev/null @@ -1,317 +0,0 @@ -import CodableToTypeScript -import Foundation -import SwiftTypeReader -import TypeScriptAST - -fileprivate struct SourceEntry { - var file: String - var source: TSSourceFile -} - -struct GenerateTSClient { - var definitionModule: String - var srcDirectory: URL - var dstDirectory: URL - var dependencies: [URL] - var nextjs: Bool - var `extension`: String = "gen.ts" - - private let typeMap: TypeMap = { - var typeMapTable: [String: TypeMap.Entry] = TypeMap.defaultTable - typeMapTable["URL"] = .identity(name: "string") - typeMapTable["Date"] = .coding(entityType: "Date", jsonType: "number", decode: "Date_decode", encode: "Date_encode") - typeMapTable["UUID"] = .identity(name: "string") - return TypeMap(table: typeMapTable) - }() - - private func processFile( - generator: CodeGenerator, - swift: SourceFile, - ts: PackageEntry - ) throws { - var insertionIndex: Int = ts.source.elements.lastIndex(where: { $0 is TSImportDecl }) ?? 0 - - for stype in swift.types.compactMap(ServiceProtocolScanner.scan) { - let clientInterface = TSInterfaceDecl( - modifiers: [.export], - name: "I\(stype.serviceName)Client", - body: TSBlockStmt(try stype.functions.map { (f) in - let res: any TSType = try f.response.map { try generator.converter(for: $0.raw).type(for: .entity) } ?? TSIdentType.void - let method = TSMethodDecl( - name: f.name, - params: try f.request.map { - [.init(name: $0.argName, type: try generator.converter(for: $0.raw).type(for: .entity))] - } ?? [], - result: TSIdentType.promise(res) - ) - return method - }) - ) - ts.source.elements.insert(clientInterface, at: insertionIndex) - insertionIndex += 1 - - let functionsDecls: [TSMethodDecl] = try stype.functions.map { f in - let res: TSType = try f.response.map { try generator.converter(for: $0.raw).type(for: .entity) } ?? TSIdentType.void - - let reqExpr: any TSExpr - if let sReq = f.request?.raw, - try generator.converter(for: sReq).hasEncode() { - let encodeExpr = try generator.converter(for: sReq).callEncode(entity: TSIdentExpr(f.request!.argName)) - reqExpr = encodeExpr - } else { - reqExpr = f.request.map { TSIdentExpr($0.argName) } ?? TSObjectExpr([]) - } - - let fetchExpr: any TSExpr = TSCallExpr( - callee: TSMemberExpr( - base: TSIdentExpr("stub"), - name: "send" - ), - args: [ - reqExpr, - TSStringLiteralExpr("\(stype.serviceName)/\(f.name)") - ] - ) - - let blockBody: [any ASTNode] - if let sRes = f.response?.raw, - try generator.converter(for: sRes).hasDecode() - { - let jsonTsType = try generator.converter(for: sRes).type(for: .json) - let decodeExpr = try generator.converter(for: sRes).callDecode(json: TSIdentExpr("json")) - - blockBody = [ - TSVarDecl( - kind: .const, name: "json", - initializer: TSAwaitExpr( - TSAsExpr(fetchExpr, jsonTsType) - ) - ), - TSReturnStmt(decodeExpr) - ] - } else { - blockBody = [ - TSReturnStmt( - TSAwaitExpr( - TSAsExpr(fetchExpr, res) - ) - ) - ] - } - - return TSMethodDecl( - modifiers: [.async], - name: f.name, - params: try f.request.map { - [.init( - name: $0.argName, - type: try generator.converter(for: $0.raw).type(for: .entity) - )] - } ?? [], - result: TSIdentType.promise(res), - body: TSBlockStmt(blockBody) - ) - } - - let bindDecl = TSVarDecl( - modifiers: [.export], - kind: .const, name: "bind\(stype.serviceName)", - initializer: TSClosureExpr( - params: [.init(name: "stub", type: TSIdentType("IStubClient"))], - result: TSIdentType(clientInterface.name), - body: TSBlockStmt([ - TSReturnStmt(TSObjectExpr(functionsDecls.map(TSObjectExpr.Field.method))) - ]) - ) - ) - - ts.source.elements.insert(bindDecl, at: insertionIndex) - insertionIndex += 1 - } - } - - func run() throws { - let g = Generator( - definitionModule: definitionModule, - srcDirectory: srcDirectory, - dstDirectory: dstDirectory, - dependencies: dependencies, - isOutputFileName: { [ext = self.extension] (name) in - name.hasSuffix(ext) - } - ) - - try g.run { input, write in - var symbols = SymbolTable( - standardLibrarySymbols: SymbolTable.standardLibrarySymbols.union([ - "Response", - "Object", - "JSON", - "URL", - "fetch", - ]) - ) - - let commonLib = PackageEntry( - file: dstDirectory.appendingPathComponent("CallableKit.\(`extension`)"), - source: generateCommon() - ) - - symbols.add(source: commonLib.source, file: commonLib.file) - - let package = PackageGenerator( - context: input.context, - typeConverterProvider: TypeConverterProvider(typeMap: typeMap, customProvider: { (generator, stype) in - if let rawValueType = stype.asStruct?.rawValueType(requiresTransferringRawValueType: false) { - var transferringRawValue = false - if let comment = stype.asNominal?.nominalTypeDecl.comment, - let match = comment.firstMatch(of: /@CallableKit\((.+?)\)/) { - let options = extractOptions(match.output.1) - if options["transferringRawValue"] == "true" { - transferringRawValue = true - } - } - - return try? FlatRawRepresentableConverter( - generator: generator, - swiftType: stype, - rawValueType: rawValueType, - transferringRawValue: transferringRawValue - ) - } - - return nil - }), - symbols: symbols, - importFileExtension: nextjs ? .none : .js, - outputDirectory: dstDirectory, - typeScriptExtension: `extension` - ) - package.didConvertSource = { [unowned package] (source, entry) in - try self.processFile( - generator: package.codeGenerator, - swift: source, - ts: entry - ) - } - - var modules = input.context.modules - modules.removeAll { $0 === input.context.swiftModule } - var entries = try package.generate(modules: modules).entries - entries.removeAll(where: { $0.source.elements.isEmpty }) - entries.append(commonLib) - - for entry in entries { - try write(file: toOutputFile(entry: entry)) - } - } - } - - private func toOutputFile(entry: PackageEntry) -> Generator.OutputFile { - return Generator.OutputFile( - name: URLs.relativePath(to: entry.file, from: dstDirectory).relativePath, - content: entry.print() - ) - } -} - -// TS側で.rawValueを経由せず直接RawValueで扱えるようにするコンバータ -struct FlatRawRepresentableConverter: TypeConverter { - init( - generator: CodeGenerator, - swiftType: any SType, - rawValueType substituted: any SType, - transferringRawValue: Bool - ) throws { - self.generator = generator - self.swiftType = swiftType - self.rawValueType = try generator.converter(for: substituted) - self.isTransferringRawValueType = swiftType.asStruct?.rawValueType(requiresTransferringRawValueType: true) != nil - || transferringRawValue - } - - var generator: CodeGenerator - var swiftType: any SType - var rawValueType: any TypeConverter - var isTransferringRawValueType: Bool - - func typeDecl(for target: GenerationTarget) throws -> TSTypeDecl? { - let name = try self.name(for: target) - let genericParams: [TSTypeParameterNode] = try self.genericParams().map { - .init(try $0.name(for: target)) - } - let type = try rawValueType.type(for: target) - switch target { - case .entity: - let tag = try generator.tagRecord( - name: name, - genericArgs: try self.genericParams().map { (param) in - TSIdentType(try param.name(for: .entity)) - } - ) - - return TSTypeDecl( - modifiers: [.export], - name: name, - genericParams: genericParams, - type: TSIntersectionType([type, tag]) - ) - case .json: - if isTransferringRawValueType { - return nil - } - return TSTypeDecl( - modifiers: [.export], - name: name, - genericParams: genericParams, - type: TSObjectType([ - .field(.init(name: "rawValue", type: type)) - ]) - ) - } - } - - func hasDecode() -> Bool { - return !isTransferringRawValueType - } - - func decodeDecl() throws -> TSFunctionDecl? { - guard let decl = try decodeSignature() else { return nil } - assert(!isTransferringRawValueType) - - let value = try rawValueType.callDecode(json: TSMemberExpr(base: TSIdentExpr("json"), name: "rawValue")) - let field = try rawValueType.valueToField(value: value, for: .entity) - - decl.body.elements.append( - TSReturnStmt(TSAsExpr(field, try type(for: .entity))) - ) - return decl - } - - func hasEncode() -> Bool { - return !isTransferringRawValueType - } - - func encodeDecl() throws -> TSFunctionDecl? { - guard let decl = try encodeSignature() else { return nil } - assert(!isTransferringRawValueType) - - let field = try rawValueType.callEncodeField(entity: TSIdentExpr("entity")) - let value = try rawValueType.fieldToValue(field: field, for: .json) - decl.body.elements.append( - TSReturnStmt(TSObjectExpr([ - .named(name: "rawValue", value: value), - ])) - ) - return decl - } -} - -fileprivate func extractOptions(_ parametersString: some StringProtocol) -> [Substring: Substring] { - let keyAndValues = parametersString - .split(separator: ",") - .map({ $0.trimmingCharacters(in: .whitespacesAndNewlines) }) - .compactMap({ $0.wholeMatch(of: /(\w+)\s*:\s*(.*)/) }) - .map({ ($0.output.1, $0.output.2) }) - return [Substring: Substring](uniqueKeysWithValues: keyAndValues) -} diff --git a/Sources/CodegenImpl/Generator.swift b/Sources/CodegenImpl/Generator.swift deleted file mode 100644 index 534f33e..0000000 --- a/Sources/CodegenImpl/Generator.swift +++ /dev/null @@ -1,105 +0,0 @@ -import Foundation -import SwiftTypeReader - -struct Generator { - var definitionModule: String - var srcDirectory: URL - var dstDirectory: URL - var dependencies: [URL] - var fileManager = FileManager() - var isOutputFileName: ((_ filename: String) -> Bool)? - - typealias InputFile = SwiftTypeReader.SourceFile - - struct OutputFile { - var name: String - var content: String - } - - struct Input { - var context: SwiftTypeReader.Context - var files: [InputFile] - } - - class OutputSink { - public init(dstDirectory: URL, fileManager: FileManager) { - self.dstDirectory = dstDirectory.absoluteURL.standardized - self.fileManager = fileManager - } - - let dstDirectory: URL - let fileManager: FileManager - private var writtenFiles: Set = [] - - func path(name: String) -> URL { - dstDirectory.appendingPathComponent(name).absoluteURL.standardized - } - - func isWritten(name: String) -> Bool { - writtenFiles.contains(path(name: name)) - } - - func callAsFunction(file: OutputFile) throws { - let dst = self.path(name: file.name) - try fileManager.createDirectory(at: dst.deletingLastPathComponent(), withIntermediateDirectories: true) - try file.content.data(using: .utf8)!.write(to: dst, options: .atomic) - print("generated...", file.name) - writtenFiles.insert(dst) - } - } - - func run(_ perform: (Input, _ write: OutputSink) throws -> ()) throws { - let context = SwiftTypeReader.Context() - - var inputFiles: [InputFile] = [] - let sources = try SwiftTypeReader.Reader( - context: context, - module: context.getOrCreateModule(name: definitionModule) - ) - .read(directory: srcDirectory) - inputFiles.append(contentsOf: sources) - for dependency in dependencies { - let module = context.getOrCreateModule( - name: detectModuleName(dir: dependency) ?? dependency.lastPathComponent - ) - let sources = try SwiftTypeReader.Reader(context: context,module: module) - .read(directory: dependency) - inputFiles.append(contentsOf: sources) - } - - let input = Input(context: context, files: inputFiles) - let sink = OutputSink(dstDirectory: dstDirectory, fileManager: fileManager) - try fileManager.createDirectory(at: dstDirectory, withIntermediateDirectories: true) - try perform(input, sink) - - // リネームなどによって不要になった生成物を出力ディレクトリから削除 - for dstFile in try findFiles(in: dstDirectory) { - if let isOutputFileName = isOutputFileName, !isOutputFileName(URL(fileURLWithPath: dstFile).lastPathComponent) { - continue - } - if !sink.isWritten(name: dstFile) { - try fileManager.removeItem(at: sink.path(name: dstFile)) - print("removed...", dstFile) - } - } - // 空のディレクトリを削除 - for dstFile in try fileManager.subpathsOfDirectory(atPath: dstDirectory.path) { - let path = sink.path(name: dstFile) - var isDir: ObjCBool = false - if fileManager.fileExists(atPath: path.path, isDirectory: &isDir), - isDir.boolValue, - try fileManager.contentsOfDirectory(atPath: path.path).isEmpty - { - try fileManager.removeItem(at: path) - print("removed...", dstFile) - } - } - } - - private func findFiles(in directory: URL) throws -> [String] { - try fileManager.subpathsOfDirectory(atPath: directory.path) - .filter { - !fileManager.isDirectory(at: directory.appendingPathComponent($0)) - } - } -} diff --git a/Sources/CodegenImpl/Runner.swift b/Sources/CodegenImpl/Runner.swift deleted file mode 100644 index 7ad2ad3..0000000 --- a/Sources/CodegenImpl/Runner.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation - -public struct Runner { - public init(definitionDirectory: URL, tsOut: URL? = nil, module: String? = nil, dependencies: [URL] = [], nextjs: Bool = false) { - self.definitionDirectory = definitionDirectory - self.tsOut = tsOut - self.module = module - self.dependencies = dependencies - self.nextjs = nextjs - } - - public var definitionDirectory: URL - public var tsOut: URL? - public var module: String? - public var dependencies: [URL] = [] - public var nextjs: Bool = false - - public func run() throws { - guard let module = module ?? detectModuleName(dir: definitionDirectory) else { - throw MessageError("definitionsModuleNameNotFound") - } - - if let tsOut { - try GenerateTSClient( - definitionModule: module, - srcDirectory: definitionDirectory, - dstDirectory: tsOut, - dependencies: dependencies, - nextjs: nextjs - ).run() - } - } -} diff --git a/Sources/CodegenImpl/ServiceProtocolScanner.swift b/Sources/CodegenImpl/ServiceProtocolScanner.swift deleted file mode 100644 index d905885..0000000 --- a/Sources/CodegenImpl/ServiceProtocolScanner.swift +++ /dev/null @@ -1,90 +0,0 @@ -import Foundation -import SwiftTypeReader - -struct ServiceProtocolType { - var name: String - var serviceName: String - - struct Function { - var name: String - struct Request { - var argOuterName: String? - var argName: String - var typeName: String - var raw: any SType - } - var request: Request? - struct Response { - var typeName: String - var raw: any SType - } - var response: Response? - var hasRequest: Bool { - request != nil - } - var hasResponse: Bool { - response != nil - } - var raw: FuncDecl - } - var functions: [Function] - var raw: ProtocolType -} - -enum ServiceProtocolScanner { - static func scan(_ decl: any TypeDecl) -> ServiceProtocolType? { - scan(decl.declaredInterfaceType) - } - - static func scan(_ stype: any SType) -> ServiceProtocolType? { - guard let ptype = stype as? ProtocolType, - ptype.decl.attributes.contains(where: { $0.name == "Callable" }), - !ptype.decl.functions.isEmpty - else { return nil } - - let serviceName = ptype.name.trimmingSuffix("Protocol").trimmingSuffix("Service") - - let functions = ptype.decl.functions.compactMap { fdecl -> ServiceProtocolType.Function? in - guard fdecl.parameters.count <= 1 else { - print("⚠ the number of arguments must be zero or one. \(ptype.name).\(fdecl.name) is ignored.") - return nil - } - - return ServiceProtocolType.Function( - name: fdecl.name, - request: fdecl.parameters.first.map { - .init(argOuterName: $0.outerName, argName: $0.name!, typeName: $0.interfaceType.description, raw: $0.interfaceType) - }, - response: fdecl.resultTypeRepr.map { - .init( - typeName: $0.description, - raw: $0.resolve(from: fdecl) - ) - }, - raw: fdecl - ) - } - - if functions.isEmpty { - return nil - } - - return ServiceProtocolType( - name: ptype.name, - serviceName: serviceName, - functions: functions, - raw: ptype - ) - } -} - -extension String { - fileprivate func trimmingSuffix(_ suffix: String) -> String { - if self.hasSuffix(suffix) { - var copy = self - copy.removeLast(suffix.count) - return copy - } - return self - } -} diff --git a/Sources/CodegenImpl/Utils.swift b/Sources/CodegenImpl/Utils.swift deleted file mode 100644 index bfba4ab..0000000 --- a/Sources/CodegenImpl/Utils.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Foundation - -extension FileManager { - func isDirectory(at url: URL) -> Bool { - var isDir: ObjCBool = false - if fileExists(atPath: url.path, isDirectory: &isDir) { - return isDir.boolValue - } - return false - } -} - -struct UnitStringType: - RawRepresentable, - Equatable, - Hashable, - CustomStringConvertible, - LosslessStringConvertible -{ - init(_ rawValue: String) { - self.rawValue = rawValue - } - init(rawValue: String) { - self.rawValue = rawValue - } - - var rawValue: String - - var description: String { - rawValue.description - } -} - -struct MessageError: Error, CustomStringConvertible { - var description: String - init(_ description: String) { - self.description = description - } -} - -func detectModuleName(dir: URL) -> String? { - dir - .resolvingSymlinksInPath() - .pathComponents - .eachPairs() - .first { (f, s) in - f == "Sources" - } - .map(\.1) -} - -extension Sequence { - func eachPairs() -> AnySequence<(Element, Element)> { - AnySequence( - zip(self, self.dropFirst()) - ) - } -}