Skip to content

Commit 285ebba

Browse files
Add example package using shared types in client and server (#592)
### Motivation We get asked from time to time how to structure a package to share types from an OpenAPI document between multiple targets, e.g. a client and a server. This allows adopters to write extensions or other functionality that uses the common types once, and use them from both downstream modules. ### Modifications Add an example package that combines the existing Hello World client and server but factors the types generation into a separate module. ### Result Another example package. ### Test Plan - Tested locally, which produces the same result as in the example README. - CI.
1 parent 9385754 commit 285ebba

14 files changed

+258
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.DS_Store
2+
.build
3+
/Packages
4+
/*.xcodeproj
5+
xcuserdata/
6+
DerivedData/
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.vscode
9+
/Package.resolved
10+
.ci/
11+
.docc-build/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// swift-tools-version:5.9
2+
//===----------------------------------------------------------------------===//
3+
//
4+
// This source file is part of the SwiftOpenAPIGenerator open source project
5+
//
6+
// Copyright (c) 2024 Apple Inc. and the SwiftOpenAPIGenerator project authors
7+
// Licensed under Apache License v2.0
8+
//
9+
// See LICENSE.txt for license information
10+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
11+
//
12+
// SPDX-License-Identifier: Apache-2.0
13+
//
14+
//===----------------------------------------------------------------------===//
15+
import PackageDescription
16+
17+
let package = Package(
18+
name: "shared-types-client-server-example",
19+
platforms: [.macOS(.v13)],
20+
products: [
21+
.executable(name: "hello-world-client", targets: ["Client"]),
22+
.executable(name: "hello-world-server", targets: ["Server"]),
23+
],
24+
dependencies: [
25+
.package(url: "https://github.com/apple/swift-openapi-generator", from: "1.0.0"),
26+
.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.0.0"),
27+
.package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.0.0"),
28+
.package(url: "https://github.com/swift-server/swift-openapi-hummingbird", from: "1.0.0"),
29+
],
30+
targets: [
31+
.target(
32+
name: "Types",
33+
dependencies: [.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime")],
34+
plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")]
35+
),
36+
.executableTarget(
37+
name: "Client",
38+
dependencies: [
39+
"Types", .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
40+
.product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"),
41+
],
42+
plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")]
43+
),
44+
.executableTarget(
45+
name: "Server",
46+
dependencies: [
47+
"Types", .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
48+
.product(name: "OpenAPIHummingbird", package: "swift-openapi-hummingbird"),
49+
],
50+
plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")]
51+
),
52+
]
53+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Common types between client and server modules
2+
3+
An example project using [Swift OpenAPI Generator](https://github.com/apple/swift-openapi-generator).
4+
5+
> **Disclaimer:** This example is deliberately simplified and is intended for illustrative purposes only.
6+
7+
## Overview
8+
9+
This example shows how you can structure a Swift package to share the types
10+
from an OpenAPI document between a client and server module by having a common
11+
target that runs the generator in `types` mode only.
12+
13+
This allows you to write extensions or other helper functions that use these
14+
types and use them in both the client and server code.
15+
16+
## Usage
17+
18+
Build and run the server using:
19+
20+
```console
21+
% swift run hello-world-server
22+
Build complete!
23+
...
24+
info HummingBird : [HummingbirdCore] Server started and listening on 127.0.0.1:8080
25+
```
26+
27+
Then, in another terminal window, run the client:
28+
29+
```console
30+
% swift run hello-world-client
31+
Build complete!
32+
+––––––––––––––––––+
33+
|+––––––––––––––––+|
34+
||Hello, Stranger!||
35+
|+––––––––––––––––+|
36+
+––––––––––––––––––+
37+
```
38+
39+
Note how the message is boxed twice: once by the server and once by the client,
40+
both using an extension on a shared type, defined in the `Types` module.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
import OpenAPIRuntime
15+
import OpenAPIURLSession
16+
import Foundation
17+
18+
@main struct HelloWorldURLSessionClient {
19+
static func main() async throws {
20+
let client = Client(serverURL: URL(string: "http://localhost:8080/api")!, transport: URLSessionTransport())
21+
let response = try await client.getGreeting()
22+
print(try response.ok.body.json.boxed().message)
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
generate:
2+
- client
3+
accessModifier: internal
4+
additionalImports:
5+
- Types
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../openapi.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
import OpenAPIRuntime
15+
import OpenAPIHummingbird
16+
import Hummingbird
17+
import Foundation
18+
import Types
19+
20+
struct Handler: APIProtocol {
21+
func getGreeting(_ input: Operations.getGreeting.Input) async throws -> Operations.getGreeting.Output {
22+
let name = input.query.name ?? "Stranger"
23+
let message = Components.Schemas.Greeting(message: "Hello, \(name)!")
24+
return .ok(.init(body: .json(message.boxed())))
25+
}
26+
}
27+
28+
@main struct HelloWorldHummingbirdServer {
29+
static func main() async throws {
30+
let app = Hummingbird.HBApplication()
31+
let transport = HBOpenAPITransport(app)
32+
let handler = Handler()
33+
try handler.registerHandlers(on: transport, serverURL: URL(string: "/api")!)
34+
try await app.asyncRun()
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
generate:
2+
- server
3+
accessModifier: internal
4+
additionalImports:
5+
- Types
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../openapi.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
extension Components.Schemas.Greeting {
16+
package func boxed(maxBoxWidth: Int = 80) -> Self {
17+
// Reflow the text.
18+
let maxTextLength = maxBoxWidth - 4
19+
var reflowedLines: [Substring] = []
20+
for var line in message.split(whereSeparator: \.isNewline) {
21+
while !line.isEmpty {
22+
let prefix = line.prefix(maxTextLength)
23+
reflowedLines.append(prefix)
24+
line = line.dropFirst(prefix.count)
25+
}
26+
}
27+
28+
// Determine the box size (might be smaller than max).
29+
let longestLineCount = reflowedLines.map(\.count).max()!
30+
let horizontalEdge = "+\(String(repeating: "", count: longestLineCount))+"
31+
32+
var boxedMessageLines: [String] = []
33+
boxedMessageLines.reserveCapacity(reflowedLines.count + 2)
34+
boxedMessageLines.append(horizontalEdge)
35+
for line in reflowedLines {
36+
boxedMessageLines.append("|\(line.padding(toLength: longestLineCount, withPad: " ", startingAt: 0))|")
37+
}
38+
boxedMessageLines.append(horizontalEdge)
39+
return Self(message: boxedMessageLines.joined(separator: "\n"))
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
generate:
2+
- types
3+
accessModifier: package
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../openapi.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
openapi: '3.1.0'
2+
info:
3+
title: GreetingService
4+
version: 1.0.0
5+
servers:
6+
- url: https://example.com/api
7+
description: Example service deployment.
8+
paths:
9+
/greet:
10+
get:
11+
operationId: getGreeting
12+
parameters:
13+
- name: name
14+
required: false
15+
in: query
16+
description: The name used in the returned greeting.
17+
schema:
18+
type: string
19+
responses:
20+
'200':
21+
description: A success response with a greeting.
22+
content:
23+
application/json:
24+
schema:
25+
$ref: '#/components/schemas/Greeting'
26+
components:
27+
schemas:
28+
Greeting:
29+
type: object
30+
description: A value with the greeting contents.
31+
properties:
32+
message:
33+
type: string
34+
description: The string representation of the greeting.
35+
required:
36+
- message

scripts/check-license-headers.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ for FILE_PATH in "${PATHS_TO_CHECK_FOR_LICENSE[@]}"; do
9595
FILE_HEADER=$(head -n "${EXPECTED_FILE_HEADER_LINECOUNT}" "${FILE_PATH}")
9696
NORMALIZED_FILE_HEADER=$(
9797
echo "${FILE_HEADER}" \
98-
| sed -e 's/202[3]-202[3]/YEARS/' -e 's/202[3]/YEARS/' \
98+
| sed -e 's/202[3]-202[3,4]/YEARS/' -e 's/202[3,4]/YEARS/' \
9999
)
100100

101101
if ! diff -u \

0 commit comments

Comments
 (0)