From b701955fba42765856e7c3b82f3e7b114c55586a Mon Sep 17 00:00:00 2001 From: Si Beaumont Date: Mon, 15 Jul 2024 09:59:16 +0100 Subject: [PATCH] Add example package using shared types in client and server --- .../.gitignore | 11 ++++ .../Package.swift | 55 +++++++++++++++++++ .../README.md | 40 ++++++++++++++ .../Client/HelloWorldURLSessionClient.swift | 24 ++++++++ .../Client/openapi-generator-config.yaml | 5 ++ .../Sources/Client/openapi.yaml | 1 + .../Server/HelloWorldHummingbirdServer.swift | 36 ++++++++++++ .../Server/openapi-generator-config.yaml | 5 ++ .../Sources/Server/openapi.yaml | 1 + .../Sources/Types/Extensions.swift | 41 ++++++++++++++ .../Types/openapi-generator-config.yaml | 3 + .../Sources/Types/openapi.yaml | 1 + .../Sources/openapi.yaml | 36 ++++++++++++ 13 files changed, 259 insertions(+) create mode 100644 Examples/shared-types-client-server-example/.gitignore create mode 100644 Examples/shared-types-client-server-example/Package.swift create mode 100644 Examples/shared-types-client-server-example/README.md create mode 100644 Examples/shared-types-client-server-example/Sources/Client/HelloWorldURLSessionClient.swift create mode 100644 Examples/shared-types-client-server-example/Sources/Client/openapi-generator-config.yaml create mode 120000 Examples/shared-types-client-server-example/Sources/Client/openapi.yaml create mode 100644 Examples/shared-types-client-server-example/Sources/Server/HelloWorldHummingbirdServer.swift create mode 100644 Examples/shared-types-client-server-example/Sources/Server/openapi-generator-config.yaml create mode 120000 Examples/shared-types-client-server-example/Sources/Server/openapi.yaml create mode 100644 Examples/shared-types-client-server-example/Sources/Types/Extensions.swift create mode 100644 Examples/shared-types-client-server-example/Sources/Types/openapi-generator-config.yaml create mode 120000 Examples/shared-types-client-server-example/Sources/Types/openapi.yaml create mode 100644 Examples/shared-types-client-server-example/Sources/openapi.yaml diff --git a/Examples/shared-types-client-server-example/.gitignore b/Examples/shared-types-client-server-example/.gitignore new file mode 100644 index 00000000..f6f5465e --- /dev/null +++ b/Examples/shared-types-client-server-example/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.vscode +/Package.resolved +.ci/ +.docc-build/ diff --git a/Examples/shared-types-client-server-example/Package.swift b/Examples/shared-types-client-server-example/Package.swift new file mode 100644 index 00000000..a5b2710b --- /dev/null +++ b/Examples/shared-types-client-server-example/Package.swift @@ -0,0 +1,55 @@ +// swift-tools-version:5.9 +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import PackageDescription + +let package = Package( + name: "shared-types-client-server-example", + platforms: [.macOS(.v13)], + products: [ + .executable(name: "hello-world-client", targets: ["Client"]), + .executable(name: "hello-world-server", targets: ["Server"]), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-openapi-generator", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.0.0"), + .package(url: "https://github.com/swift-server/swift-openapi-hummingbird", from: "1.0.0"), + ], + targets: [ + .target( + name: "Types", + dependencies: [.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime")], + plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")] + ), + .executableTarget( + name: "Client", + dependencies: [ + "Types", + .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), + .product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"), + ], + plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")] + ), + .executableTarget( + name: "Server", + dependencies: [ + "Types", + .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), + .product(name: "OpenAPIHummingbird", package: "swift-openapi-hummingbird"), + ], + plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")] + ), + ] +) diff --git a/Examples/shared-types-client-server-example/README.md b/Examples/shared-types-client-server-example/README.md new file mode 100644 index 00000000..c0037395 --- /dev/null +++ b/Examples/shared-types-client-server-example/README.md @@ -0,0 +1,40 @@ +# Common types between client and server modules + +An example project using [Swift OpenAPI Generator](https://github.com/apple/swift-openapi-generator). + +> **Disclaimer:** This example is deliberately simplified and is intended for illustrative purposes only. + +## Overview + +This example shows how you can structure a Swift package to share the types +from an OpenAPI document between a client and server module by having a common +target that runs the generator in `types` mode only. + +This allows you to write extensions or other helper functions that use these +types and use them in both the client and server code. + +## Usage + +Build and run the server using: + +```console +% swift run hello-world-server +Build complete! +... +info HummingBird : [HummingbirdCore] Server started and listening on 127.0.0.1:8080 +``` + +Then, in another terminal window, run the client: + +```console +% swift run hello-world-client +Build complete! ++––––––––––––––––––+ +|+––––––––––––––––+| +||Hello, Stranger!|| +|+––––––––––––––––+| ++––––––––––––––––––+ +``` + +Note how the message is boxed twice: once by the server and once by the client, +both using an extension on a shared type, defined in the `Types` module. diff --git a/Examples/shared-types-client-server-example/Sources/Client/HelloWorldURLSessionClient.swift b/Examples/shared-types-client-server-example/Sources/Client/HelloWorldURLSessionClient.swift new file mode 100644 index 00000000..74252246 --- /dev/null +++ b/Examples/shared-types-client-server-example/Sources/Client/HelloWorldURLSessionClient.swift @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import OpenAPIRuntime +import OpenAPIURLSession +import Foundation + +@main struct HelloWorldURLSessionClient { + static func main() async throws { + let client = Client(serverURL: URL(string: "http://localhost:8080/api")!, transport: URLSessionTransport()) + let response = try await client.getGreeting() + print(try response.ok.body.json.boxed().message) + } +} diff --git a/Examples/shared-types-client-server-example/Sources/Client/openapi-generator-config.yaml b/Examples/shared-types-client-server-example/Sources/Client/openapi-generator-config.yaml new file mode 100644 index 00000000..c8205b30 --- /dev/null +++ b/Examples/shared-types-client-server-example/Sources/Client/openapi-generator-config.yaml @@ -0,0 +1,5 @@ +generate: + - client +accessModifier: internal +additionalImports: + - Types diff --git a/Examples/shared-types-client-server-example/Sources/Client/openapi.yaml b/Examples/shared-types-client-server-example/Sources/Client/openapi.yaml new file mode 120000 index 00000000..1c2a243e --- /dev/null +++ b/Examples/shared-types-client-server-example/Sources/Client/openapi.yaml @@ -0,0 +1 @@ +../openapi.yaml \ No newline at end of file diff --git a/Examples/shared-types-client-server-example/Sources/Server/HelloWorldHummingbirdServer.swift b/Examples/shared-types-client-server-example/Sources/Server/HelloWorldHummingbirdServer.swift new file mode 100644 index 00000000..e2650754 --- /dev/null +++ b/Examples/shared-types-client-server-example/Sources/Server/HelloWorldHummingbirdServer.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import OpenAPIRuntime +import OpenAPIHummingbird +import Hummingbird +import Foundation +import Types + +struct Handler: APIProtocol { + func getGreeting(_ input: Operations.getGreeting.Input) async throws -> Operations.getGreeting.Output { + let name = input.query.name ?? "Stranger" + let message = Components.Schemas.Greeting(message: "Hello, \(name)!") + return .ok(.init(body: .json(message.boxed()))) + } +} + +@main struct HelloWorldHummingbirdServer { + static func main() async throws { + let app = Hummingbird.HBApplication() + let transport = HBOpenAPITransport(app) + let handler = Handler() + try handler.registerHandlers(on: transport, serverURL: URL(string: "/api")!) + try await app.asyncRun() + } +} diff --git a/Examples/shared-types-client-server-example/Sources/Server/openapi-generator-config.yaml b/Examples/shared-types-client-server-example/Sources/Server/openapi-generator-config.yaml new file mode 100644 index 00000000..0e38ae57 --- /dev/null +++ b/Examples/shared-types-client-server-example/Sources/Server/openapi-generator-config.yaml @@ -0,0 +1,5 @@ +generate: + - server +accessModifier: internal +additionalImports: + - Types diff --git a/Examples/shared-types-client-server-example/Sources/Server/openapi.yaml b/Examples/shared-types-client-server-example/Sources/Server/openapi.yaml new file mode 120000 index 00000000..1c2a243e --- /dev/null +++ b/Examples/shared-types-client-server-example/Sources/Server/openapi.yaml @@ -0,0 +1 @@ +../openapi.yaml \ No newline at end of file diff --git a/Examples/shared-types-client-server-example/Sources/Types/Extensions.swift b/Examples/shared-types-client-server-example/Sources/Types/Extensions.swift new file mode 100644 index 00000000..5456ea17 --- /dev/null +++ b/Examples/shared-types-client-server-example/Sources/Types/Extensions.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +extension Components.Schemas.Greeting { + package func boxed(maxBoxWidth: Int = 80) -> Self { + // Reflow the text. + let maxTextLength = maxBoxWidth - 4 + var reflowedLines = [Substring]() + for var line in message.split(whereSeparator: \.isNewline) { + while !line.isEmpty { + let prefix = line.prefix(maxTextLength) + reflowedLines.append(prefix) + line = line.dropFirst(prefix.count) + } + } + + // Determine the box size (might be smaller than max). + let longestLineCount = reflowedLines.map(\.count).max()! + let horizontalEdge = "+\(String(repeating: "–", count: longestLineCount))+" + + var boxedMessageLines = [String]() + boxedMessageLines.reserveCapacity(reflowedLines.count + 2) + boxedMessageLines.append(horizontalEdge) + for line in reflowedLines { + boxedMessageLines.append("|\(line.padding(toLength: longestLineCount, withPad: " ", startingAt: 0))|") + } + boxedMessageLines.append(horizontalEdge) + return Self(message: boxedMessageLines.joined(separator: "\n")) + } +} diff --git a/Examples/shared-types-client-server-example/Sources/Types/openapi-generator-config.yaml b/Examples/shared-types-client-server-example/Sources/Types/openapi-generator-config.yaml new file mode 100644 index 00000000..7e4ffe46 --- /dev/null +++ b/Examples/shared-types-client-server-example/Sources/Types/openapi-generator-config.yaml @@ -0,0 +1,3 @@ +generate: + - types +accessModifier: package diff --git a/Examples/shared-types-client-server-example/Sources/Types/openapi.yaml b/Examples/shared-types-client-server-example/Sources/Types/openapi.yaml new file mode 120000 index 00000000..1c2a243e --- /dev/null +++ b/Examples/shared-types-client-server-example/Sources/Types/openapi.yaml @@ -0,0 +1 @@ +../openapi.yaml \ No newline at end of file diff --git a/Examples/shared-types-client-server-example/Sources/openapi.yaml b/Examples/shared-types-client-server-example/Sources/openapi.yaml new file mode 100644 index 00000000..877dd1ca --- /dev/null +++ b/Examples/shared-types-client-server-example/Sources/openapi.yaml @@ -0,0 +1,36 @@ +openapi: '3.1.0' +info: + title: GreetingService + version: 1.0.0 +servers: + - url: https://example.com/api + description: Example service deployment. +paths: + /greet: + get: + operationId: getGreeting + parameters: + - name: name + required: false + in: query + description: The name used in the returned greeting. + schema: + type: string + responses: + '200': + description: A success response with a greeting. + content: + application/json: + schema: + $ref: '#/components/schemas/Greeting' +components: + schemas: + Greeting: + type: object + description: A value with the greeting contents. + properties: + message: + type: string + description: The string representation of the greeting. + required: + - message