diff --git a/Examples/README.md b/Examples/README.md index 52b974cb..8dbda14c 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -34,6 +34,8 @@ The following packages show working with various content types, such as JSON, UR - [various-content-types-server-example](./various-content-types-server-example) - A server showing how to handle and provide the various content types. - [event-streams-client-example](./event-streams-client-example) - A client showing how to provide and handle event streams. - [event-streams-server-example](./event-streams-server-example) - A server showing how to handle and provide event streams. +- [bidirectional-event-streams-client-example](./bidirectional-event-streams-client-example) - A client showing how to provide and handle bidirectional event streams. +- [bidirectional-event-streams-server-example](./bidirectional-event-streams-server-example) - A server showing how to handle and provide bidirectional event streams. ## Integrations diff --git a/Examples/bidirectional-event-streams-client-example/.gitignore b/Examples/bidirectional-event-streams-client-example/.gitignore new file mode 100644 index 00000000..f6f5465e --- /dev/null +++ b/Examples/bidirectional-event-streams-client-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/bidirectional-event-streams-client-example/Package.swift b/Examples/bidirectional-event-streams-client-example/Package.swift new file mode 100644 index 00000000..570e4f16 --- /dev/null +++ b/Examples/bidirectional-event-streams-client-example/Package.swift @@ -0,0 +1,35 @@ +// 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: "bidirectional-event-streams-client-example", + platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .visionOS(.v1)], + 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.2.0"), + .package(url: "https://github.com/swift-server/swift-openapi-async-http-client", from: "1.0.0"), + ], + targets: [ + .executableTarget( + name: "BidirectionalEventStreamsClient", + dependencies: [ + .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), + .product(name: "OpenAPIAsyncHTTPClient", package: "swift-openapi-async-http-client"), + ], + plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")] + ) + ] +) diff --git a/Examples/bidirectional-event-streams-client-example/README.md b/Examples/bidirectional-event-streams-client-example/README.md new file mode 100644 index 00000000..1a15d464 --- /dev/null +++ b/Examples/bidirectional-event-streams-client-example/README.md @@ -0,0 +1,23 @@ +# Client handling bidirectional event streams + +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 + +A command-line tool that uses a generated client to show how to work with bidirectional event streams. + +Instead of [URLSession](https://developer.apple.com/documentation/foundation/urlsession), which will return stream only until at least “some” bytes of the body have also been received (see [comment](https://github.com/apple/swift-openapi-urlsession/blob/main/Tests/OpenAPIURLSessionTests/URLSessionBidirectionalStreamingTests/URLSessionBidirectionalStreamingTests.swift#L193-L206)), tool uses the [AsyncHTTPClient](https://github.com/swift-server/async-http-client) API to perform the HTTP call, wrapped in the [AsyncHTTPClient Transport for Swift OpenAPI Generator](https://github.com/swift-server/swift-openapi-async-http-client). A workaround for URLSession could be sending an `empty`, `.joined` or some kind of hearbeat message from server first when initialising a stream. + +The server can be started by running `bidirectional-event-streams-server-example` locally. + +## Usage + +Build and run the client CLI using: + +```console +% swift run +Sending and fetching back greetings using JSON Lines +... +``` diff --git a/Examples/bidirectional-event-streams-client-example/Sources/BidirectionalEventStreamsClient/BidirectionalEventStreamsClient.swift b/Examples/bidirectional-event-streams-client-example/Sources/BidirectionalEventStreamsClient/BidirectionalEventStreamsClient.swift new file mode 100644 index 00000000..b5b82b26 --- /dev/null +++ b/Examples/bidirectional-event-streams-client-example/Sources/BidirectionalEventStreamsClient/BidirectionalEventStreamsClient.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 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 OpenAPIAsyncHTTPClient +import Foundation + +@main struct BidirectionalEventStreamsClient { + private static let templates: [String] = [ + "Hello, %@!", "Good morning, %@!", "Hi, %@!", "Greetings, %@!", "Hey, %@!", "Hi there, %@!", + "Good evening, %@!", + ] + static func main() async throws { + let client = Client(serverURL: URL(string: "http://localhost:8080/api")!, transport: AsyncHTTPClientTransport()) + do { + print("Sending and fetching back greetings using JSON Lines") + let (stream, continuation) = AsyncStream.makeStream() + /// To keep it simple, using JSON Lines, as it most straightforward and easy way to have streams. + /// For SSE and JSON Sequences cases please check `event-streams-client-example`. + let requestBody: Operations.getGreetingsStream.Input.Body = .application_jsonl( + .init(stream.asEncodedJSONLines(), length: .unknown, iterationBehavior: .single) + ) + let response = try await client.getGreetingsStream(query: .init(name: "Example"), body: requestBody) + let greetingStream = try response.ok.body.application_jsonl.asDecodedJSONLines( + of: Components.Schemas.Greeting.self + ) + try await withThrowingTaskGroup(of: Void.self) { group in + // Listen for upcoming messages + group.addTask { + for try await greeting in greetingStream { + try Task.checkCancellation() + print("Got greeting: \(greeting.message)") + } + } + // Send messages + group.addTask { + for template in Self.templates { + try Task.checkCancellation() + continuation.yield(.init(message: template)) + try await Task.sleep(nanoseconds: 1 * 1_000_000_000) + } + continuation.finish() + } + return try await group.waitForAll() + } + } + } +} diff --git a/Examples/bidirectional-event-streams-client-example/Sources/BidirectionalEventStreamsClient/openapi-generator-config.yaml b/Examples/bidirectional-event-streams-client-example/Sources/BidirectionalEventStreamsClient/openapi-generator-config.yaml new file mode 100644 index 00000000..1df6f287 --- /dev/null +++ b/Examples/bidirectional-event-streams-client-example/Sources/BidirectionalEventStreamsClient/openapi-generator-config.yaml @@ -0,0 +1,4 @@ +generate: + - types + - client +accessModifier: internal diff --git a/Examples/bidirectional-event-streams-client-example/Sources/BidirectionalEventStreamsClient/openapi.yaml b/Examples/bidirectional-event-streams-client-example/Sources/BidirectionalEventStreamsClient/openapi.yaml new file mode 100644 index 00000000..f40fa061 --- /dev/null +++ b/Examples/bidirectional-event-streams-client-example/Sources/BidirectionalEventStreamsClient/openapi.yaml @@ -0,0 +1,39 @@ +openapi: '3.1.0' +info: + title: EventStreamsService + version: 1.0.0 +servers: + - url: https://example.com/api + description: Example service deployment. +paths: + /greetings: + post: + operationId: getGreetingsStream + parameters: + - name: name + required: false + in: query + description: The name used in the returned greeting. + schema: + type: string + requestBody: + description: A body with a greetings stream. + required: true + content: + application/jsonl: {} + responses: + '200': + description: A success response with a greetings stream. + content: + application/jsonl: {} +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 diff --git a/Examples/bidirectional-event-streams-server-example/.gitignore b/Examples/bidirectional-event-streams-server-example/.gitignore new file mode 100644 index 00000000..df65a34f --- /dev/null +++ b/Examples/bidirectional-event-streams-server-example/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.vscode +/Package.resolved +.ci/ +.docc-build/ + diff --git a/Examples/bidirectional-event-streams-server-example/Package.swift b/Examples/bidirectional-event-streams-server-example/Package.swift new file mode 100644 index 00000000..5b079ae6 --- /dev/null +++ b/Examples/bidirectional-event-streams-server-example/Package.swift @@ -0,0 +1,37 @@ +// 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: "bidirectional-event-streams-server-example", + platforms: [.macOS(.v10_15)], + 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.2.0"), + .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0-rc.1"), + .package(url: "https://github.com/swift-server/swift-openapi-hummingbird.git", from: "2.0.0-beta.4"), + ], + targets: [ + .executableTarget( + name: "BidirectionalEventStreamsServer", + dependencies: [ + .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), + .product(name: "OpenAPIHummingbird", package: "swift-openapi-hummingbird"), + .product(name: "Hummingbird", package: "hummingbird"), + ], + plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")] + ) + ] +) diff --git a/Examples/bidirectional-event-streams-server-example/README.md b/Examples/bidirectional-event-streams-server-example/README.md new file mode 100644 index 00000000..32b03419 --- /dev/null +++ b/Examples/bidirectional-event-streams-server-example/README.md @@ -0,0 +1,23 @@ +# Server supporting bidirectional event streams + +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 + +A server that uses generated server stubs to show how to work with bidirectional event streams. + +The tool uses the [Hummingbird](https://github.com/hummingbird-project/hummingbird) server framework to handle HTTP requests, wrapped in the [Swift OpenAPI Hummingbird](https://github.com/swift-server/swift-openapi-hummingbird). + +The CLI starts the server on `http://localhost:8080` and can be invoked by running `bidirectional-event-streams-client-example`. + +## Usage + +Build and run the server CLI using: + +```console +% swift run +2024-07-04T08:56:23+0200 info Hummingbird : [HummingbirdCore] Server started and listening on 127.0.0.1:8080 +... +``` diff --git a/Examples/bidirectional-event-streams-server-example/Sources/BidirectionalEventStreamsServer/BidirectionalEventStreamsServer.swift b/Examples/bidirectional-event-streams-server-example/Sources/BidirectionalEventStreamsServer/BidirectionalEventStreamsServer.swift new file mode 100644 index 00000000..01da6351 --- /dev/null +++ b/Examples/bidirectional-event-streams-server-example/Sources/BidirectionalEventStreamsServer/BidirectionalEventStreamsServer.swift @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 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 + +struct Handler: APIProtocol { + private let storage: StreamStorage = .init() + func getGreetingsStream(_ input: Operations.getGreetingsStream.Input) async throws + -> Operations.getGreetingsStream.Output + { + let eventStream = await self.storage.makeStream(input: input) + /// To keep it simple, using JSON Lines, as it most straightforward and easy way to have streams. + /// For SSE and JSON Sequences cases please check `event-streams-server-example`. + let responseBody = Operations.getGreetingsStream.Output.Ok.Body.application_jsonl( + .init(eventStream.asEncodedJSONLines(), length: .unknown, iterationBehavior: .single) + ) + return .ok(.init(body: responseBody)) + } +} + +@main struct BidirectionalEventStreamsServer { + static func main() async throws { + let router = Router() + let handler = Handler() + try handler.registerHandlers(on: router, serverURL: URL(string: "/api")!) + let app = Application(router: router, configuration: .init()) + try await app.run() + } +} diff --git a/Examples/bidirectional-event-streams-server-example/Sources/BidirectionalEventStreamsServer/GreetingStream.swift b/Examples/bidirectional-event-streams-server-example/Sources/BidirectionalEventStreamsServer/GreetingStream.swift new file mode 100644 index 00000000..0e87bb13 --- /dev/null +++ b/Examples/bidirectional-event-streams-server-example/Sources/BidirectionalEventStreamsServer/GreetingStream.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 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 Foundation + +actor StreamStorage: Sendable { + private typealias StreamType = AsyncStream + private var streams: [String: Task] = [:] + init() {} + private func finishedStream(id: String) { + guard self.streams[id] != nil else { return } + self.streams.removeValue(forKey: id) + } + private func cancelStream(id: String) { + guard let task = self.streams[id] else { return } + self.streams.removeValue(forKey: id) + task.cancel() + print("Canceled stream \(id)") + } + func makeStream(input: Operations.getGreetingsStream.Input) -> AsyncStream { + let name = input.query.name ?? "Stranger" + let id = UUID().uuidString + print("Creating stream \(id) for name: \(name)") + let (stream, continuation) = StreamType.makeStream() + continuation.onTermination = { termination in + Task { [weak self] in + switch termination { + case .cancelled: await self?.cancelStream(id: id) + case .finished: await self?.finishedStream(id: id) + @unknown default: await self?.finishedStream(id: id) + } + } + } + let inputStream = + switch input.body { + case .application_jsonl(let body): body.asDecodedJSONLines(of: Components.Schemas.Greeting.self) + } + let task = Task { + for try await message in inputStream { + try Task.checkCancellation() + print("Recieved a message \(message)") + print("Sending greeting back for \(id)") + let greetingText = String(format: message.message, name) + continuation.yield(.init(message: greetingText)) + } + continuation.finish() + } + self.streams[id] = task + return stream + } +} diff --git a/Examples/bidirectional-event-streams-server-example/Sources/BidirectionalEventStreamsServer/openapi-generator-config.yaml b/Examples/bidirectional-event-streams-server-example/Sources/BidirectionalEventStreamsServer/openapi-generator-config.yaml new file mode 100644 index 00000000..f68d9c85 --- /dev/null +++ b/Examples/bidirectional-event-streams-server-example/Sources/BidirectionalEventStreamsServer/openapi-generator-config.yaml @@ -0,0 +1,4 @@ +generate: + - types + - server +accessModifier: internal diff --git a/Examples/bidirectional-event-streams-server-example/Sources/BidirectionalEventStreamsServer/openapi.yaml b/Examples/bidirectional-event-streams-server-example/Sources/BidirectionalEventStreamsServer/openapi.yaml new file mode 100644 index 00000000..f40fa061 --- /dev/null +++ b/Examples/bidirectional-event-streams-server-example/Sources/BidirectionalEventStreamsServer/openapi.yaml @@ -0,0 +1,39 @@ +openapi: '3.1.0' +info: + title: EventStreamsService + version: 1.0.0 +servers: + - url: https://example.com/api + description: Example service deployment. +paths: + /greetings: + post: + operationId: getGreetingsStream + parameters: + - name: name + required: false + in: query + description: The name used in the returned greeting. + schema: + type: string + requestBody: + description: A body with a greetings stream. + required: true + content: + application/jsonl: {} + responses: + '200': + description: A success response with a greetings stream. + content: + application/jsonl: {} +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