From fd47bd2597056bf77f012365f4f0101a8e342148 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 1 Sep 2023 17:00:27 +0200 Subject: [PATCH 01/55] [WIP] Async body currency type --- Sources/OpenAPIRuntime/Interface/Body.swift | 535 ++++++++++++++++++ .../Interface/Test_Body.swift | 271 +++++++++ 2 files changed, 806 insertions(+) create mode 100644 Sources/OpenAPIRuntime/Interface/Body.swift create mode 100644 Tests/OpenAPIRuntimeTests/Interface/Test_Body.swift diff --git a/Sources/OpenAPIRuntime/Interface/Body.swift b/Sources/OpenAPIRuntime/Interface/Body.swift new file mode 100644 index 00000000..57675724 --- /dev/null +++ b/Sources/OpenAPIRuntime/Interface/Body.swift @@ -0,0 +1,535 @@ +//===----------------------------------------------------------------------===// +// +// 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 class Foundation.NSLock +import protocol Foundation.LocalizedError +import struct Foundation.Data // only for convenience initializers + +/// The type representing a request or response body. +public final class Body: @unchecked Sendable { + + /// The underlying data type. + public typealias DataType = ArraySlice + + /// How many times the provided sequence can be iterated. + public enum IterationBehavior: Sendable { + + /// The input sequence can only be iterated once. + /// + /// If a retry or a redirect is encountered, fail the call with + /// a descriptive error. + case single + + /// The input sequence can be iterated multiple times. + /// + /// Supports retries and redirects, as a new iterator is created each + /// time. + case multiple + } + + /// How many times the provided sequence can be iterated. + public let iterationBehavior: IterationBehavior + + /// The total length of the body, if known. + public enum Length: Sendable { + + /// Total length not known yet. + case unknown + + /// Total length is known. + case known(Int) + } + + /// The total length of the body, if known. + public let length: Length + + /// The underlying type-erased async sequence. + private let sequence: BodySequence + + /// A lock for shared mutable state. + private let lock: NSLock = { + let lock = NSLock() + lock.name = "com.apple.swift-openapi-generator.runtime.body" + return lock + }() + + /// Whether an iterator has already been created. + private var locked_iteratorCreated: Bool = false + + private init( + sequence: BodySequence, + length: Length, + iterationBehavior: IterationBehavior + ) { + self.sequence = sequence + self.length = length + self.iterationBehavior = iterationBehavior + } +} + +extension Body: Equatable { + public static func == ( + lhs: Body, + rhs: Body + ) -> Bool { + ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } +} + +extension Body: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } +} + +// MARK: - Creating the Body + +extension Body { + + public convenience init( + data: DataType, + length: Length + ) { + self.init( + dataChunks: [data], + length: length + ) + } + + public convenience init( + data: DataType + ) { + self.init( + dataChunks: [data], + length: .known(data.count) + ) + } + + public convenience init( + dataChunks: S, + length: Length, + iterationBehavior: IterationBehavior + ) where S.Element == DataType { + self.init( + sequence: .init(WrappedSyncSequence(sequence: dataChunks)), + length: length, + iterationBehavior: iterationBehavior + ) + } + + public convenience init( + dataChunks: C, + length: Length + ) where C.Element == DataType { + self.init( + sequence: .init(WrappedSyncSequence(sequence: dataChunks)), + length: length, + iterationBehavior: .multiple + ) + } + + public convenience init( + dataChunks: C + ) where C.Element == DataType { + self.init( + sequence: .init(WrappedSyncSequence(sequence: dataChunks)), + length: .known(dataChunks.map(\.count).reduce(0, +)), + iterationBehavior: .multiple + ) + } + + public convenience init( + stream: AsyncThrowingStream, + length: Body.Length + ) { + self.init( + sequence: .init(stream), + length: length, + iterationBehavior: .single + ) + } + + public convenience init( + stream: AsyncStream, + length: Body.Length + ) { + self.init( + sequence: .init(stream), + length: length, + iterationBehavior: .single + ) + } + + public convenience init( + sequence: S, + length: Body.Length, + iterationBehavior: IterationBehavior + ) where S.Element == DataType { + self.init( + sequence: .init(sequence), + length: length, + iterationBehavior: iterationBehavior + ) + } +} + +// MARK: - Consuming the body + +extension Body: AsyncSequence { + public typealias Element = DataType + public typealias AsyncIterator = Iterator + public func makeAsyncIterator() -> AsyncIterator { + if iterationBehavior == .single { + lock.lock() + defer { + lock.unlock() + } + guard !locked_iteratorCreated else { + fatalError( + "OpenAPIRuntime.Body attempted to create a second iterator, but the underlying sequence is only safe to be iterated once." + ) + } + locked_iteratorCreated = true + } + return sequence.makeAsyncIterator() + } +} + +// MARK: - Transforming the body + +extension Body { + + /// Creates a body where each chunk is transformed by the provided closure. + /// - Parameter transform: A mapping closure. + /// - Throws: If a known length was provided to this body at + /// creation time, the transform closure must not change the length of + /// each chunk. + public func mapChunks( + _ transform: @escaping @Sendable (Element) async -> Element + ) -> Body { + let validatedTransform: @Sendable (Element) async -> Element + switch length { + case .known: + validatedTransform = { element in + let transformedElement = await transform(element) + guard transformedElement.count == element.count else { + fatalError( + "OpenAPIRuntime.Body.mapChunks transform closure attempted to change the length of a chunk in a body which has a total length specified, this is not allowed." + ) + } + return transformedElement + } + case .unknown: + validatedTransform = transform + } + return Body( + sequence: map(validatedTransform), + length: length, + iterationBehavior: iterationBehavior + ) + } +} + +// MARK: - Consumption utils + +extension Body { + + /// An error thrown by the `collect` function when the body contains more + /// than the maximum allowed number of bytes. + private struct TooManyBytesError: Error, CustomStringConvertible, LocalizedError { + let maxBytes: Int + + var description: String { + "OpenAPIRuntime.Body contains more than the maximum allowed \(maxBytes) bytes." + } + + var errorDescription: String? { + description + } + } + + /// An error thrown by the `collect` function when another iteration of + /// the body is not allowed. + private struct TooManyIterationsError: Error, CustomStringConvertible, LocalizedError { + + var description: String { + "OpenAPIRuntime.Body attempted to create a second iterator, but the underlying sequence is only safe to be iterated once." + } + + var errorDescription: String? { + description + } + } + + /// Accumulates the full body in-memory into a single buffer + /// up to `maxBytes` and returns it. + /// - Parameters: + /// - maxBytes: The maximum number of bytes this method is allowed + /// to accumulate in memory before it throws an error. + /// - Throws: `TooManyBytesError` if the the sequence contains more + /// than `maxBytes`. + public func collect(upTo maxBytes: Int) async throws -> DataType { + + // As a courtesy, check if another iteration is allowed, and throw + // an error instead of fatalError here if the user is trying to + // iterate a sequence for the second time, if it's only safe to be + // iterated once. + if iterationBehavior == .single { + try { + lock.lock() + defer { + lock.unlock() + } + guard !locked_iteratorCreated else { + throw TooManyIterationsError() + } + }() + } + + var buffer = DataType.init() + for try await chunk in self { + guard buffer.count + chunk.count <= maxBytes else { + throw TooManyBytesError(maxBytes: maxBytes) + } + buffer.append(contentsOf: chunk) + } + return buffer + } +} + +// MARK: - String-based bodies + +extension StringProtocol { + fileprivate var asBodyChunk: Body.DataType { + Array(utf8)[...] + } +} + +extension Body { + + public convenience init( + data: some StringProtocol, + length: Length + ) { + self.init( + dataChunks: [data.asBodyChunk], + length: length + ) + } + + public convenience init( + data: some StringProtocol + ) { + self.init( + dataChunks: [data.asBodyChunk], + length: .known(data.count) + ) + } + + public convenience init( + dataChunks: S, + length: Length, + iterationBehavior: IterationBehavior + ) where S.Element: StringProtocol { + self.init( + dataChunks: dataChunks.map(\.asBodyChunk), + length: length, + iterationBehavior: iterationBehavior + ) + } + + public convenience init( + dataChunks: C, + length: Length + ) where C.Element: StringProtocol { + self.init( + dataChunks: dataChunks.map(\.asBodyChunk), + length: length + ) + } + + public convenience init( + dataChunks: C + ) where C.Element: StringProtocol { + self.init( + dataChunks: dataChunks.map(\.asBodyChunk) + ) + } + + public convenience init( + stream: AsyncThrowingStream, + length: Body.Length + ) { + self.init( + sequence: .init(stream.map(\.asBodyChunk)), + length: length, + iterationBehavior: .single + ) + } + + public convenience init( + stream: AsyncStream, + length: Body.Length + ) { + self.init( + sequence: .init(stream.map(\.asBodyChunk)), + length: length, + iterationBehavior: .single + ) + } + + public convenience init( + sequence: S, + length: Body.Length, + iterationBehavior: IterationBehavior + ) where S.Element: StringProtocol { + self.init( + sequence: .init(sequence.map(\.asBodyChunk)), + length: length, + iterationBehavior: iterationBehavior + ) + } +} + +extension Body { + + /// Accumulates the full body in-memory into a single buffer + /// up to `maxBytes`, converts it to String, and returns it. + /// - Parameters: + /// - maxBytes: The maximum number of bytes this method is allowed + /// to accumulate in memory before it throws an error. + /// - Throws: `TooManyBytesError` if the the body contains more + /// than `maxBytes`. + public func collectAsString(upTo maxBytes: Int) async throws -> String { + let bytes: DataType = try await collect(upTo: maxBytes) + return String(decoding: bytes, as: UTF8.self) + } +} + +// MARK: - Body conversions + +extension Body: ExpressibleByStringLiteral { + + public convenience init(stringLiteral value: String) { + self.init(data: value) + } +} + +extension Body { + + public convenience init(data: [UInt8]) { + self.init(data: data[...]) + } +} + +extension Body: ExpressibleByArrayLiteral { + + public typealias ArrayLiteralElement = UInt8 + + public convenience init(arrayLiteral elements: UInt8...) { + self.init(data: elements) + } +} + +extension Body { + + public convenience init(data: Data) { + self.init(data: ArraySlice(data)) + } + + /// Accumulates the full body in-memory into a single buffer + /// up to `maxBytes`, converts it to Foundation.Data, and returns it. + /// - Parameters: + /// - maxBytes: The maximum number of bytes this method is allowed + /// to accumulate in memory before it throws an error. + /// - Throws: `TooManyBytesError` if the the body contains more + /// than `maxBytes`. + public func collectAsData(upTo maxBytes: Int) async throws -> Data { + let bytes: DataType = try await collect(upTo: maxBytes) + return Data(bytes) + } +} + +// MARK: - Underlying async sequences + +extension Body { + + /// Async iterator of both input async sequences and of the body itself. + public struct Iterator: AsyncIteratorProtocol { + + public typealias Element = Body.DataType + + private let produceNext: () async throws -> Element? + + init( + _ iterator: Iterator + ) where Iterator.Element == Element { + var iterator = iterator + self.produceNext = { + try await iterator.next() + } + } + + public func next() async throws -> Element? { + try await produceNext() + } + } +} + +extension Body { + + /// A type-erased async sequence that wraps input sequences. + private struct BodySequence: AsyncSequence { + + typealias AsyncIterator = Body.Iterator + typealias Element = DataType + + private let produceIterator: () -> AsyncIterator + + init(_ sequence: S) where S.Element == Element { + self.produceIterator = { + .init(sequence.makeAsyncIterator()) + } + } + + func makeAsyncIterator() -> AsyncIterator { + produceIterator() + } + } + + /// A wrapper for a sync sequence. + private struct WrappedSyncSequence: AsyncSequence + where S.Element == DataType, S.Iterator.Element == DataType { + + typealias AsyncIterator = Iterator + typealias Element = DataType + + struct Iterator: AsyncIteratorProtocol { + + typealias Element = DataType + + var iterator: any IteratorProtocol + + mutating func next() async throws -> Body.DataType? { + iterator.next() + } + } + + let sequence: S + + func makeAsyncIterator() -> Iterator { + Iterator(iterator: sequence.makeIterator()) + } + } +} diff --git a/Tests/OpenAPIRuntimeTests/Interface/Test_Body.swift b/Tests/OpenAPIRuntimeTests/Interface/Test_Body.swift new file mode 100644 index 00000000..66e78d94 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Interface/Test_Body.swift @@ -0,0 +1,271 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest +@_spi(Generated)@testable import OpenAPIRuntime +import Foundation + +final class Test_Body: Test_Runtime { + + func testCreateAndCollect() async throws { + + // A single string. + do { + let body: Body = Body(data: "hello") + try await _testConsume( + body, + expected: "hello" + ) + } + + // A literal string. + do { + let body: Body = "hello" + try await _testConsume( + body, + expected: "hello" + ) + } + + // A sequence of strings. + do { + let body: Body = Body(dataChunks: ["hel", "lo"]) + try await _testConsume( + body, + expected: "hello" + ) + } + + // A single substring. + do { + let body: Body = Body(data: "hello"[...]) + try await _testConsume( + body, + expected: "hello"[...] + ) + } + + // A sequence of substrings. + do { + let body: Body = Body(dataChunks: [ + "hel"[...], + "lo"[...], + ]) + try await _testConsume( + body, + expected: "hello"[...] + ) + } + + // A single array of bytes. + do { + let body: Body = Body(data: [0]) + try await _testConsume( + body, + expected: [0] + ) + } + + // A literal array of bytes. + do { + let body: Body = [0] + try await _testConsume( + body, + expected: [0] + ) + } + + // A single data. + do { + let body: Body = Body(data: Data([0])) + try await _testConsume( + body, + expected: [0] + ) + } + + // A sequence of arrays of bytes. + do { + let body: Body = Body(dataChunks: [[0], [1]]) + try await _testConsume( + body, + expected: [0, 1] + ) + } + + // A single slice of an array of bytes. + do { + let body: Body = Body(data: [0][...]) + try await _testConsume( + body, + expected: [0][...] + ) + } + + // A sequence of slices of an array of bytes. + do { + let body: Body = Body(dataChunks: [ + [0][...], + [1][...], + ]) + try await _testConsume( + body, + expected: [0, 1][...] + ) + } + + // An async throwing stream. + do { + let body: Body = Body( + stream: AsyncThrowingStream( + String.self, + { continuation in + continuation.yield("hel") + continuation.yield("lo") + continuation.finish() + } + ), + length: .known(5) + ) + try await _testConsume( + body, + expected: "hello" + ) + } + + // An async stream. + do { + let body: Body = Body( + stream: AsyncStream( + String.self, + { continuation in + continuation.yield("hel") + continuation.yield("lo") + continuation.finish() + } + ), + length: .known(5) + ) + try await _testConsume( + body, + expected: "hello" + ) + } + + // Another async sequence. + do { + let sequence = AsyncStream( + String.self, + { continuation in + continuation.yield("hel") + continuation.yield("lo") + continuation.finish() + } + ) + .map { $0 } + let body: Body = Body( + sequence: sequence, + length: .known(5), + iterationBehavior: .single + ) + try await _testConsume( + body, + expected: "hello" + ) + } + } + + func testChunksPreserved() async throws { + let sequence = AsyncStream( + String.self, + { continuation in + continuation.yield("hel") + continuation.yield("lo") + continuation.finish() + } + ) + .map { $0 } + let body: Body = Body( + sequence: sequence, + length: .known(5), + iterationBehavior: .single + ) + var chunks: [Body.DataType] = [] + for try await chunk in body { + chunks.append(chunk) + } + XCTAssertEqual(chunks, ["hel", "lo"].map { Array($0.utf8)[...] }) + } + + func testMapChunks() async throws { + let body: Body = Body( + stream: AsyncStream( + String.self, + { continuation in + continuation.yield("hello") + continuation.yield(" ") + continuation.yield("world") + continuation.finish() + } + ), + length: .known(5) + ) + actor Chunker { + private var iterator: Array.Iterator + init(expectedChunks: [Body.DataType]) { + self.iterator = expectedChunks.makeIterator() + } + func checkNextChunk(_ actual: Body.DataType) { + XCTAssertEqual(actual, iterator.next()) + } + } + let chunker = Chunker( + expectedChunks: [ + "hello", + " ", + "world", + ] + .map { Array($0.utf8)[...] } + ) + let finalString = + try await body + .mapChunks { element in + await chunker.checkNextChunk(element) + return element.reversed()[...] + } + .collectAsString(upTo: .max) + XCTAssertEqual(finalString, "olleh dlrow") + } +} + +extension Test_Body { + func _testConsume( + _ body: Body, + expected: Body.DataType, + file: StaticString = #file, + line: UInt = #line + ) async throws { + let output = try await body.collect(upTo: .max) + XCTAssertEqual(output, expected, file: file, line: line) + } + + func _testConsume( + _ body: Body, + expected: some StringProtocol, + file: StaticString = #file, + line: UInt = #line + ) async throws { + let output = try await body.collectAsString(upTo: .max) + XCTAssertEqual(output, expected.description, file: file, line: line) + } +} From beca6304ff21a05bc26395c6766f22fc9531260d Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 4 Sep 2023 10:44:04 +0200 Subject: [PATCH 02/55] Rename Body to HTTPBody to avoid confusion with the generated Body enums --- .../Interface/{Body.swift => HTTPBody.swift} | 70 +++++++++---------- .../{Test_Body.swift => Test_HTTPBody.swift} | 46 ++++++------ 2 files changed, 58 insertions(+), 58 deletions(-) rename Sources/OpenAPIRuntime/Interface/{Body.swift => HTTPBody.swift} (89%) rename Tests/OpenAPIRuntimeTests/Interface/{Test_Body.swift => Test_HTTPBody.swift} (85%) diff --git a/Sources/OpenAPIRuntime/Interface/Body.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift similarity index 89% rename from Sources/OpenAPIRuntime/Interface/Body.swift rename to Sources/OpenAPIRuntime/Interface/HTTPBody.swift index 57675724..4e4a4731 100644 --- a/Sources/OpenAPIRuntime/Interface/Body.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -17,7 +17,7 @@ import protocol Foundation.LocalizedError import struct Foundation.Data // only for convenience initializers /// The type representing a request or response body. -public final class Body: @unchecked Sendable { +public final class HTTPBody: @unchecked Sendable { /// The underlying data type. public typealias DataType = ArraySlice @@ -78,24 +78,24 @@ public final class Body: @unchecked Sendable { } } -extension Body: Equatable { +extension HTTPBody: Equatable { public static func == ( - lhs: Body, - rhs: Body + lhs: HTTPBody, + rhs: HTTPBody ) -> Bool { ObjectIdentifier(lhs) == ObjectIdentifier(rhs) } } -extension Body: Hashable { +extension HTTPBody: Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } } -// MARK: - Creating the Body +// MARK: - Creating the HTTPBody -extension Body { +extension HTTPBody { public convenience init( data: DataType, @@ -151,7 +151,7 @@ extension Body { public convenience init( stream: AsyncThrowingStream, - length: Body.Length + length: HTTPBody.Length ) { self.init( sequence: .init(stream), @@ -162,7 +162,7 @@ extension Body { public convenience init( stream: AsyncStream, - length: Body.Length + length: HTTPBody.Length ) { self.init( sequence: .init(stream), @@ -173,7 +173,7 @@ extension Body { public convenience init( sequence: S, - length: Body.Length, + length: HTTPBody.Length, iterationBehavior: IterationBehavior ) where S.Element == DataType { self.init( @@ -186,7 +186,7 @@ extension Body { // MARK: - Consuming the body -extension Body: AsyncSequence { +extension HTTPBody: AsyncSequence { public typealias Element = DataType public typealias AsyncIterator = Iterator public func makeAsyncIterator() -> AsyncIterator { @@ -197,7 +197,7 @@ extension Body: AsyncSequence { } guard !locked_iteratorCreated else { fatalError( - "OpenAPIRuntime.Body attempted to create a second iterator, but the underlying sequence is only safe to be iterated once." + "OpenAPIRuntime.HTTPBody attempted to create a second iterator, but the underlying sequence is only safe to be iterated once." ) } locked_iteratorCreated = true @@ -208,7 +208,7 @@ extension Body: AsyncSequence { // MARK: - Transforming the body -extension Body { +extension HTTPBody { /// Creates a body where each chunk is transformed by the provided closure. /// - Parameter transform: A mapping closure. @@ -217,7 +217,7 @@ extension Body { /// each chunk. public func mapChunks( _ transform: @escaping @Sendable (Element) async -> Element - ) -> Body { + ) -> HTTPBody { let validatedTransform: @Sendable (Element) async -> Element switch length { case .known: @@ -225,7 +225,7 @@ extension Body { let transformedElement = await transform(element) guard transformedElement.count == element.count else { fatalError( - "OpenAPIRuntime.Body.mapChunks transform closure attempted to change the length of a chunk in a body which has a total length specified, this is not allowed." + "OpenAPIRuntime.HTTPBody.mapChunks transform closure attempted to change the length of a chunk in a body which has a total length specified, this is not allowed." ) } return transformedElement @@ -233,7 +233,7 @@ extension Body { case .unknown: validatedTransform = transform } - return Body( + return HTTPBody( sequence: map(validatedTransform), length: length, iterationBehavior: iterationBehavior @@ -243,7 +243,7 @@ extension Body { // MARK: - Consumption utils -extension Body { +extension HTTPBody { /// An error thrown by the `collect` function when the body contains more /// than the maximum allowed number of bytes. @@ -251,7 +251,7 @@ extension Body { let maxBytes: Int var description: String { - "OpenAPIRuntime.Body contains more than the maximum allowed \(maxBytes) bytes." + "OpenAPIRuntime.HTTPBody contains more than the maximum allowed \(maxBytes) bytes." } var errorDescription: String? { @@ -264,7 +264,7 @@ extension Body { private struct TooManyIterationsError: Error, CustomStringConvertible, LocalizedError { var description: String { - "OpenAPIRuntime.Body attempted to create a second iterator, but the underlying sequence is only safe to be iterated once." + "OpenAPIRuntime.HTTPBody attempted to create a second iterator, but the underlying sequence is only safe to be iterated once." } var errorDescription: String? { @@ -311,12 +311,12 @@ extension Body { // MARK: - String-based bodies extension StringProtocol { - fileprivate var asBodyChunk: Body.DataType { + fileprivate var asBodyChunk: HTTPBody.DataType { Array(utf8)[...] } } -extension Body { +extension HTTPBody { public convenience init( data: some StringProtocol, @@ -369,7 +369,7 @@ extension Body { public convenience init( stream: AsyncThrowingStream, - length: Body.Length + length: HTTPBody.Length ) { self.init( sequence: .init(stream.map(\.asBodyChunk)), @@ -380,7 +380,7 @@ extension Body { public convenience init( stream: AsyncStream, - length: Body.Length + length: HTTPBody.Length ) { self.init( sequence: .init(stream.map(\.asBodyChunk)), @@ -391,7 +391,7 @@ extension Body { public convenience init( sequence: S, - length: Body.Length, + length: HTTPBody.Length, iterationBehavior: IterationBehavior ) where S.Element: StringProtocol { self.init( @@ -402,7 +402,7 @@ extension Body { } } -extension Body { +extension HTTPBody { /// Accumulates the full body in-memory into a single buffer /// up to `maxBytes`, converts it to String, and returns it. @@ -417,23 +417,23 @@ extension Body { } } -// MARK: - Body conversions +// MARK: - HTTPBody conversions -extension Body: ExpressibleByStringLiteral { +extension HTTPBody: ExpressibleByStringLiteral { public convenience init(stringLiteral value: String) { self.init(data: value) } } -extension Body { +extension HTTPBody { public convenience init(data: [UInt8]) { self.init(data: data[...]) } } -extension Body: ExpressibleByArrayLiteral { +extension HTTPBody: ExpressibleByArrayLiteral { public typealias ArrayLiteralElement = UInt8 @@ -442,7 +442,7 @@ extension Body: ExpressibleByArrayLiteral { } } -extension Body { +extension HTTPBody { public convenience init(data: Data) { self.init(data: ArraySlice(data)) @@ -463,12 +463,12 @@ extension Body { // MARK: - Underlying async sequences -extension Body { +extension HTTPBody { /// Async iterator of both input async sequences and of the body itself. public struct Iterator: AsyncIteratorProtocol { - public typealias Element = Body.DataType + public typealias Element = HTTPBody.DataType private let produceNext: () async throws -> Element? @@ -487,12 +487,12 @@ extension Body { } } -extension Body { +extension HTTPBody { /// A type-erased async sequence that wraps input sequences. private struct BodySequence: AsyncSequence { - typealias AsyncIterator = Body.Iterator + typealias AsyncIterator = HTTPBody.Iterator typealias Element = DataType private let produceIterator: () -> AsyncIterator @@ -521,7 +521,7 @@ extension Body { var iterator: any IteratorProtocol - mutating func next() async throws -> Body.DataType? { + mutating func next() async throws -> HTTPBody.DataType? { iterator.next() } } diff --git a/Tests/OpenAPIRuntimeTests/Interface/Test_Body.swift b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift similarity index 85% rename from Tests/OpenAPIRuntimeTests/Interface/Test_Body.swift rename to Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift index 66e78d94..5b19e7f9 100644 --- a/Tests/OpenAPIRuntimeTests/Interface/Test_Body.swift +++ b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift @@ -21,7 +21,7 @@ final class Test_Body: Test_Runtime { // A single string. do { - let body: Body = Body(data: "hello") + let body: HTTPBody = HTTPBody(data: "hello") try await _testConsume( body, expected: "hello" @@ -30,7 +30,7 @@ final class Test_Body: Test_Runtime { // A literal string. do { - let body: Body = "hello" + let body: HTTPBody = "hello" try await _testConsume( body, expected: "hello" @@ -39,7 +39,7 @@ final class Test_Body: Test_Runtime { // A sequence of strings. do { - let body: Body = Body(dataChunks: ["hel", "lo"]) + let body: HTTPBody = HTTPBody(dataChunks: ["hel", "lo"]) try await _testConsume( body, expected: "hello" @@ -48,7 +48,7 @@ final class Test_Body: Test_Runtime { // A single substring. do { - let body: Body = Body(data: "hello"[...]) + let body: HTTPBody = HTTPBody(data: "hello"[...]) try await _testConsume( body, expected: "hello"[...] @@ -57,7 +57,7 @@ final class Test_Body: Test_Runtime { // A sequence of substrings. do { - let body: Body = Body(dataChunks: [ + let body: HTTPBody = HTTPBody(dataChunks: [ "hel"[...], "lo"[...], ]) @@ -69,7 +69,7 @@ final class Test_Body: Test_Runtime { // A single array of bytes. do { - let body: Body = Body(data: [0]) + let body: HTTPBody = HTTPBody(data: [0]) try await _testConsume( body, expected: [0] @@ -78,7 +78,7 @@ final class Test_Body: Test_Runtime { // A literal array of bytes. do { - let body: Body = [0] + let body: HTTPBody = [0] try await _testConsume( body, expected: [0] @@ -87,7 +87,7 @@ final class Test_Body: Test_Runtime { // A single data. do { - let body: Body = Body(data: Data([0])) + let body: HTTPBody = HTTPBody(data: Data([0])) try await _testConsume( body, expected: [0] @@ -96,7 +96,7 @@ final class Test_Body: Test_Runtime { // A sequence of arrays of bytes. do { - let body: Body = Body(dataChunks: [[0], [1]]) + let body: HTTPBody = HTTPBody(dataChunks: [[0], [1]]) try await _testConsume( body, expected: [0, 1] @@ -105,7 +105,7 @@ final class Test_Body: Test_Runtime { // A single slice of an array of bytes. do { - let body: Body = Body(data: [0][...]) + let body: HTTPBody = HTTPBody(data: [0][...]) try await _testConsume( body, expected: [0][...] @@ -114,7 +114,7 @@ final class Test_Body: Test_Runtime { // A sequence of slices of an array of bytes. do { - let body: Body = Body(dataChunks: [ + let body: HTTPBody = HTTPBody(dataChunks: [ [0][...], [1][...], ]) @@ -126,7 +126,7 @@ final class Test_Body: Test_Runtime { // An async throwing stream. do { - let body: Body = Body( + let body: HTTPBody = HTTPBody( stream: AsyncThrowingStream( String.self, { continuation in @@ -145,7 +145,7 @@ final class Test_Body: Test_Runtime { // An async stream. do { - let body: Body = Body( + let body: HTTPBody = HTTPBody( stream: AsyncStream( String.self, { continuation in @@ -173,7 +173,7 @@ final class Test_Body: Test_Runtime { } ) .map { $0 } - let body: Body = Body( + let body: HTTPBody = HTTPBody( sequence: sequence, length: .known(5), iterationBehavior: .single @@ -195,12 +195,12 @@ final class Test_Body: Test_Runtime { } ) .map { $0 } - let body: Body = Body( + let body: HTTPBody = HTTPBody( sequence: sequence, length: .known(5), iterationBehavior: .single ) - var chunks: [Body.DataType] = [] + var chunks: [HTTPBody.DataType] = [] for try await chunk in body { chunks.append(chunk) } @@ -208,7 +208,7 @@ final class Test_Body: Test_Runtime { } func testMapChunks() async throws { - let body: Body = Body( + let body: HTTPBody = HTTPBody( stream: AsyncStream( String.self, { continuation in @@ -221,11 +221,11 @@ final class Test_Body: Test_Runtime { length: .known(5) ) actor Chunker { - private var iterator: Array.Iterator - init(expectedChunks: [Body.DataType]) { + private var iterator: Array.Iterator + init(expectedChunks: [HTTPBody.DataType]) { self.iterator = expectedChunks.makeIterator() } - func checkNextChunk(_ actual: Body.DataType) { + func checkNextChunk(_ actual: HTTPBody.DataType) { XCTAssertEqual(actual, iterator.next()) } } @@ -250,8 +250,8 @@ final class Test_Body: Test_Runtime { extension Test_Body { func _testConsume( - _ body: Body, - expected: Body.DataType, + _ body: HTTPBody, + expected: HTTPBody.DataType, file: StaticString = #file, line: UInt = #line ) async throws { @@ -260,7 +260,7 @@ extension Test_Body { } func _testConsume( - _ body: Body, + _ body: HTTPBody, expected: some StringProtocol, file: StaticString = #file, line: UInt = #line From 34f7c59bbf28642aa8a2194644a3a08dcbfb6609 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 4 Sep 2023 13:40:11 +0200 Subject: [PATCH 03/55] [WIP] Adopt swift-http-types --- Package.swift | 5 +- .../Conversion/Converter+Client.swift | 86 ++---- .../Conversion/Converter+Common.swift | 17 +- .../Conversion/Converter+Server.swift | 106 ++----- .../Conversion/CurrencyExtensions.swift | 262 ++++++++-------- .../Conversion/FoundationExtensions.swift | 39 --- .../OpenAPIRuntime/Errors/ClientError.swift | 53 ++-- .../OpenAPIRuntime/Errors/RuntimeError.swift | 6 + .../OpenAPIRuntime/Errors/ServerError.swift | 39 +-- .../Interface/ClientTransport.swift | 227 +------------- .../Interface/CurrencyTypes.swift | 291 ------------------ .../Interface/ServerTransport.swift | 206 +------------ .../Interface/UniversalClient.swift | 66 ++-- .../Interface/UniversalServer.swift | 69 ++--- 14 files changed, 300 insertions(+), 1172 deletions(-) delete mode 100644 Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift diff --git a/Package.swift b/Package.swift index ea000944..2672bc5e 100644 --- a/Package.swift +++ b/Package.swift @@ -37,12 +37,15 @@ let package = Package( ), ], dependencies: [ + .package(url: "https://github.com/apple/swift-http-types", branch: "main"), .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), ], targets: [ .target( name: "OpenAPIRuntime", - dependencies: [], + dependencies: [ + .product(name: "HTTPTypes", package: "swift-http-types"), + ], swiftSettings: swiftSettings ), .testTarget( diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index 4c484818..f9699435 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// import Foundation +import HTTPTypes extension Converter { @@ -20,15 +21,10 @@ extension Converter { /// - headerFields: The header fields where to add the "accept" header. /// - contentTypes: The array of acceptable content types by the client. public func setAcceptHeader( - in headerFields: inout [HeaderField], + in headerFields: inout HTTPFields, contentTypes: [AcceptHeaderContentType] ) { - headerFields.append( - .init( - name: "accept", - value: contentTypes.map(\.rawValue).joined(separator: ", ") - ) - ) + headerFields[.accept] = contentTypes.map(\.rawValue).joined(separator: ", ") } // | client | set | request path | URI | required | renderedPath | @@ -60,7 +56,7 @@ extension Converter { // | client | set | request query | URI | both | setQueryItemAsURI | public func setQueryItemAsURI( - in request: inout Request, + in request: inout HTTPRequest, style: ParameterStyle?, explode: Bool?, name: String, @@ -84,40 +80,12 @@ extension Converter { ) } - // | client | set | request body | string | optional | setOptionalRequestBodyAsString | - public func setOptionalRequestBodyAsString( - _ value: T?, - headerFields: inout [HeaderField], - contentType: String - ) throws -> Data? { - try setOptionalRequestBody( - value, - headerFields: &headerFields, - contentType: contentType, - convert: convertToStringData - ) - } - - // | client | set | request body | string | required | setRequiredRequestBodyAsString | - public func setRequiredRequestBodyAsString( - _ value: T, - headerFields: inout [HeaderField], - contentType: String - ) throws -> Data { - try setRequiredRequestBody( - value, - headerFields: &headerFields, - contentType: contentType, - convert: convertToStringData - ) - } - // | client | set | request body | JSON | optional | setOptionalRequestBodyAsJSON | public func setOptionalRequestBodyAsJSON( _ value: T?, - headerFields: inout [HeaderField], + headerFields: inout HTTPFields, contentType: String - ) throws -> Data? { + ) throws -> HTTPBody? { try setOptionalRequestBody( value, headerFields: &headerFields, @@ -129,9 +97,9 @@ extension Converter { // | client | set | request body | JSON | required | setRequiredRequestBodyAsJSON | public func setRequiredRequestBodyAsJSON( _ value: T, - headerFields: inout [HeaderField], + headerFields: inout HTTPFields, contentType: String - ) throws -> Data { + ) throws -> HTTPBody { try setRequiredRequestBody( value, headerFields: &headerFields, @@ -142,10 +110,10 @@ extension Converter { // | client | set | request body | binary | optional | setOptionalRequestBodyAsBinary | public func setOptionalRequestBodyAsBinary( - _ value: Data?, - headerFields: inout [HeaderField], + _ value: HTTPBody?, + headerFields: inout HTTPFields, contentType: String - ) throws -> Data? { + ) throws -> HTTPBody? { try setOptionalRequestBody( value, headerFields: &headerFields, @@ -156,10 +124,10 @@ extension Converter { // | client | set | request body | binary | required | setRequiredRequestBodyAsBinary | public func setRequiredRequestBodyAsBinary( - _ value: Data, - headerFields: inout [HeaderField], + _ value: HTTPBody, + headerFields: inout HTTPFields, contentType: String - ) throws -> Data { + ) throws -> HTTPBody { try setRequiredRequestBody( value, headerFields: &headerFields, @@ -168,27 +136,13 @@ extension Converter { ) } - // | client | get | response body | string | required | getResponseBodyAsString | - public func getResponseBodyAsString( - _ type: T.Type, - from data: Data, - transforming transform: (T) -> C - ) throws -> C { - try getResponseBody( - type, - from: data, - transforming: transform, - convert: convertFromStringData - ) - } - // | client | get | response body | JSON | required | getResponseBodyAsJSON | public func getResponseBodyAsJSON( _ type: T.Type, - from data: Data, + from data: HTTPBody, transforming transform: (T) -> C - ) throws -> C { - try getResponseBody( + ) async throws -> C { + try await getBufferingResponseBody( type, from: data, transforming: transform, @@ -198,9 +152,9 @@ extension Converter { // | client | get | response body | binary | required | getResponseBodyAsBinary | public func getResponseBodyAsBinary( - _ type: Data.Type, - from data: Data, - transforming transform: (Data) -> C + _ type: HTTPBody.Type, + from data: HTTPBody, + transforming transform: (HTTPBody) -> C ) throws -> C { try getResponseBody( type, diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift index 1630a397..fc9235a4 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// import Foundation +import HTTPTypes extension Converter { @@ -21,8 +22,8 @@ extension Converter { /// - Parameter headerFields: The header fields to inspect for the content /// type header. /// - Returns: The content type value, or nil if not found or invalid. - public func extractContentTypeIfPresent(in headerFields: [HeaderField]) -> OpenAPIMIMEType? { - guard let rawValue = headerFields.firstValue(name: "content-type") else { + public func extractContentTypeIfPresent(in headerFields: HTTPFields) -> OpenAPIMIMEType? { + guard let rawValue = headerFields[.contentType] else { return nil } return OpenAPIMIMEType(rawValue) @@ -72,7 +73,7 @@ extension Converter { // | common | set | header field | URI | both | setHeaderFieldAsURI | public func setHeaderFieldAsURI( - in headerFields: inout [HeaderField], + in headerFields: inout HTTPFields, name: String, value: T? ) throws { @@ -97,7 +98,7 @@ extension Converter { // | common | set | header field | JSON | both | setHeaderFieldAsJSON | public func setHeaderFieldAsJSON( - in headerFields: inout [HeaderField], + in headerFields: inout HTTPFields, name: String, value: T? ) throws { @@ -111,7 +112,7 @@ extension Converter { // | common | get | header field | URI | optional | getOptionalHeaderFieldAsURI | public func getOptionalHeaderFieldAsURI( - in headerFields: [HeaderField], + in headerFields: HTTPFields, name: String, as type: T.Type ) throws -> T? { @@ -133,7 +134,7 @@ extension Converter { // | common | get | header field | URI | required | getRequiredHeaderFieldAsURI | public func getRequiredHeaderFieldAsURI( - in headerFields: [HeaderField], + in headerFields: HTTPFields, name: String, as type: T.Type ) throws -> T { @@ -155,7 +156,7 @@ extension Converter { // | common | get | header field | JSON | optional | getOptionalHeaderFieldAsJSON | public func getOptionalHeaderFieldAsJSON( - in headerFields: [HeaderField], + in headerFields: HTTPFields, name: String, as type: T.Type ) throws -> T? { @@ -169,7 +170,7 @@ extension Converter { // | common | get | header field | JSON | required | getRequiredHeaderFieldAsJSON | public func getRequiredHeaderFieldAsJSON( - in headerFields: [HeaderField], + in headerFields: HTTPFields, name: String, as type: T.Type ) throws -> T { diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index 05c34097..f3950d6b 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// import Foundation +import HTTPTypes extension Converter { @@ -23,9 +24,9 @@ extension Converter { /// - Returns: The parsed content types, or the default content types if /// the header was not provided. public func extractAcceptHeaderIfPresent( - in headerFields: [HeaderField] + in headerFields: HTTPFields ) throws -> [AcceptHeaderContentType] { - guard let rawValue = headerFields.firstValue(name: "accept") else { + guard let rawValue = headerFields[.accept] else { return AcceptHeaderContentType.defaultValues } let rawComponents = @@ -50,10 +51,12 @@ extension Converter { /// Also supports wildcars, such as "application/\*" and "\*/\*". public func validateAcceptIfPresent( _ substring: String, - in headerFields: [HeaderField] + in headerFields: HTTPFields ) throws { // for example: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8 - let acceptHeader = headerFields.values(name: "accept").joined(separator: ", ") + guard let acceptHeader = headerFields[.accept] else { + return + } // Split with commas to get the individual values let acceptValues = @@ -177,53 +180,13 @@ extension Converter { ) } - // | server | get | request body | string | optional | getOptionalRequestBodyAsString | - public func getOptionalRequestBodyAsString( - _ type: T.Type, - from data: Data?, - transforming transform: (T) -> C - ) throws -> C? { - try getOptionalRequestBody( - type, - from: data, - transforming: transform, - convert: { encodedData in - let decoder = StringDecoder( - dateTranscoder: configuration.dateTranscoder - ) - let encodedString = String(decoding: encodedData, as: UTF8.self) - return try decoder.decode(T.self, from: encodedString) - } - ) - } - - // | server | get | request body | string | required | getRequiredRequestBodyAsString | - public func getRequiredRequestBodyAsString( - _ type: T.Type, - from data: Data?, - transforming transform: (T) -> C - ) throws -> C { - try getRequiredRequestBody( - type, - from: data, - transforming: transform, - convert: { encodedData in - let decoder = StringDecoder( - dateTranscoder: configuration.dateTranscoder - ) - let encodedString = String(decoding: encodedData, as: UTF8.self) - return try decoder.decode(T.self, from: encodedString) - } - ) - } - // | server | get | request body | JSON | optional | getOptionalRequestBodyAsJSON | public func getOptionalRequestBodyAsJSON( _ type: T.Type, - from data: Data?, + from data: HTTPBody?, transforming transform: (T) -> C - ) throws -> C? { - try getOptionalRequestBody( + ) async throws -> C? { + try await getOptionalBufferingRequestBody( type, from: data, transforming: transform, @@ -234,10 +197,10 @@ extension Converter { // | server | get | request body | JSON | required | getRequiredRequestBodyAsJSON | public func getRequiredRequestBodyAsJSON( _ type: T.Type, - from data: Data?, + from data: HTTPBody?, transforming transform: (T) -> C - ) throws -> C { - try getRequiredRequestBody( + ) async throws -> C { + try await getRequiredBufferingRequestBody( type, from: data, transforming: transform, @@ -247,9 +210,9 @@ extension Converter { // | server | get | request body | binary | optional | getOptionalRequestBodyAsBinary | public func getOptionalRequestBodyAsBinary( - _ type: Data.Type, - from data: Data?, - transforming transform: (Data) -> C + _ type: HTTPBody.Type, + from data: HTTPBody?, + transforming transform: (HTTPBody) -> C ) throws -> C? { try getOptionalRequestBody( type, @@ -261,9 +224,9 @@ extension Converter { // | server | get | request body | binary | required | getRequiredRequestBodyAsBinary | public func getRequiredRequestBodyAsBinary( - _ type: Data.Type, - from data: Data?, - transforming transform: (Data) -> C + _ type: HTTPBody.Type, + from data: HTTPBody?, + transforming transform: (HTTPBody) -> C ) throws -> C { try getRequiredRequestBody( type, @@ -273,33 +236,12 @@ extension Converter { ) } - // | server | set | response body | string | required | setResponseBodyAsString | - public func setResponseBodyAsString( - _ value: T, - headerFields: inout [HeaderField], - contentType: String - ) throws -> Data { - try setResponseBody( - value, - headerFields: &headerFields, - contentType: contentType, - convert: { value in - let encoder = StringEncoder( - dateTranscoder: configuration.dateTranscoder - ) - let encodedString = try encoder.encode(value) - let encodedData = Data(encodedString.utf8) - return encodedData - } - ) - } - // | server | set | response body | JSON | required | setResponseBodyAsJSON | public func setResponseBodyAsJSON( _ value: T, - headerFields: inout [HeaderField], + headerFields: inout HTTPFields, contentType: String - ) throws -> Data { + ) throws -> HTTPBody { try setResponseBody( value, headerFields: &headerFields, @@ -310,10 +252,10 @@ extension Converter { // | server | set | response body | binary | required | setResponseBodyAsBinary | public func setResponseBodyAsBinary( - _ value: Data, - headerFields: inout [HeaderField], + _ value: HTTPBody, + headerFields: inout HTTPFields, contentType: String - ) throws -> Data { + ) throws -> HTTPBody { try setResponseBody( value, headerFields: &headerFields, diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index fe0b9c25..fe7bd011 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -12,80 +12,7 @@ // //===----------------------------------------------------------------------===// import Foundation - -extension HeaderField: CustomStringConvertible { - public var description: String { - "\(name): \(value)" - } -} - -extension Request: CustomStringConvertible { - public var description: String { - "path: \(path), query: \(query ?? ""), method: \(method), header fields: \(headerFields.description), body (prefix): \(body?.prettyPrefix ?? "")" - } -} - -extension Response: CustomStringConvertible { - public var description: String { - "status: \(statusCode), header fields: \(headerFields.description), body: \(body.prettyPrefix)" - } -} - -extension ServerRequestMetadata: CustomStringConvertible { - public var description: String { - "path parameters: \(pathParameters.description), query parameters: \(queryParameters.description)" - } -} - -extension Array where Element == HeaderField { - - /// Adds a header for the provided name and value. - /// - Parameters: - /// - name: Header name. - /// - value: Header value. If nil, the header is not added. - mutating func add(name: String, value: String?) { - guard let value = value else { - return - } - append(.init(name: name, value: value)) - } - - /// Adds headers for the provided name and values. - /// - Parameters: - /// - name: Header name. - /// - value: Header values. - mutating func add(name: String, values: [String]?) { - guard let values = values else { - return - } - for value in values { - append(.init(name: name, value: value)) - } - } - - /// Removes all headers matching the provided (case-insensitive) name. - /// - Parameters: - /// - name: Header name. - mutating func removeAll(named name: String) { - removeAll { - $0.name.caseInsensitiveCompare(name) == .orderedSame - } - } - - /// Returns the first header value for the provided (case-insensitive) name. - /// - Parameter name: Header name. - /// - Returns: First value for the given name. Nil if one does not exist. - func firstValue(name: String) -> String? { - first { $0.name.caseInsensitiveCompare(name) == .orderedSame }?.value - } - - /// Returns all header values for the given (case-insensitive) name. - /// - Parameter name: Header name. - /// - Returns: All values for the given name, might be empty if none are found. - func values(name: String) -> [String] { - filter { $0.name.caseInsensitiveCompare(name) == .orderedSame }.map { $0.value } - } -} +import HTTPTypes extension ParameterStyle { @@ -114,6 +41,30 @@ extension ParameterStyle { } } +extension HTTPField.Name { + + // TODO: Docs + init(validated name: String) throws { + guard let fieldName = Self.init(name) else { + throw RuntimeError.invalidHeaderFieldName(name) + } + self = fieldName + } +} + +extension HTTPRequest { + + // TODO: Docs + var requiredPath: Substring { + get throws { + guard let path else { + throw RuntimeError.pathUnset + } + return path[...] + } + } +} + extension Converter { // MARK: Converter helpers @@ -172,15 +123,17 @@ extension Converter { } func convertJSONToBodyCodable( - _ data: Data - ) throws -> T { - try decoder.decode(T.self, from: data) + _ body: HTTPBody + ) async throws -> T { + let data = try await body.collectAsData(upTo: .max) + return try decoder.decode(T.self, from: data) } func convertBodyCodableToJSON( _ value: T - ) throws -> Data { - try encoder.encode(value) + ) throws -> HTTPBody { + let data = try encoder.encode(value) + return HTTPBody(data: data) } func convertHeaderFieldCodableToJSON( @@ -199,8 +152,9 @@ extension Converter { } func convertFromStringData( - _ data: Data - ) throws -> T { + _ body: HTTPBody + ) async throws -> T { + let data = try await body.collect(upTo: .max) let encodedString = String(decoding: data, as: UTF8.self) let decoder = StringDecoder( dateTranscoder: configuration.dateTranscoder @@ -214,30 +168,30 @@ extension Converter { func convertToStringData( _ value: T - ) throws -> Data { + ) throws -> HTTPBody { let encoder = StringEncoder( dateTranscoder: configuration.dateTranscoder ) let encodedString = try encoder.encode(value) - return Data(encodedString.utf8) + return HTTPBody(data: Array(encodedString.utf8)) } func convertBinaryToData( - _ binary: Data - ) throws -> Data { + _ binary: HTTPBody + ) throws -> HTTPBody { binary } func convertDataToBinary( - _ data: Data - ) throws -> Data { + _ data: HTTPBody + ) throws -> HTTPBody { data } // MARK: - Helpers for specific types of parameters func setHeaderField( - in headerFields: inout [HeaderField], + in headerFields: inout HTTPFields, name: String, value: T?, convert: (T) throws -> String @@ -245,31 +199,29 @@ extension Converter { guard let value else { return } - headerFields.add( - name: name, - value: try convert(value) + try headerFields.append( + .init( + name: .init(validated: name), + value: convert(value) + ) ) } func getHeaderFieldValuesString( - in headerFields: [HeaderField], + in headerFields: HTTPFields, name: String - ) -> String? { - let values = headerFields.values(name: name) - guard !values.isEmpty else { - return nil - } - return values.joined(separator: ",") + ) throws -> String? { + try headerFields[.init(validated: name)] } func getOptionalHeaderField( - in headerFields: [HeaderField], + in headerFields: HTTPFields, name: String, as type: T.Type, convert: (String) throws -> T ) throws -> T? { guard - let stringValue = getHeaderFieldValuesString( + let stringValue = try getHeaderFieldValuesString( in: headerFields, name: name ) @@ -280,13 +232,13 @@ extension Converter { } func getRequiredHeaderField( - in headerFields: [HeaderField], + in headerFields: HTTPFields, name: String, as type: T.Type, convert: (String) throws -> T ) throws -> T { guard - let stringValue = getHeaderFieldValuesString( + let stringValue = try getHeaderFieldValuesString( in: headerFields, name: name ) @@ -297,7 +249,7 @@ extension Converter { } func setEscapedQueryItem( - in request: inout Request, + in request: inout HTTPRequest, style: ParameterStyle?, explode: Bool?, name: String, @@ -312,8 +264,31 @@ extension Converter { style: style, explode: explode ) - let uriSnippet = try convert(value, resolvedStyle, resolvedExplode) - request.addEscapedQuerySnippet(uriSnippet) + let escapedUriSnippet = try convert(value, resolvedStyle, resolvedExplode) + + let pathAndAll = try request.requiredPath + + // https://datatracker.ietf.org/doc/html/rfc3986#section-3.4 + // > The query component is indicated by the first question + // > mark ("?") character and terminated by a number sign ("#") + // > character or by the end of the URI. + + let fragmentStart = pathAndAll.firstIndex(of: "#") ?? pathAndAll.endIndex + let fragment = pathAndAll[fragmentStart..( @@ -360,20 +335,20 @@ extension Converter { func setRequiredRequestBody( _ value: T, - headerFields: inout [HeaderField], + headerFields: inout HTTPFields, contentType: String, - convert: (T) throws -> Data - ) throws -> Data { - headerFields.add(name: "content-type", value: contentType) + convert: (T) throws -> HTTPBody + ) throws -> HTTPBody { + headerFields[.contentType] = contentType return try convert(value) } func setOptionalRequestBody( _ value: T?, - headerFields: inout [HeaderField], + headerFields: inout HTTPFields, contentType: String, - convert: (T) throws -> Data - ) throws -> Data? { + convert: (T) throws -> HTTPBody + ) throws -> HTTPBody? { guard let value else { return nil } @@ -385,11 +360,24 @@ extension Converter { ) } + func getOptionalBufferingRequestBody( + _ type: T.Type, + from data: HTTPBody?, + transforming transform: (T) -> C, + convert: (HTTPBody) async throws -> T + ) async throws -> C? { + guard let data else { + return nil + } + let decoded = try await convert(data) + return transform(decoded) + } + func getOptionalRequestBody( _ type: T.Type, - from data: Data?, + from data: HTTPBody?, transforming transform: (T) -> C, - convert: (Data) throws -> T + convert: (HTTPBody) throws -> T ) throws -> C? { guard let data else { return nil @@ -398,11 +386,30 @@ extension Converter { return transform(decoded) } + func getRequiredBufferingRequestBody( + _ type: T.Type, + from data: HTTPBody?, + transforming transform: (T) -> C, + convert: (HTTPBody) async throws -> T + ) async throws -> C { + guard + let body = try await getOptionalBufferingRequestBody( + type, + from: data, + transforming: transform, + convert: convert + ) + else { + throw RuntimeError.missingRequiredRequestBody + } + return body + } + func getRequiredRequestBody( _ type: T.Type, - from data: Data?, + from data: HTTPBody?, transforming transform: (T) -> C, - convert: (Data) throws -> T + convert: (HTTPBody) throws -> T ) throws -> C { guard let body = try getOptionalRequestBody( @@ -417,11 +424,22 @@ extension Converter { return body } + func getBufferingResponseBody( + _ type: T.Type, + from data: HTTPBody, + transforming transform: (T) -> C, + convert: (HTTPBody) async throws -> T + ) async throws -> C { + let parsedValue = try await convert(data) + let transformedValue = transform(parsedValue) + return transformedValue + } + func getResponseBody( _ type: T.Type, - from data: Data, + from data: HTTPBody, transforming transform: (T) -> C, - convert: (Data) throws -> T + convert: (HTTPBody) throws -> T ) throws -> C { let parsedValue = try convert(data) let transformedValue = transform(parsedValue) @@ -430,11 +448,11 @@ extension Converter { func setResponseBody( _ value: T, - headerFields: inout [HeaderField], + headerFields: inout HTTPFields, contentType: String, - convert: (T) throws -> Data - ) throws -> Data { - headerFields.add(name: "content-type", value: contentType) + convert: (T) throws -> HTTPBody + ) throws -> HTTPBody { + headerFields[.contentType] = contentType return try convert(value) } diff --git a/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift b/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift index ce6d6833..0d39d580 100644 --- a/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift @@ -13,45 +13,6 @@ //===----------------------------------------------------------------------===// import Foundation -extension Data { - /// Returns a pretty representation of the Data. - var pretty: String { - String(decoding: self, as: UTF8.self) - } - - /// Returns a prefix of a pretty representation of the Data. - var prettyPrefix: String { - prefix(256).pretty - } -} - -extension Request { - /// Allows modifying the parsed query parameters of the request. - mutating func mutateQuery(_ closure: (inout URLComponents) throws -> Void) rethrows { - var urlComponents = URLComponents() - if let query { - urlComponents.percentEncodedQuery = query - } - try closure(&urlComponents) - query = urlComponents.percentEncodedQuery - } - - /// Adds the provided URI snippet to the URL's query. - /// - /// Percent encoding is already applied. - /// - Parameters: - /// - snippet: A full URI snippet. - mutating func addEscapedQuerySnippet(_ snippet: String) { - let prefix: String - if let query { - prefix = query + "&" - } else { - prefix = "" - } - query = prefix + snippet - } -} - extension String { /// Returns the string with leading and trailing whitespace (such as spaces diff --git a/Sources/OpenAPIRuntime/Errors/ClientError.swift b/Sources/OpenAPIRuntime/Errors/ClientError.swift index 0bd5d349..c69a0bd5 100644 --- a/Sources/OpenAPIRuntime/Errors/ClientError.swift +++ b/Sources/OpenAPIRuntime/Errors/ClientError.swift @@ -11,6 +11,9 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// + +import HTTPTypes + #if canImport(Darwin) import Foundation #else @@ -18,60 +21,41 @@ import Foundation @preconcurrency import protocol Foundation.LocalizedError #endif -/// An error thrown by a client performing an OpenAPI operation. -/// -/// Use a `ClientError` to inspect details about the request and response that resulted in an error. -/// -/// You don't create or throw instances of `ClientError` yourself; they are created and thrown on -/// your behalf by the runtime library when a client operation fails. public struct ClientError: Error { - /// The identifier of the operation, as defined in the OpenAPI document. + public var operationID: String - /// The operation-specific Input value. public var operationInput: any Sendable - /// The HTTP request created during the operation. - /// - /// Will be nil if the error resulted before the request was generated, - /// for example if generating the request from the Input failed. - public var request: Request? + public var request: HTTPRequest? + + public var requestBody: HTTPBody? - /// The base URL for HTTP requests. - /// - /// Will be nil if the error resulted before the request was generated, - /// for example if generating the request from the Input failed. public var baseURL: URL? - /// The HTTP response received during the operation. - /// - /// Will be `nil` if the error resulted before the `Response` was received. - public var response: Response? + public var response: HTTPResponse? + + public var responseBody: HTTPBody? - /// The underlying error that caused the operation to fail. public var underlyingError: any Error - /// Creates a new error. - /// - Parameters: - /// - operationID: The OpenAPI operation identifier. - /// - operationInput: The operation-specific Input value. - /// - request: The HTTP request created during the operation. - /// - baseURL: The base URL for HTTP requests. - /// - response: The HTTP response received during the operation. - /// - underlyingError: The underlying error that caused the operation to fail. public init( operationID: String, operationInput: any Sendable, - request: Request? = nil, + request: HTTPRequest? = nil, + requestBody: HTTPBody? = nil, baseURL: URL? = nil, - response: Response? = nil, + response: HTTPResponse? = nil, + responseBody: HTTPBody? = nil, underlyingError: any Error ) { self.operationID = operationID self.operationInput = operationInput self.request = request + self.requestBody = requestBody self.baseURL = baseURL self.response = response + self.responseBody = responseBody self.underlyingError = underlyingError } @@ -85,9 +69,12 @@ public struct ClientError: Error { } } +// TODO: Adopt pretty descriptions here (except the bodies). + extension ClientError: CustomStringConvertible { public var description: String { - "Client error - operationID: \(operationID), operationInput: \(String(describing: operationInput)), request: \(request?.description ?? ""), baseURL: \(baseURL?.absoluteString ?? ""), response: \(response?.description ?? ""), underlying error: \(underlyingErrorDescription)" + // TODO: Bring back all the fields for easier debugging. + "Client error - operationID: \(operationID), underlying error: \(underlyingErrorDescription)" } } diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index 205b0e33..3a2c0939 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -20,6 +20,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret // Miscs case invalidServerURL(String) case invalidExpectedContentType(String) + case invalidHeaderFieldName(String) // Data conversion case failedToDecodeStringConvertibleValue(type: String) @@ -41,6 +42,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret // Path case missingRequiredPathParameter(String) + case pathUnset // Query case missingRequiredQueryParameter(String) @@ -64,6 +66,8 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret return "Invalid server URL: \(string)" case .invalidExpectedContentType(let string): return "Invalid expected content type: '\(string)'" + case .invalidHeaderFieldName(let name): + return "Invalid header field name: '\(name)'" case .failedToDecodeStringConvertibleValue(let string): return "Failed to decode a value of type '\(string)'." case .unsupportedParameterStyle(name: let name, location: let location, style: let style, explode: let explode): @@ -79,6 +83,8 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret return "Malformed Accept header: \(accept)" case .missingRequiredPathParameter(let name): return "Missing required path parameter named: \(name)" + case .pathUnset: + return "Path was not set on the request." case .missingRequiredQueryParameter(let name): return "Missing required query parameter named: \(name)" case .missingRequiredRequestBody: diff --git a/Sources/OpenAPIRuntime/Errors/ServerError.swift b/Sources/OpenAPIRuntime/Errors/ServerError.swift index 52985d6a..fce96bf9 100644 --- a/Sources/OpenAPIRuntime/Errors/ServerError.swift +++ b/Sources/OpenAPIRuntime/Errors/ServerError.swift @@ -11,52 +11,35 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// -import Foundation -/// An error thrown by a server handling an OpenAPI operation. +import HTTPTypes +import protocol Foundation.LocalizedError + public struct ServerError: Error { - /// Identifier of the operation that threw the error. + public var operationID: String - /// HTTP request provided to the server. - public var request: Request + public var request: HTTPRequest - /// Request metadata extracted by the server. - public var requestMetadata: ServerRequestMetadata + public var requestBody: HTTPBody? - /// Operation-specific Input value. - /// - /// Is nil if error was thrown during request -> Input conversion. public var operationInput: (any Sendable)? - /// Operation-specific Output value. - /// - /// Is nil if error was thrown before/during Output -> response conversion. public var operationOutput: (any Sendable)? - /// The underlying error that caused the operation to fail. public var underlyingError: any Error - /// Creates a new error. - /// - Parameters: - /// - operationID: The OpenAPI operation identifier. - /// - request: HTTP request provided to the server. - /// - requestMetadata: Request metadata extracted by the server. - /// - operationInput: Operation-specific Input value. - /// - operationOutput: Operation-specific Output value. - /// - underlyingError: The underlying error that caused the operation - /// to fail. public init( operationID: String, - request: Request, - requestMetadata: ServerRequestMetadata, + request: HTTPRequest, + requestBody: HTTPBody?, operationInput: (any Sendable)? = nil, operationOutput: (any Sendable)? = nil, underlyingError: (any Error) ) { self.operationID = operationID self.request = request - self.requestMetadata = requestMetadata + self.requestBody = requestBody self.operationInput = operationInput self.operationOutput = operationOutput self.underlyingError = underlyingError @@ -72,9 +55,11 @@ public struct ServerError: Error { } } +// TODO: Make pretty printable (except the body) + extension ServerError: CustomStringConvertible { public var description: String { - "Server error - operationID: \(operationID), request: \(request.description), metadata: \(requestMetadata.description), operationInput: \(operationInput.map { String(describing: $0) } ?? ""), operationOutput: \(operationOutput.map { String(describing: $0) } ?? ""), underlying error: \(underlyingErrorDescription)" + "Server error - operationID: \(operationID), underlying error: \(underlyingErrorDescription)" } } diff --git a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift index 1bbb7a06..f89d2c4b 100644 --- a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift +++ b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift @@ -11,228 +11,31 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// -import Foundation -/// A type that performs HTTP operations. -/// -/// Decouples an underlying HTTP library from generated client code. -/// -/// ### Choose between a transport and a middleware -/// -/// The ``ClientTransport`` and ``ClientMiddleware`` protocols look similar, -/// however each serves a different purpose. -/// -/// A _transport_ abstracts over the underlying HTTP library that actually -/// performs the HTTP operation by using the network. A generated `Client` -/// requires an exactly one client transport. -/// -/// A _middleware_ intercepts the HTTP request and response, without being -/// responsible for performing the HTTP operation itself. That's why -/// middlewares take the extra `next` parameter, to delegate making the HTTP -/// call to the transport at the top of the middleware stack. -/// -/// ### Use an existing client transport -/// -/// Instantiate the transport using the parameters required by the specific -/// implementation. For example, using the client transport for the -/// `URLSession` HTTP client provided by the Foundation framework: -/// -/// let transport = URLSessionTransport() -/// -/// Create the base URL of the server to call using your client. If the server -/// URL was defined in the OpenAPI document, you find a generated method for it -/// on the `Servers` type, for example: -/// -/// let serverURL = try Servers.server1() -/// -/// Instantiate the `Client` type generated by the Swift OpenAPI Generator for -/// your provided OpenAPI document. For example: -/// -/// let client = Client( -/// serverURL: serverURL, -/// transport: transport -/// ) -/// -/// Use the client to make HTTP calls defined in your OpenAPI document. For -/// example, if the OpenAPI document contains an HTTP operation with -/// the identifier `checkHealth`, call it from Swift with: -/// -/// let response = try await client.checkHealth(.init()) -/// switch response { -/// case .ok(let okPayload): -/// // ... -/// -/// // Handle any HTTP status code not documented in -/// // your OpenAPI document. -/// case .undocumented(let statusCode, _): -/// // ... -/// } -/// -/// The generated operation method takes an `Input` type unique to -/// the operation, and returns an `Output` type unique to the operation. -/// -/// > Note: You use the `Input` type to provide parameters such as HTTP request headers, -/// query items, path parameters, and request bodies; and inspect the `Output` -/// type to handle the received HTTP response status code, response header and -/// body. -/// -/// ### Implement a custom client transport -/// -/// If a client transport implementation for your preferred HTTP library doesn't -/// yet exist, or you need to simulate rare network conditions in your tests, -/// consider implementing a custom client transport. -/// -/// For example, to implement a test client transport that allows you -/// to test both a healthy and unhealthy response from a `checkHealth` -/// operation, define a new struct that conforms to the `ClientTransport` -/// protocol: -/// -/// struct TestTransport: ClientTransport { -/// var isHealthy: Bool = true -/// func send( -/// _ request: Request, -/// baseURL: URL, -/// operationID: String -/// ) async throws -> Response { -/// Response(statusCode: isHealthy ? 200 : 500) -/// } -/// } -/// -/// Then in your test code, instantiate and provide the test transport to your -/// generated client instead: -/// -/// let transport = TestTransport() -/// transport.isHealthy = true // for HTTP status code 200 (success) -/// transport.isHealthy = false // for HTTP status code 500 (failure) -/// let serverURL = try Servers.server1() -/// let client = Client( -/// serverURL: serverURL, -/// transport: transport -/// ) -/// let response = try await client.checkHealth(.init()) -/// // ... -/// -/// Implementing a test client transport is just one way to help test your -/// code that integrates with a generated client. Another is to implement -/// a type conforming to the generated protocol `APIProtocol`, and to implement -/// a custom ``ClientMiddleware``. +import HTTPTypes +#if canImport(Darwin) +import struct Foundation.URL +#else +@preconcurrency import struct Foundation.URL +#endif + public protocol ClientTransport: Sendable { - /// Sends the underlying HTTP request and returns the received - /// HTTP response. - /// - Parameters: - /// - request: An HTTP request. - /// - baseURL: A server base URL. - /// - operationID: The identifier of the OpenAPI operation. - /// - Returns: An HTTP response. func send( - _ request: Request, + _ request: HTTPRequest, + body: HTTPBody?, baseURL: URL, operationID: String - ) async throws -> Response + ) async throws -> (HTTPResponse, HTTPBody) } -/// A type that intercepts HTTP requests and responses. -/// -/// It allows you to read and modify the request before it is received by -/// the transport and the response after it is returned by the transport. -/// -/// Appropriate for handling authentication, logging, metrics, tracing, -/// injecting custom headers such as "user-agent", and more. -/// -/// ### Choose between a transport and a middleware -/// -/// The ``ClientTransport`` and ``ClientMiddleware`` protocols look similar, -/// however each serves a different purpose. -/// -/// A _transport_ abstracts over the underlying HTTP library that actually -/// performs the HTTP operation by using the network. A generated `Client` -/// requires an exactly one client transport. -/// -/// A _middleware_ intercepts the HTTP request and response, without being -/// responsible for performing the HTTP operation itself. That's why -/// middlewares take the extra `next` parameter, to delegate making the HTTP -/// call to the transport at the top of the middleware stack. -/// -/// ### Use an existing client middleware -/// -/// Instantiate the middleware using the parameters required by the specific -/// implementation. For example, using a hypothetical existing middleware -/// that logs every request and response: -/// -/// let loggingMiddleware = LoggingMiddleware() -/// -/// Similarly to the process of using an existing ``ClientTransport``, provide -/// the middleware to the initializer of the generated `Client` type: -/// -/// let client = Client( -/// serverURL: serverURL, -/// transport: transport, -/// middlewares: [ -/// loggingMiddleware, -/// ] -/// ) -/// -/// Then make a call to one of the generated client methods: -/// -/// let response = try await client.checkHealth(.init()) -/// // ... -/// -/// As part of the invocation of `checkHealth`, the client first invokes -/// the middlewares in the order you provided them, and then passes the request -/// to the transport. When a response is received, the last middleware handles -/// it first, in the reverse order of the `middlewares` array. -/// -/// ### Implement a custom client middleware -/// -/// If a client middleware implementation with your desired behavior doesn't -/// yet exist, or you need to simulate rare network conditions your tests, -/// consider implementing a custom client middleware. -/// -/// For example, to implement a middleware that injects the "Authorization" -/// header to every outgoing request, define a new struct that conforms to -/// the `ClientMiddleware` protocol: -/// -/// /// Injects an authorization header to every request. -/// struct AuthenticationMiddleware: ClientMiddleware { -/// -/// /// The token value. -/// var bearerToken: String -/// -/// func intercept( -/// _ request: Request, -/// baseURL: URL, -/// operationID: String, -/// next: (Request, URL) async throws -> Response -/// ) async throws -> Response { -/// var request = request -/// request.headerFields.append(.init( -/// name: "Authorization", value: "Bearer \(bearerToken)" -/// )) -/// return try await next(request, baseURL) -/// } -/// } -/// -/// An alternative use case for a middleware is to inject random failures -/// when calling a real server, to test your retry and error-handling logic. -/// -/// Implementing a test client middleware is just one way to help test your -/// code that integrates with a generated client. Another is to implement -/// a type conforming to the generated protocol `APIProtocol`, and to implement -/// a custom ``ClientTransport``. -public protocol ClientMiddleware: Sendable { +public protocol NewClientMiddleware: Sendable { - /// Intercepts an outgoing HTTP request and an incoming HTTP response. - /// - Parameters: - /// - request: An HTTP request. - /// - baseURL: baseURL: A server base URL. - /// - operationID: The identifier of the OpenAPI operation. - /// - next: A closure that calls the next middleware, or the transport. - /// - Returns: An HTTP response. func intercept( - _ request: Request, + _ request: HTTPRequest, + body: HTTPBody?, baseURL: URL, operationID: String, - next: @Sendable (Request, URL) async throws -> Response - ) async throws -> Response + next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody) + ) async throws -> (HTTPResponse, HTTPBody) } diff --git a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift deleted file mode 100644 index 88b82794..00000000 --- a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift +++ /dev/null @@ -1,291 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// -#if canImport(Darwin) -import Foundation -#else -@preconcurrency import struct Foundation.Data -@preconcurrency import struct Foundation.URLQueryItem -#endif - -/// A header field used in an HTTP request or response. -public struct HeaderField: Hashable, Sendable { - - /// The name of the HTTP header field. - public var name: String - - /// The value of the HTTP header field. - public var value: String - - /// Creates a new HTTP header field. - /// - Parameters: - /// - name: A name of the HTTP header field. - /// - value: A value of the HTTP header field. - public init(name: String, value: String) { - self.name = name - self.value = value - } -} - -/// Describes the HTTP method used in an OpenAPI operation. -/// -/// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#fixed-fields-7 -public struct HTTPMethod: RawRepresentable, Hashable, Sendable { - - /// Describes an HTTP method explicitly supported by OpenAPI. - private enum OpenAPIHTTPMethod: String, Hashable, Sendable { - case GET - case PUT - case POST - case DELETE - case OPTIONS - case HEAD - case PATCH - case TRACE - } - - /// The underlying HTTP method. - private let value: OpenAPIHTTPMethod - - /// Creates a new method from the provided known supported HTTP method. - private init(value: OpenAPIHTTPMethod) { - self.value = value - } - - public init?(rawValue: String) { - guard let value = OpenAPIHTTPMethod(rawValue: rawValue) else { - return nil - } - self.value = value - } - - public var rawValue: String { - value.rawValue - } - - /// The name of the HTTP method. - public var name: String { - rawValue - } - - /// Returns an HTTP GET method. - public static var get: Self { - .init(value: .GET) - } - - /// Returns an HTTP PUT method. - public static var put: Self { - .init(value: .PUT) - } - - /// Returns an HTTP POST method. - public static var post: Self { - .init(value: .POST) - } - - /// Returns an HTTP DELETE method. - public static var delete: Self { - .init(value: .DELETE) - } - - /// Returns an HTTP OPTIONS method. - public static var options: Self { - .init(value: .OPTIONS) - } - - /// Returns an HTTP HEAD method. - public static var head: Self { - .init(value: .HEAD) - } - - /// Returns an HTTP PATCH method. - public static var patch: Self { - .init(value: .PATCH) - } - - /// Returns an HTTP TRACE method. - public static var trace: Self { - .init(value: .TRACE) - } -} - -/// An HTTP request, sent by the client to the server. -public struct Request: Hashable, Sendable { - - /// The path of the URL for the HTTP request. - public var path: String - - /// The query string of the URL for the HTTP request. - /// - /// A query string provides support for assigning values to parameters - /// within a URL. - /// - /// _URL encoding_, officially known as _percent-encoding_, is a method - /// to encode arbitrary data in a URI using only ASCII characters. - /// - /// An example of a URL with a query string is: - /// - /// ``` - /// https://example.com?name=Maria%20Ruiz&email=mruiz2%40icloud.com - /// ``` - /// - /// For this request, the query string is: - /// - /// ``` - /// name=Maria%20Ruiz&email=mruiz2%40icloud.com - /// ``` - /// - /// - NOTE: The `?` is a seperator in the URL and is **not** part of - /// the query string. - /// - NOTE: Only query parameter names and values are percent-encoded, - /// the `&` and `=` remain. - public var query: String? - - /// The method of the HTTP request. - public var method: HTTPMethod - - /// The header fields of the HTTP request. - public var headerFields: [HeaderField] - - /// The body data of the HTTP request. - public var body: Data? - - /// Creates a new HTTP request. - /// - Parameters: - /// - path: The path of the URL for the request. This must not include - /// the base URL of the server. - /// - query: The query string of the URL for the request. This should not - /// include the separator question mark (`?`) and the names and values - /// should be percent-encoded. See ``query`` for more information. - /// - method: The method of the HTTP request. - /// - headerFields: The header fields of the HTTP request. - /// - body: The body data of the HTTP request. - /// - /// An example of a request: - /// ``` - /// let request = Request( - /// path: "/users", - /// query: "name=Maria%20Ruiz&email=mruiz2%40icloud.com", - /// method: .GET, - /// headerFields: [ - /// .init(name: "Accept", value: "application/json" - /// ], - /// body: nil - /// ) - /// ``` - public init( - path: String, - query: String? = nil, - method: HTTPMethod, - headerFields: [HeaderField] = [], - body: Data? = nil - ) { - self.path = path - self.query = query - self.method = method - self.headerFields = headerFields - self.body = body - } -} - -/// An HTTP response, returned by the server to the client. -public struct Response: Hashable, Sendable { - - /// The status code of the HTTP response, for example `200`. - public var statusCode: Int - - /// The header fields of the HTTP response. - public var headerFields: [HeaderField] - - /// The body data of the HTTP response. - public var body: Data - - /// Creates a new HTTP response. - /// - Parameters: - /// - statusCode: The status code of the HTTP response, for example `200`. - /// - headerFeilds: The header fields of the HTTP response. - /// - body: The body data of the HTTP response. - public init( - statusCode: Int, - headerFields: [HeaderField] = [], - body: Data = .init() - ) { - self.statusCode = statusCode - self.headerFields = headerFields - self.body = body - } -} - -/// A container for request metadata already parsed and validated -/// by the server transport. -public struct ServerRequestMetadata: Hashable, Sendable { - - /// The path parameters parsed from the URL of the HTTP request. - public var pathParameters: [String: String] - - /// The query parameters parsed from the URL of the HTTP request. - public var queryParameters: [URLQueryItem] - - /// Creates a new metadata wrapper with the specified path and query parameters. - /// - Parameters: - /// - pathParameters: Path parameters parsed from the URL of the HTTP - /// request. - /// - queryParameters: The query parameters parsed from the URL of - /// the HTTP request. - public init( - pathParameters: [String: String] = [:], - queryParameters: [URLQueryItem] = [] - ) { - self.pathParameters = pathParameters - self.queryParameters = queryParameters - } -} - -/// Describes the kind and associated data of a URL path component. -public enum RouterPathComponent: Hashable, Sendable { - - /// A constant string component. - /// - /// For example, in `/pets`, the associated value is "pets". - case constant(String) - - /// A parameter component. - /// - /// For example, in `/{petId}`, the associated value is "petId" - case parameter(String) -} - -extension RouterPathComponent: ExpressibleByStringLiteral { - /// Creates a new component for the provided value. - /// - Parameter value: A string literal. If the string begins with - /// a colon (for example `":petId"`), it gets parsed as a parameter - /// component, otherwise it is treated as a constant component. - public init(stringLiteral value: StringLiteralType) { - if value.first == ":" { - self = .parameter(String(value.dropFirst())) - } else { - self = .constant(value) - } - } -} - -extension RouterPathComponent: CustomStringConvertible { - public var description: String { - switch self { - case .constant(let string): - return string - case .parameter(let string): - return ":\(string)" - } - } -} diff --git a/Sources/OpenAPIRuntime/Interface/ServerTransport.swift b/Sources/OpenAPIRuntime/Interface/ServerTransport.swift index 11774140..d6a230e2 100644 --- a/Sources/OpenAPIRuntime/Interface/ServerTransport.swift +++ b/Sources/OpenAPIRuntime/Interface/ServerTransport.swift @@ -12,211 +12,23 @@ // //===----------------------------------------------------------------------===// -/// A type that registers and handles HTTP operations. -/// -/// Decouples the HTTP server framework from the generated server code. -/// -/// ### Choose between a transport and a middleware -/// -/// The ``ServerTransport`` and ``ServerMiddleware`` protocols look similar, -/// however each serves a different purpose. -/// -/// A _transport_ abstracts over the underlying HTTP library that actually -/// receives the HTTP requests from the network. An implemented _handler_ -/// (a type implemented by you that conforms to the generated `APIProtocol` -/// protocol) is generally configured with exactly one server transport. -/// -/// A _middleware_ intercepts the HTTP request and response, without being -/// responsible for receiving the HTTP operations itself. That's why -/// middlewares take the extra `next` parameter, to delegate calling the handler -/// to the transport at the top of the middleware stack. -/// -/// ### Use an existing server transport -/// -/// Instantiate the transport using the parameters required by the specific -/// implementation. For example, using the server transport for the -/// `Vapor` web framework, first create the `Application` object provided by -/// Vapor, and provided it to the initializer of `VaporTransport`: -/// -/// let app = Vapor.Application() -/// let transport = VaporTransport(routesBuilder: app) -/// -/// Implement a new type that conforms to the generated `APIProtocol`, which -/// serves as the request handler of your server's business logic. For example, -/// this is what a simple implementation of a server that has a single -/// HTTP operation called `checkHealth` defined in the OpenAPI document, and -/// it always returns the 200 HTTP status code: -/// -/// struct MyAPIImplementation: APIProtocol { -/// func checkHealth( -/// _ input: Operations.checkHealth.Input -/// ) async throws -> Operations.checkHealth.Output { -/// .ok(.init()) -/// } -/// } -/// -/// The generated operation method takes an `Input` type unique to -/// the operation, and returns an `Output` type unique to the operation. -/// -/// > Note: You use the `Input` type to provide parameters such as HTTP request -/// headers, query items, path parameters, and request bodies; and inspect -/// the `Output` type to handle the received HTTP response status code, -/// response header and body. -/// -/// Create an instance of your handler: -/// -/// let handler = MyAPIImplementation() -/// -/// Create the URL where the server will run. The path of the URL is extracted -/// by the transport to create a common prefix (such as `/api/v1`) that might -/// be expected by the clients. If the server URL is defined in the OpenAPI -/// document, find the generated method for it on the `Servers` type, -/// for example: -/// -/// let serverURL = try Servers.server1() -/// -/// Register the generated request handlers by calling the method generated -/// on the `APIProtocol` protocol: -/// -/// try handler.registerHandlers(on: transport, serverURL: serverURL) -/// -/// Start the server by following the documentation of your chosen transport: -/// -/// try app.run() -/// -/// ### Implement a custom server transport -/// -/// If a server transport implementation for your preferred web framework -/// doesn't yet exist, or you need to simulate rare network conditions in -/// your tests, consider implementing a custom server transport. -/// -/// Define a new type that conforms to the `ServerTransport` protocol by -/// registering request handlers with the underlying web framework, to be -/// later called when the web framework receives an HTTP request to one -/// of the HTTP routes. -/// -/// In tests, this might require using the web framework's specific test -/// APIs to allow for simulating incoming HTTP requests. -/// -/// Implementing a test server transport is just one way to help test your -/// code that integrates with your handler. Another is to implement -/// a type conforming to the generated protocol `APIProtocol`, and to implement -/// a custom ``ServerMiddleware``. +import HTTPTypes + public protocol ServerTransport { - /// Registers an HTTP operation handler at the provided path and method. - /// - Parameters: - /// - handler: A handler to be invoked when an HTTP request is received. - /// - method: An HTTP request method. - /// - path: The URL path components, for example `["pets", ":petId"]`. - /// - queryItemNames: The names of query items to be extracted - /// from the request URL that matches the provided HTTP operation. func register( - _ handler: @Sendable @escaping (Request, ServerRequestMetadata) async throws -> Response, - method: HTTPMethod, - path: [RouterPathComponent], - queryItemNames: Set + _ handler: @Sendable @escaping (HTTPRequest, HTTPBody?) async throws -> (HTTPResponse, HTTPBody), + method: HTTPRequest.Method, + path: String ) throws } -/// A type that intercepts HTTP requests and responses. -/// -/// It allows you to customize the request after it was provided by -/// the transport, but before it was parsed, validated, and provided to -/// the request handler; and the response after it was provided by the request -/// handler, but before it was handed back to the transport. -/// -/// Appropriate for verifying authentication, performing logging, metrics, -/// tracing, injecting custom headers such as "user-agent", and more. -/// -/// ### Choose between a transport and a middleware -/// -/// The ``ServerTransport`` and ``ServerMiddleware`` protocols look similar, -/// however each serves a different purpose. -/// -/// A _transport_ abstracts over the underlying HTTP library that actually -/// receives the HTTP requests from the network. An implemented _handler_ -/// (a type implemented by you that conforms to the generated `APIProtocol` -/// protocol) is generally configured with exactly one server transport. -/// -/// A _middleware_ intercepts the HTTP request and response, without being -/// responsible for receiving the HTTP operations itself. That's why -/// middlewares take the extra `next` parameter, to delegate calling the handler -/// to the transport at the top of the middleware stack. -/// -/// ### Use an existing server middleware -/// -/// Instantiate the middleware using the parameters required by the specific -/// implementation. For example, using a hypothetical existing middleware -/// that logs every request and response: -/// -/// let loggingMiddleware = LoggingMiddleware() -/// -/// Similarly to the process of using an existing ``ServerTransport``, provide -/// the middleware to the call to register handlers: -/// -/// try handler.registerHandlers( -/// on: transport, -/// serverURL: serverURL, -/// middlewares: [ -/// loggingMiddleware, -/// ] -/// ) -/// -/// Then when an HTTP request is received, the server first invokes -/// the middlewares in the order you provided them, and then passes -/// the parsed request to your handler. When a response is received from -/// the handler, the last middleware handles the response first, and it goes -/// back in the reverse order of the `middlewares` array. At the end, -/// the transport sends the final response back to the client. -/// -/// ### Implement a custom server middleware -/// -/// If a server middleware implementation with your desired behavior doesn't -/// yet exist, or you need to simulate rare requests in your tests, -/// consider implementing a custom server middleware. -/// -/// For example, an implementation a middleware that prints only basic -/// information about the incoming request and outgoing response: -/// -/// /// A middleware that prints request and response metadata. -/// struct PrintingMiddleware: ServerMiddleware { -/// func intercept( -/// _ request: Request, -/// metadata: ServerRequestMetadata, -/// operationID: String, -/// next: (Request, ServerRequestMetadata) async throws -> Response -/// ) async throws -> Response { -/// print(">>>: \(request.method.name) \(request.path)") -/// do { -/// let response = try await next(request, metadata) -/// print("<<<: \(response.statusCode)") -/// return response -/// } catch { -/// print("!!!: \(error.localizedDescription)") -/// throw error -/// } -/// } -/// } -/// -/// Implementing a test server middleware is just one way to help test your -/// code that integrates with your handler. Another is to implement -/// a type conforming to the generated protocol `APIProtocol`, and to implement -/// a custom ``ServerTransport``. public protocol ServerMiddleware: Sendable { - /// Intercepts an incoming HTTP request and an outgoing HTTP response. - /// - Parameters: - /// - request: An HTTP request. - /// - metadata: The metadata parsed from the HTTP request, including path - /// and query parameters. - /// - operationID: The identifier of the OpenAPI operation. - /// - next: A closure that calls the next middleware, or the transport. - /// - Returns: An HTTP response. func intercept( - _ request: Request, - metadata: ServerRequestMetadata, + _ request: HTTPRequest, + body: HTTPBody?, operationID: String, - next: @Sendable (Request, ServerRequestMetadata) async throws -> Response - ) async throws -> Response + next: @Sendable (HTTPRequest, HTTPBody?) async throws -> (HTTPResponse, HTTPBody) + ) async throws -> (HTTPResponse, HTTPBody) } diff --git a/Sources/OpenAPIRuntime/Interface/UniversalClient.swift b/Sources/OpenAPIRuntime/Interface/UniversalClient.swift index 7e486328..aa2eb710 100644 --- a/Sources/OpenAPIRuntime/Interface/UniversalClient.swift +++ b/Sources/OpenAPIRuntime/Interface/UniversalClient.swift @@ -11,39 +11,26 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// +import HTTPTypes #if canImport(Darwin) import Foundation #else @preconcurrency import struct Foundation.URL #endif -/// OpenAPI document-agnostic HTTP client used by OpenAPI document-specific, -/// generated clients to perform request serialization, middleware and transport -/// invocation, and response deserialization. -/// -/// Do not call this directly, only invoked by generated code. @_spi(Generated) public struct UniversalClient: Sendable { - /// The URL of the server, used as the base URL for requests made by the - /// client. public let serverURL: URL - - /// Converter for encoding/decoding data. public let converter: Converter - - /// Type capable of sending HTTP requests and receiving HTTP responses. public var transport: any ClientTransport + public var middlewares: [any NewClientMiddleware] - /// Middlewares to be invoked before `transport`. - public var middlewares: [any ClientMiddleware] - - /// Internal initializer that takes an initialized `Converter`. internal init( serverURL: URL, converter: Converter, transport: any ClientTransport, - middlewares: [any ClientMiddleware] + middlewares: [any NewClientMiddleware] ) { self.serverURL = serverURL self.converter = converter @@ -51,12 +38,11 @@ public struct UniversalClient: Sendable { self.middlewares = middlewares } - /// Creates a new client. public init( serverURL: URL = .defaultOpenAPIServerURL, configuration: Configuration = .init(), transport: any ClientTransport, - middlewares: [any ClientMiddleware] = [] + middlewares: [any NewClientMiddleware] = [] ) { self.init( serverURL: serverURL, @@ -66,28 +52,11 @@ public struct UniversalClient: Sendable { ) } - /// Performs the HTTP operation. - /// - /// Should only be called by generated code, not directly. - /// - /// An operation consists of three steps: - /// 1. Convert Input into an HTTP request. - /// 2. Invoke the `ClientTransport` to perform the HTTP call, wrapped by middlewares. - /// 3. Convert the HTTP response into Output. - /// - /// It wraps any thrown errors and attaches appropriate context. - /// - /// - Parameters: - /// - input: Operation-specific input value. - /// - operationID: The OpenAPI operation identifier. - /// - serializer: Creates an HTTP request from the provided Input value. - /// - deserializer: Creates an Output value from the provided HTTP response. - /// - Returns: The Output value produced by `deserializer`. public func send( input: OperationInput, forOperation operationID: String, - serializer: @Sendable (OperationInput) throws -> Request, - deserializer: @Sendable (Response) throws -> OperationOutput + serializer: @Sendable (OperationInput) throws -> (HTTPRequest, HTTPBody?), + deserializer: @Sendable (HTTPResponse, HTTPBody) throws -> OperationOutput ) async throws -> OperationOutput where OperationInput: Sendable, OperationOutput: Sendable { @Sendable func wrappingErrors( @@ -102,30 +71,36 @@ public struct UniversalClient: Sendable { } let baseURL = serverURL func makeError( - request: Request? = nil, + request: HTTPRequest? = nil, + requestBody: HTTPBody? = nil, baseURL: URL? = nil, - response: Response? = nil, + response: HTTPResponse? = nil, + responseBody: HTTPBody? = nil, error: any Error ) -> any Error { ClientError( operationID: operationID, operationInput: input, request: request, + requestBody: requestBody, baseURL: baseURL, response: response, + responseBody: responseBody, underlyingError: error ) } - let request: Request = try await wrappingErrors { + let (request, requestBody): (HTTPRequest, HTTPBody?) = try await wrappingErrors { try serializer(input) } mapError: { error in makeError(error: error) } - let response: Response = try await wrappingErrors { - var next: @Sendable (Request, URL) async throws -> Response = { (_request, _url) in + let (response, responseBody): (HTTPResponse, HTTPBody) = try await wrappingErrors { + var next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody) = { + (_request, _body, _url) in try await wrappingErrors { try await transport.send( _request, + body: _body, baseURL: _url, operationID: operationID ) @@ -138,18 +113,19 @@ public struct UniversalClient: Sendable { next = { try await middleware.intercept( $0, - baseURL: $1, + body: $1, + baseURL: $2, operationID: operationID, next: tmp ) } } - return try await next(request, baseURL) + return try await next(request, requestBody, baseURL) } mapError: { error in makeError(request: request, baseURL: baseURL, error: error) } return try await wrappingErrors { - try deserializer(response) + try deserializer(response, responseBody) } mapError: { error in makeError(request: request, baseURL: baseURL, response: response, error: error) } diff --git a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift index e6c862b6..4a91692b 100644 --- a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift +++ b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift @@ -11,35 +11,28 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// + +import HTTPTypes + #if canImport(Darwin) -import Foundation +import struct Foundation.URL +import struct Foundation.URLComponents #else @preconcurrency import struct Foundation.URL @preconcurrency import struct Foundation.URLComponents #endif -/// OpenAPI document-agnostic HTTP server used by OpenAPI document-specific, -/// generated servers to perform request deserialization, middleware and handler -/// invocation, and response serialization. -/// -/// Do not call this directly, only invoked by generated code. @_spi(Generated) public struct UniversalServer: Sendable { - /// The URL of the server, used to determine the path prefix for - /// registered request handlers. public var serverURL: URL - /// Helper for configuration driven data conversion public var converter: Converter - /// Implements the handler for each HTTP operation. public var handler: APIHandler - /// Middlewares to be invoked before `api` handles the request. public var middlewares: [any ServerMiddleware] - /// Internal initializer that takes an initialized converter. internal init( serverURL: URL, converter: Converter, @@ -52,7 +45,6 @@ public struct UniversalServer: Sendable { self.middlewares = middlewares } - /// Creates a new server with the specified parameters. public init( serverURL: URL = .defaultOpenAPIServerURL, handler: APIHandler, @@ -67,32 +59,14 @@ public struct UniversalServer: Sendable { ) } - /// Performs the operation. - /// - /// Should only be called by generated code, not directly. - /// - /// An operation consists of three steps (middlewares happen before 1 and after 3): - /// 1. Convert HTTP request into Input. - /// 2. Invoke the user handler to perform the user logic. - /// 3. Convert Output into an HTTP response. - /// - /// It wraps any thrown errors and attaching appropriate context. - /// - Parameters: - /// - request: HTTP request. - /// - metadata: HTTP request metadata. - /// - operationID: The OpenAPI operation identifier. - /// - handlerMethod: User handler method. - /// - deserializer: Creates an Input value from the provided HTTP request. - /// - serializer: Creates an HTTP response from the provided Output value. - /// - Returns: The HTTP response produced by `serializer`. public func handle( - request: Request, - with metadata: ServerRequestMetadata, + request: HTTPRequest, + requestBody: HTTPBody?, forOperation operationID: String, using handlerMethod: @Sendable @escaping (APIHandler) -> ((OperationInput) async throws -> OperationOutput), - deserializer: @Sendable @escaping (Request, ServerRequestMetadata) throws -> OperationInput, - serializer: @Sendable @escaping (OperationOutput, Request) throws -> Response - ) async throws -> Response where OperationInput: Sendable, OperationOutput: Sendable { + deserializer: @Sendable @escaping (HTTPRequest, HTTPBody?) throws -> OperationInput, + serializer: @Sendable @escaping (OperationOutput, HTTPRequest) throws -> (HTTPResponse, HTTPBody) + ) async throws -> (HTTPResponse, HTTPBody) where OperationInput: Sendable, OperationOutput: Sendable { @Sendable func wrappingErrors( work: () async throws -> R, @@ -113,15 +87,17 @@ public struct UniversalServer: Sendable { ServerError( operationID: operationID, request: request, - requestMetadata: metadata, + requestBody: requestBody, operationInput: input, operationOutput: output, underlyingError: error ) } - var next: @Sendable (Request, ServerRequestMetadata) async throws -> Response = { _request, _metadata in + var next: @Sendable (HTTPRequest, HTTPBody?) async throws -> (HTTPResponse, HTTPBody) = { + _request, + _requestBody in let input: OperationInput = try await wrappingErrors { - try deserializer(_request, _metadata) + try deserializer(_request, _requestBody) } mapError: { error in makeError(error: error) } @@ -146,29 +122,24 @@ public struct UniversalServer: Sendable { next = { try await middleware.intercept( $0, - metadata: $1, + body: $1, operationID: operationID, next: tmp ) } } - return try await next(request, metadata) + return try await next(request, requestBody) } - /// Prepends the path components for the selected remote server URL. public func apiPathComponentsWithServerPrefix( - _ path: [RouterPathComponent] - ) throws -> [RouterPathComponent] { + _ path: String + ) throws -> String { // Operation path is for example [pets, 42] // Server may be configured with a prefix, for example http://localhost/foo/bar/v1 // Goal is to return something like [foo, bar, v1, pets, 42] guard let components = URLComponents(url: serverURL, resolvingAgainstBaseURL: false) else { throw RuntimeError.invalidServerURL(serverURL.absoluteString) } - let prefixComponents = components.path - .split(separator: "/") - .filter { !$0.isEmpty } - .map { RouterPathComponent.constant(String($0)) } - return prefixComponents + path + return components.path + path } } From d046bf94ff9124d027a883b3b72fccfe7693ab88 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 4 Sep 2023 17:51:05 +0200 Subject: [PATCH 04/55] WIP --- .../Conversion/Converter+Server.swift | 6 +- .../Conversion/CurrencyExtensions.swift | 49 +- .../OpenAPIRuntime/Errors/ServerError.swift | 4 + .../Interface/ClientTransport.swift | 2 +- .../Interface/CurrencyTypes.swift | 26 + .../OpenAPIRuntime/Interface/HTTPBody.swift | 8 + .../Interface/ServerTransport.swift | 5 +- .../Interface/UniversalClient.swift | 10 +- .../Interface/UniversalServer.swift | 14 +- .../URICoder/Decoding/URIDecoder.swift | 4 +- .../URICoder/Parsing/URIParser.swift | 2 +- .../Conversion/Test_Converter+Client.swift | 610 +++++++------- .../Conversion/Test_Converter+Common.swift | 534 ++++++------ .../Conversion/Test_Converter+Server.swift | 758 +++++++++--------- .../Conversion/Test_CurrencyExtensions.swift | 86 -- .../Interface/Test_UniversalServer.swift | 94 +-- Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 38 +- .../URICoder/Parsing/Test_URIParser.swift | 2 +- .../URICoder/Test_URICodingRoundtrip.swift | 2 +- 19 files changed, 1119 insertions(+), 1135 deletions(-) create mode 100644 Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift delete mode 100644 Tests/OpenAPIRuntimeTests/Conversion/Test_CurrencyExtensions.swift diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index f3950d6b..0a1d5ba1 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -87,7 +87,7 @@ extension Converter { // | server | get | request path | URI | required | getPathParameterAsURI | public func getPathParameterAsURI( - in pathParameters: [String: String], + in pathParameters: [String: Substring], name: String, as type: T.Type ) throws -> T { @@ -116,7 +116,7 @@ extension Converter { // | server | get | request query | URI | optional | getOptionalQueryItemAsURI | public func getOptionalQueryItemAsURI( - in query: String?, + in query: Substring?, style: ParameterStyle?, explode: Bool?, name: String, @@ -149,7 +149,7 @@ extension Converter { // | server | get | request query | URI | required | getRequiredQueryItemAsURI | public func getRequiredQueryItemAsURI( - in query: String?, + in query: Substring?, style: ParameterStyle?, explode: Bool?, name: String, diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index fe7bd011..95effb05 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -65,6 +65,35 @@ extension HTTPRequest { } } +extension HTTPRequest { + + @_spi(Generated) + public init(path: String, method: Method) { + self.init(method: method, scheme: nil, authority: nil, path: path) + } + + @_spi(Generated) + public var query: Substring? { + guard let path else { + return nil + } + guard let queryStart = path.firstIndex(of: "?") else { + return nil + } + let queryEnd = path.firstIndex(of: "#") ?? path.endIndex + let query = path[path.index(after: queryStart) ..< queryEnd] + return query + } +} + +extension HTTPResponse { + + @_spi(Generated) + public init(statusCode: Int) { + self.init(status: .init(code: statusCode)) + } +} + extension Converter { // MARK: Converter helpers @@ -105,7 +134,7 @@ extension Converter { explode: Bool, inBody: Bool, key: String, - encodedValue: String + encodedValue: some StringProtocol ) throws -> T { let decoder = URIDecoder( configuration: uriCoderConfiguration( @@ -117,7 +146,7 @@ extension Converter { let value = try decoder.decode( T.self, forKey: key, - from: encodedValue + from: Substring(encodedValue) ) return value } @@ -283,21 +312,21 @@ extension Converter { guard let queryStart else { // No existing query substring, add the question mark. - request.path = pathAndAll.appending("\(path)?\(escapedUriSnippet)\(fragment)") + request.path = path.appending("?\(escapedUriSnippet)\(fragment)") return } let query = pathAndAll[pathAndAll.index(after: queryStart)..( - in query: String?, + in query: Substring?, style: ParameterStyle?, explode: Bool?, name: String, as type: T.Type, - convert: (String, ParameterStyle, Bool) throws -> T + convert: (Substring, ParameterStyle, Bool) throws -> T ) throws -> T? { guard let query else { return nil @@ -311,12 +340,12 @@ extension Converter { } func getRequiredQueryItem( - in query: String?, + in query: Substring?, style: ParameterStyle?, explode: Bool?, name: String, as type: T.Type, - convert: (String, ParameterStyle, Bool) throws -> T + convert: (Substring, ParameterStyle, Bool) throws -> T ) throws -> T { guard let value = try getOptionalQueryItem( @@ -457,10 +486,10 @@ extension Converter { } func getRequiredRequestPath( - in pathParameters: [String: String], + in pathParameters: [String: Substring], name: String, as type: T.Type, - convert: (String) throws -> T + convert: (Substring) throws -> T ) throws -> T { guard let untypedValue = pathParameters[name] else { throw RuntimeError.missingRequiredPathParameter(name) diff --git a/Sources/OpenAPIRuntime/Errors/ServerError.swift b/Sources/OpenAPIRuntime/Errors/ServerError.swift index fce96bf9..badd43ff 100644 --- a/Sources/OpenAPIRuntime/Errors/ServerError.swift +++ b/Sources/OpenAPIRuntime/Errors/ServerError.swift @@ -22,6 +22,8 @@ public struct ServerError: Error { public var request: HTTPRequest public var requestBody: HTTPBody? + + public var metadata: ServerRequestMetadata public var operationInput: (any Sendable)? @@ -33,6 +35,7 @@ public struct ServerError: Error { operationID: String, request: HTTPRequest, requestBody: HTTPBody?, + metadata: ServerRequestMetadata, operationInput: (any Sendable)? = nil, operationOutput: (any Sendable)? = nil, underlyingError: (any Error) @@ -40,6 +43,7 @@ public struct ServerError: Error { self.operationID = operationID self.request = request self.requestBody = requestBody + self.metadata = metadata self.operationInput = operationInput self.operationOutput = operationOutput self.underlyingError = underlyingError diff --git a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift index f89d2c4b..c86de753 100644 --- a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift +++ b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift @@ -29,7 +29,7 @@ public protocol ClientTransport: Sendable { ) async throws -> (HTTPResponse, HTTPBody) } -public protocol NewClientMiddleware: Sendable { +public protocol ClientMiddleware: Sendable { func intercept( _ request: HTTPRequest, diff --git a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift new file mode 100644 index 00000000..a890e58f --- /dev/null +++ b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift @@ -0,0 +1,26 @@ +// +// File.swift +// +// +// Created by Honza Dvorsky on 9/4/23. +// + +import Foundation + +/// A container for request metadata already parsed and validated +/// by the server transport. +public struct ServerRequestMetadata: Hashable, Sendable { + + /// The path parameters parsed from the URL of the HTTP request. + public var pathParameters: [String: Substring] + + /// Creates a new metadata wrapper with the specified path and query parameters. + /// - Parameters: + /// - pathParameters: Path parameters parsed from the URL of the HTTP + /// request. + public init( + pathParameters: [String: Substring] = [:] + ) { + self.pathParameters = pathParameters + } +} diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index 4e4a4731..0ee94c99 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -97,6 +97,14 @@ extension HTTPBody: Hashable { extension HTTPBody { + public convenience init() { + self.init( + dataChunks: [], + length: .known(0), + iterationBehavior: .multiple + ) + } + public convenience init( data: DataType, length: Length diff --git a/Sources/OpenAPIRuntime/Interface/ServerTransport.swift b/Sources/OpenAPIRuntime/Interface/ServerTransport.swift index d6a230e2..4af9aaac 100644 --- a/Sources/OpenAPIRuntime/Interface/ServerTransport.swift +++ b/Sources/OpenAPIRuntime/Interface/ServerTransport.swift @@ -17,7 +17,7 @@ import HTTPTypes public protocol ServerTransport { func register( - _ handler: @Sendable @escaping (HTTPRequest, HTTPBody?) async throws -> (HTTPResponse, HTTPBody), + _ handler: @Sendable @escaping (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody), method: HTTPRequest.Method, path: String ) throws @@ -28,7 +28,8 @@ public protocol ServerMiddleware: Sendable { func intercept( _ request: HTTPRequest, body: HTTPBody?, + metadata: ServerRequestMetadata, operationID: String, - next: @Sendable (HTTPRequest, HTTPBody?) async throws -> (HTTPResponse, HTTPBody) + next: @Sendable (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody) ) async throws -> (HTTPResponse, HTTPBody) } diff --git a/Sources/OpenAPIRuntime/Interface/UniversalClient.swift b/Sources/OpenAPIRuntime/Interface/UniversalClient.swift index aa2eb710..32644b97 100644 --- a/Sources/OpenAPIRuntime/Interface/UniversalClient.swift +++ b/Sources/OpenAPIRuntime/Interface/UniversalClient.swift @@ -24,13 +24,13 @@ public struct UniversalClient: Sendable { public let serverURL: URL public let converter: Converter public var transport: any ClientTransport - public var middlewares: [any NewClientMiddleware] + public var middlewares: [any ClientMiddleware] internal init( serverURL: URL, converter: Converter, transport: any ClientTransport, - middlewares: [any NewClientMiddleware] + middlewares: [any ClientMiddleware] ) { self.serverURL = serverURL self.converter = converter @@ -42,7 +42,7 @@ public struct UniversalClient: Sendable { serverURL: URL = .defaultOpenAPIServerURL, configuration: Configuration = .init(), transport: any ClientTransport, - middlewares: [any NewClientMiddleware] = [] + middlewares: [any ClientMiddleware] = [] ) { self.init( serverURL: serverURL, @@ -56,7 +56,7 @@ public struct UniversalClient: Sendable { input: OperationInput, forOperation operationID: String, serializer: @Sendable (OperationInput) throws -> (HTTPRequest, HTTPBody?), - deserializer: @Sendable (HTTPResponse, HTTPBody) throws -> OperationOutput + deserializer: @Sendable (HTTPResponse, HTTPBody) async throws -> OperationOutput ) async throws -> OperationOutput where OperationInput: Sendable, OperationOutput: Sendable { @Sendable func wrappingErrors( @@ -125,7 +125,7 @@ public struct UniversalClient: Sendable { makeError(request: request, baseURL: baseURL, error: error) } return try await wrappingErrors { - try deserializer(response, responseBody) + try await deserializer(response, responseBody) } mapError: { error in makeError(request: request, baseURL: baseURL, response: response, error: error) } diff --git a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift index 4a91692b..0b1a40d1 100644 --- a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift +++ b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift @@ -62,9 +62,10 @@ public struct UniversalServer: Sendable { public func handle( request: HTTPRequest, requestBody: HTTPBody?, + metadata: ServerRequestMetadata, forOperation operationID: String, using handlerMethod: @Sendable @escaping (APIHandler) -> ((OperationInput) async throws -> OperationOutput), - deserializer: @Sendable @escaping (HTTPRequest, HTTPBody?) throws -> OperationInput, + deserializer: @Sendable @escaping (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> OperationInput, serializer: @Sendable @escaping (OperationOutput, HTTPRequest) throws -> (HTTPResponse, HTTPBody) ) async throws -> (HTTPResponse, HTTPBody) where OperationInput: Sendable, OperationOutput: Sendable { @Sendable @@ -88,16 +89,18 @@ public struct UniversalServer: Sendable { operationID: operationID, request: request, requestBody: requestBody, + metadata: metadata, operationInput: input, operationOutput: output, underlyingError: error ) } - var next: @Sendable (HTTPRequest, HTTPBody?) async throws -> (HTTPResponse, HTTPBody) = { + var next: @Sendable (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody) = { _request, - _requestBody in + _requestBody, + _metadata in let input: OperationInput = try await wrappingErrors { - try deserializer(_request, _requestBody) + try await deserializer(_request, _requestBody, _metadata) } mapError: { error in makeError(error: error) } @@ -123,12 +126,13 @@ public struct UniversalServer: Sendable { try await middleware.intercept( $0, body: $1, + metadata: $2, operationID: operationID, next: tmp ) } } - return try await next(request, requestBody) + return try await next(request, requestBody, metadata) } public func apiPathComponentsWithServerPrefix( diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift index d53c0341..263f7a69 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift @@ -75,7 +75,7 @@ extension URIDecoder { func decode( _ type: T.Type = T.self, forKey key: String = "", - from data: String + from data: Substring ) throws -> T { try withCachedParser(from: data) { decoder in try decoder.decode(type, forKey: key) @@ -91,7 +91,7 @@ extension URIDecoder { /// the `decode` method on `URICachedDecoder`. /// - Returns: The result of the closure invocation. func withCachedParser( - from data: String, + from data: Substring, calls: (URICachedDecoder) throws -> R ) throws -> R { var parser = URIParser(configuration: configuration, data: data) diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift index 795acc6e..8953da91 100644 --- a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift @@ -29,7 +29,7 @@ struct URIParser: Sendable { /// - configuration: The configuration instructing the parser how /// to interpret the raw string. /// - data: The string to parse. - init(configuration: URICoderConfiguration, data: String) { + init(configuration: URICoderConfiguration, data: Substring) { self.configuration = configuration self.data = data[...] } diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift index 11c44cbf..d70176c8 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -1,310 +1,310 @@ -//===----------------------------------------------------------------------===// +////===----------------------------------------------------------------------===// +//// +//// 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 XCTest +//@_spi(Generated)@testable import OpenAPIRuntime // -// This source file is part of the SwiftOpenAPIGenerator open source project +//final class Test_ClientConverterExtensions: Test_Runtime { // -// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors -// Licensed under Apache License v2.0 +// func test_setAcceptHeader() throws { +// var headerFields: [HeaderField] = [] +// converter.setAcceptHeader( +// in: &headerFields, +// contentTypes: [.init(contentType: TestAcceptable.json, quality: 0.8)] +// ) +// XCTAssertEqual( +// headerFields, +// [ +// .init(name: "accept", value: "application/json; q=0.800") +// ] +// ) +// } // -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// // MARK: Converter helper methods // -// SPDX-License-Identifier: Apache-2.0 +// // | client | set | request path | URI | required | renderedPath | +// func test_renderedPath_string() throws { +// let renderedPath = try converter.renderedPath( +// template: "/items/{}/detail/{}/habitats/{}", +// parameters: [ +// 1 as Int, +// "foo" as String, +// [.land, .air] as [TestHabitat], +// ] +// ) +// XCTAssertEqual(renderedPath, "/items/1/detail/foo/habitats/land,air") +// } // -//===----------------------------------------------------------------------===// -import XCTest -@_spi(Generated)@testable import OpenAPIRuntime - -final class Test_ClientConverterExtensions: Test_Runtime { - - func test_setAcceptHeader() throws { - var headerFields: [HeaderField] = [] - converter.setAcceptHeader( - in: &headerFields, - contentTypes: [.init(contentType: TestAcceptable.json, quality: 0.8)] - ) - XCTAssertEqual( - headerFields, - [ - .init(name: "accept", value: "application/json; q=0.800") - ] - ) - } - - // MARK: Converter helper methods - - // | client | set | request path | URI | required | renderedPath | - func test_renderedPath_string() throws { - let renderedPath = try converter.renderedPath( - template: "/items/{}/detail/{}/habitats/{}", - parameters: [ - 1 as Int, - "foo" as String, - [.land, .air] as [TestHabitat], - ] - ) - XCTAssertEqual(renderedPath, "/items/1/detail/foo/habitats/land,air") - } - - // | client | set | request query | URI | both | setQueryItemAsURI | - func test_setQueryItemAsURI_string() throws { - var request = testRequest - try converter.setQueryItemAsURI( - in: &request, - style: nil, - explode: nil, - name: "search", - value: "foo" - ) - XCTAssertEqual(request.query, "search=foo") - } - - func test_setQueryItemAsURI_stringConvertible_needsEncoding() throws { - var request = testRequest - try converter.setQueryItemAsURI( - in: &request, - style: nil, - explode: nil, - name: "search", - value: "h%llo" - ) - XCTAssertEqual(request.query, "search=h%25llo") - } - - func test_setQueryItemAsURI_arrayOfStrings() throws { - var request = testRequest - try converter.setQueryItemAsURI( - in: &request, - style: nil, - explode: nil, - name: "search", - value: ["foo", "bar"] - ) - XCTAssertEqual(request.query, "search=foo&search=bar") - } - - func test_setQueryItemAsURI_arrayOfStrings_unexploded() throws { - var request = testRequest - try converter.setQueryItemAsURI( - in: &request, - style: nil, - explode: false, - name: "search", - value: ["foo", "bar"] - ) - XCTAssertEqual(request.query, "search=foo,bar") - } - - func test_setQueryItemAsURI_date() throws { - var request = testRequest - try converter.setQueryItemAsURI( - in: &request, - style: nil, - explode: nil, - name: "search", - value: testDate - ) - XCTAssertEqual(request.query, "search=2023-01-18T10%3A04%3A11Z") - } - - func test_setQueryItemAsURI_arrayOfDates() throws { - var request = testRequest - try converter.setQueryItemAsURI( - in: &request, - style: nil, - explode: nil, - name: "search", - value: [testDate, testDate] - ) - XCTAssertEqual(request.query, "search=2023-01-18T10%3A04%3A11Z&search=2023-01-18T10%3A04%3A11Z") - } - - // | client | set | request body | string | optional | setOptionalRequestBodyAsString | - func test_setOptionalRequestBodyAsString_string() throws { - var headerFields: [HeaderField] = [] - let body = try converter.setOptionalRequestBodyAsString( - testString, - headerFields: &headerFields, - contentType: "text/plain" - ) - XCTAssertEqual(body, testStringData) - XCTAssertEqual( - headerFields, - [ - .init(name: "content-type", value: "text/plain") - ] - ) - } - - // | client | set | request body | string | required | setRequiredRequestBodyAsString | - func test_setRequiredRequestBodyAsString_string() throws { - var headerFields: [HeaderField] = [] - let body = try converter.setRequiredRequestBodyAsString( - testString, - headerFields: &headerFields, - contentType: "text/plain" - ) - XCTAssertEqual(body, testStringData) - XCTAssertEqual( - headerFields, - [ - .init(name: "content-type", value: "text/plain") - ] - ) - } - - func test_setOptionalRequestBodyAsString_date() throws { - var headerFields: [HeaderField] = [] - let body = try converter.setOptionalRequestBodyAsString( - testDate, - headerFields: &headerFields, - contentType: "text/plain" - ) - XCTAssertEqual(body, testDateStringData) - XCTAssertEqual( - headerFields, - [ - .init(name: "content-type", value: "text/plain") - ] - ) - } - - func test_setRequiredRequestBodyAsString_date() throws { - var headerFields: [HeaderField] = [] - let body = try converter.setRequiredRequestBodyAsString( - testDate, - headerFields: &headerFields, - contentType: "text/plain" - ) - XCTAssertEqual(body, testDateStringData) - XCTAssertEqual( - headerFields, - [ - .init(name: "content-type", value: "text/plain") - ] - ) - } - - // | client | set | request body | JSON | optional | setOptionalRequestBodyAsJSON | - func test_setOptionalRequestBodyAsJSON_codable() throws { - var headerFields: [HeaderField] = [] - let body = try converter.setOptionalRequestBodyAsJSON( - testStruct, - headerFields: &headerFields, - contentType: "application/json" - ) - XCTAssertEqual(body, testStructPrettyData) - XCTAssertEqual( - headerFields, - [ - .init(name: "content-type", value: "application/json") - ] - ) - } - - func test_setOptionalRequestBodyAsJSON_codable_string() throws { - var headerFields: [HeaderField] = [] - let body = try converter.setOptionalRequestBodyAsJSON( - testString, - headerFields: &headerFields, - contentType: "application/json" - ) - XCTAssertEqual(body, testQuotedStringData) - XCTAssertEqual( - headerFields, - [ - .init(name: "content-type", value: "application/json") - ] - ) - } - - // | client | set | request body | JSON | required | setRequiredRequestBodyAsJSON | - func test_setRequiredRequestBodyAsJSON_codable() throws { - var headerFields: [HeaderField] = [] - let body = try converter.setRequiredRequestBodyAsJSON( - testStruct, - headerFields: &headerFields, - contentType: "application/json" - ) - XCTAssertEqual(body, testStructPrettyData) - XCTAssertEqual( - headerFields, - [ - .init(name: "content-type", value: "application/json") - ] - ) - } - - // | client | set | request body | binary | optional | setOptionalRequestBodyAsBinary | - func test_setOptionalRequestBodyAsBinary_data() throws { - var headerFields: [HeaderField] = [] - let body = try converter.setOptionalRequestBodyAsBinary( - testStringData, - headerFields: &headerFields, - contentType: "application/octet-stream" - ) - XCTAssertEqual(body, testStringData) - XCTAssertEqual( - headerFields, - [ - .init(name: "content-type", value: "application/octet-stream") - ] - ) - } - - // | client | set | request body | binary | required | setRequiredRequestBodyAsBinary | - func test_setRequiredRequestBodyAsBinary_data() throws { - var headerFields: [HeaderField] = [] - let body = try converter.setRequiredRequestBodyAsBinary( - testStringData, - headerFields: &headerFields, - contentType: "application/octet-stream" - ) - XCTAssertEqual(body, testStringData) - XCTAssertEqual( - headerFields, - [ - .init(name: "content-type", value: "application/octet-stream") - ] - ) - } - - // | client | get | response body | string | required | getResponseBodyAsString | - func test_getResponseBodyAsString_stringConvertible() throws { - let value: String = try converter.getResponseBodyAsString( - String.self, - from: testStringData, - transforming: { $0 } - ) - XCTAssertEqual(value, testString) - } - - // | client | get | response body | string | required | getResponseBodyAsString | - func test_getResponseBodyAsString_date() throws { - let value: Date = try converter.getResponseBodyAsString( - Date.self, - from: testDateStringData, - transforming: { $0 } - ) - XCTAssertEqual(value, testDate) - } - - // | client | get | response body | JSON | required | getResponseBodyAsJSON | - func test_getResponseBodyAsJSON_codable() throws { - let value: TestPet = try converter.getResponseBodyAsJSON( - TestPet.self, - from: testStructData, - transforming: { $0 } - ) - XCTAssertEqual(value, testStruct) - } - - // | client | get | response body | binary | required | getResponseBodyAsBinary | - func test_getResponseBodyAsBinary_data() throws { - let value: Data = try converter.getResponseBodyAsBinary( - Data.self, - from: testStringData, - transforming: { $0 } - ) - XCTAssertEqual(value, testStringData) - } -} +// // | client | set | request query | URI | both | setQueryItemAsURI | +// func test_setQueryItemAsURI_string() throws { +// var request = testRequest +// try converter.setQueryItemAsURI( +// in: &request, +// style: nil, +// explode: nil, +// name: "search", +// value: "foo" +// ) +// XCTAssertEqual(request.query, "search=foo") +// } +// +// func test_setQueryItemAsURI_stringConvertible_needsEncoding() throws { +// var request = testRequest +// try converter.setQueryItemAsURI( +// in: &request, +// style: nil, +// explode: nil, +// name: "search", +// value: "h%llo" +// ) +// XCTAssertEqual(request.query, "search=h%25llo") +// } +// +// func test_setQueryItemAsURI_arrayOfStrings() throws { +// var request = testRequest +// try converter.setQueryItemAsURI( +// in: &request, +// style: nil, +// explode: nil, +// name: "search", +// value: ["foo", "bar"] +// ) +// XCTAssertEqual(request.query, "search=foo&search=bar") +// } +// +// func test_setQueryItemAsURI_arrayOfStrings_unexploded() throws { +// var request = testRequest +// try converter.setQueryItemAsURI( +// in: &request, +// style: nil, +// explode: false, +// name: "search", +// value: ["foo", "bar"] +// ) +// XCTAssertEqual(request.query, "search=foo,bar") +// } +// +// func test_setQueryItemAsURI_date() throws { +// var request = testRequest +// try converter.setQueryItemAsURI( +// in: &request, +// style: nil, +// explode: nil, +// name: "search", +// value: testDate +// ) +// XCTAssertEqual(request.query, "search=2023-01-18T10%3A04%3A11Z") +// } +// +// func test_setQueryItemAsURI_arrayOfDates() throws { +// var request = testRequest +// try converter.setQueryItemAsURI( +// in: &request, +// style: nil, +// explode: nil, +// name: "search", +// value: [testDate, testDate] +// ) +// XCTAssertEqual(request.query, "search=2023-01-18T10%3A04%3A11Z&search=2023-01-18T10%3A04%3A11Z") +// } +// +// // | client | set | request body | string | optional | setOptionalRequestBodyAsString | +// func test_setOptionalRequestBodyAsString_string() throws { +// var headerFields: [HeaderField] = [] +// let body = try converter.setOptionalRequestBodyAsString( +// testString, +// headerFields: &headerFields, +// contentType: "text/plain" +// ) +// XCTAssertEqual(body, testStringData) +// XCTAssertEqual( +// headerFields, +// [ +// .init(name: "content-type", value: "text/plain") +// ] +// ) +// } +// +// // | client | set | request body | string | required | setRequiredRequestBodyAsString | +// func test_setRequiredRequestBodyAsString_string() throws { +// var headerFields: [HeaderField] = [] +// let body = try converter.setRequiredRequestBodyAsString( +// testString, +// headerFields: &headerFields, +// contentType: "text/plain" +// ) +// XCTAssertEqual(body, testStringData) +// XCTAssertEqual( +// headerFields, +// [ +// .init(name: "content-type", value: "text/plain") +// ] +// ) +// } +// +// func test_setOptionalRequestBodyAsString_date() throws { +// var headerFields: [HeaderField] = [] +// let body = try converter.setOptionalRequestBodyAsString( +// testDate, +// headerFields: &headerFields, +// contentType: "text/plain" +// ) +// XCTAssertEqual(body, testDateStringData) +// XCTAssertEqual( +// headerFields, +// [ +// .init(name: "content-type", value: "text/plain") +// ] +// ) +// } +// +// func test_setRequiredRequestBodyAsString_date() throws { +// var headerFields: [HeaderField] = [] +// let body = try converter.setRequiredRequestBodyAsString( +// testDate, +// headerFields: &headerFields, +// contentType: "text/plain" +// ) +// XCTAssertEqual(body, testDateStringData) +// XCTAssertEqual( +// headerFields, +// [ +// .init(name: "content-type", value: "text/plain") +// ] +// ) +// } +// +// // | client | set | request body | JSON | optional | setOptionalRequestBodyAsJSON | +// func test_setOptionalRequestBodyAsJSON_codable() throws { +// var headerFields: [HeaderField] = [] +// let body = try converter.setOptionalRequestBodyAsJSON( +// testStruct, +// headerFields: &headerFields, +// contentType: "application/json" +// ) +// XCTAssertEqual(body, testStructPrettyData) +// XCTAssertEqual( +// headerFields, +// [ +// .init(name: "content-type", value: "application/json") +// ] +// ) +// } +// +// func test_setOptionalRequestBodyAsJSON_codable_string() throws { +// var headerFields: [HeaderField] = [] +// let body = try converter.setOptionalRequestBodyAsJSON( +// testString, +// headerFields: &headerFields, +// contentType: "application/json" +// ) +// XCTAssertEqual(body, testQuotedStringData) +// XCTAssertEqual( +// headerFields, +// [ +// .init(name: "content-type", value: "application/json") +// ] +// ) +// } +// +// // | client | set | request body | JSON | required | setRequiredRequestBodyAsJSON | +// func test_setRequiredRequestBodyAsJSON_codable() throws { +// var headerFields: [HeaderField] = [] +// let body = try converter.setRequiredRequestBodyAsJSON( +// testStruct, +// headerFields: &headerFields, +// contentType: "application/json" +// ) +// XCTAssertEqual(body, testStructPrettyData) +// XCTAssertEqual( +// headerFields, +// [ +// .init(name: "content-type", value: "application/json") +// ] +// ) +// } +// +// // | client | set | request body | binary | optional | setOptionalRequestBodyAsBinary | +// func test_setOptionalRequestBodyAsBinary_data() throws { +// var headerFields: [HeaderField] = [] +// let body = try converter.setOptionalRequestBodyAsBinary( +// testStringData, +// headerFields: &headerFields, +// contentType: "application/octet-stream" +// ) +// XCTAssertEqual(body, testStringData) +// XCTAssertEqual( +// headerFields, +// [ +// .init(name: "content-type", value: "application/octet-stream") +// ] +// ) +// } +// +// // | client | set | request body | binary | required | setRequiredRequestBodyAsBinary | +// func test_setRequiredRequestBodyAsBinary_data() throws { +// var headerFields: [HeaderField] = [] +// let body = try converter.setRequiredRequestBodyAsBinary( +// testStringData, +// headerFields: &headerFields, +// contentType: "application/octet-stream" +// ) +// XCTAssertEqual(body, testStringData) +// XCTAssertEqual( +// headerFields, +// [ +// .init(name: "content-type", value: "application/octet-stream") +// ] +// ) +// } +// +// // | client | get | response body | string | required | getResponseBodyAsString | +// func test_getResponseBodyAsString_stringConvertible() throws { +// let value: String = try converter.getResponseBodyAsString( +// String.self, +// from: testStringData, +// transforming: { $0 } +// ) +// XCTAssertEqual(value, testString) +// } +// +// // | client | get | response body | string | required | getResponseBodyAsString | +// func test_getResponseBodyAsString_date() throws { +// let value: Date = try converter.getResponseBodyAsString( +// Date.self, +// from: testDateStringData, +// transforming: { $0 } +// ) +// XCTAssertEqual(value, testDate) +// } +// +// // | client | get | response body | JSON | required | getResponseBodyAsJSON | +// func test_getResponseBodyAsJSON_codable() throws { +// let value: TestPet = try converter.getResponseBodyAsJSON( +// TestPet.self, +// from: testStructData, +// transforming: { $0 } +// ) +// XCTAssertEqual(value, testStruct) +// } +// +// // | client | get | response body | binary | required | getResponseBodyAsBinary | +// func test_getResponseBodyAsBinary_data() throws { +// let value: Data = try converter.getResponseBodyAsBinary( +// Data.self, +// from: testStringData, +// transforming: { $0 } +// ) +// XCTAssertEqual(value, testStringData) +// } +//} diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift index 23c3d1bc..7ef9c100 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift @@ -1,272 +1,272 @@ -//===----------------------------------------------------------------------===// +////===----------------------------------------------------------------------===// +//// +//// 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 XCTest +//@_spi(Generated)@testable import OpenAPIRuntime // -// This source file is part of the SwiftOpenAPIGenerator open source project +//final class Test_CommonConverterExtensions: Test_Runtime { // -// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors -// Licensed under Apache License v2.0 +// // MARK: Miscs // -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// func testContentTypeMatching() throws { +// let cases: [(received: String, expected: String, isMatch: Bool)] = [ +// ("application/json", "application/json", true), +// ("APPLICATION/JSON", "application/json", true), +// ("application/json", "application/*", true), +// ("application/json", "*/*", true), +// ("application/json", "text/*", false), +// ("application/json", "application/xml", false), +// ("application/json", "text/plain", false), // -// SPDX-License-Identifier: Apache-2.0 +// ("text/plain; charset=UTF-8", "text/plain", true), +// ("TEXT/PLAIN; CHARSET=UTF-8", "text/plain", true), +// ("text/plain; charset=UTF-8", "text/*", true), +// ("text/plain; charset=UTF-8", "*/*", true), +// ("text/plain; charset=UTF-8", "application/*", false), +// ("text/plain; charset=UTF-8", "text/html", false), +// ] +// for testCase in cases { +// XCTAssertEqual( +// try converter.isMatchingContentType( +// received: .init(testCase.received), +// expectedRaw: testCase.expected +// ), +// testCase.isMatch, +// "Wrong result for (\(testCase.received), \(testCase.expected), \(testCase.isMatch))" +// ) +// } +// } // -//===----------------------------------------------------------------------===// -import XCTest -@_spi(Generated)@testable import OpenAPIRuntime - -final class Test_CommonConverterExtensions: Test_Runtime { - - // MARK: Miscs - - func testContentTypeMatching() throws { - let cases: [(received: String, expected: String, isMatch: Bool)] = [ - ("application/json", "application/json", true), - ("APPLICATION/JSON", "application/json", true), - ("application/json", "application/*", true), - ("application/json", "*/*", true), - ("application/json", "text/*", false), - ("application/json", "application/xml", false), - ("application/json", "text/plain", false), - - ("text/plain; charset=UTF-8", "text/plain", true), - ("TEXT/PLAIN; CHARSET=UTF-8", "text/plain", true), - ("text/plain; charset=UTF-8", "text/*", true), - ("text/plain; charset=UTF-8", "*/*", true), - ("text/plain; charset=UTF-8", "application/*", false), - ("text/plain; charset=UTF-8", "text/html", false), - ] - for testCase in cases { - XCTAssertEqual( - try converter.isMatchingContentType( - received: .init(testCase.received), - expectedRaw: testCase.expected - ), - testCase.isMatch, - "Wrong result for (\(testCase.received), \(testCase.expected), \(testCase.isMatch))" - ) - } - } - - // MARK: Converter helper methods - - // | common | set | header field | URI | both | setHeaderFieldAsURI | - func test_setHeaderFieldAsURI_string() throws { - var headerFields: [HeaderField] = [] - try converter.setHeaderFieldAsURI( - in: &headerFields, - name: "foo", - value: "bar" - ) - XCTAssertEqual( - headerFields, - [ - .init(name: "foo", value: "bar") - ] - ) - } - - func test_setHeaderFieldAsURI_arrayOfStrings() throws { - var headerFields: [HeaderField] = [] - try converter.setHeaderFieldAsURI( - in: &headerFields, - name: "foo", - value: ["bar", "baz"] as [String] - ) - XCTAssertEqual( - headerFields, - [ - .init(name: "foo", value: "bar,baz") - ] - ) - } - - func test_setHeaderFieldAsURI_date() throws { - var headerFields: [HeaderField] = [] - try converter.setHeaderFieldAsURI( - in: &headerFields, - name: "foo", - value: testDate - ) - XCTAssertEqual( - headerFields, - [ - .init(name: "foo", value: testDateEscapedString) - ] - ) - } - - func test_setHeaderFieldAsURI_arrayOfDates() throws { - var headerFields: [HeaderField] = [] - try converter.setHeaderFieldAsURI( - in: &headerFields, - name: "foo", - value: [testDate, testDate] - ) - XCTAssertEqual( - headerFields, - [ - .init(name: "foo", value: "\(testDateEscapedString),\(testDateEscapedString)") - ] - ) - } - - func test_setHeaderFieldAsURI_struct() throws { - var headerFields: [HeaderField] = [] - try converter.setHeaderFieldAsURI( - in: &headerFields, - name: "foo", - value: testStruct - ) - XCTAssertEqual( - headerFields, - [ - .init(name: "foo", value: "name,Fluffz") - ] - ) - } - - // | common | set | header field | JSON | both | setHeaderFieldAsJSON | - func test_setHeaderFieldAsJSON_codable() throws { - var headerFields: [HeaderField] = [] - try converter.setHeaderFieldAsJSON( - in: &headerFields, - name: "foo", - value: testStruct - ) - XCTAssertEqual( - headerFields, - [ - .init(name: "foo", value: testStructString) - ] - ) - } - - func test_setHeaderFieldAsJSON_codable_string() throws { - var headerFields: [HeaderField] = [] - try converter.setHeaderFieldAsJSON( - in: &headerFields, - name: "foo", - value: "hello" - ) - XCTAssertEqual( - headerFields, - [ - .init(name: "foo", value: "\"hello\"") - ] - ) - } - - // | common | get | header field | URI | optional | getOptionalHeaderFieldAsURI | - func test_getOptionalHeaderFieldAsURI_string() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: "bar") - ] - let value: String? = try converter.getOptionalHeaderFieldAsURI( - in: headerFields, - name: "foo", - as: String.self - ) - XCTAssertEqual(value, "bar") - } - - // | common | get | header field | URI | required | getRequiredHeaderFieldAsURI | - func test_getRequiredHeaderFieldAsURI_stringConvertible() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: "bar") - ] - let value: String = try converter.getRequiredHeaderFieldAsURI( - in: headerFields, - name: "foo", - as: String.self - ) - XCTAssertEqual(value, "bar") - } - - func test_getOptionalHeaderFieldAsURI_arrayOfStrings_splitHeaders() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: "bar"), - .init(name: "foo", value: "baz"), - ] - let value: [String]? = try converter.getOptionalHeaderFieldAsURI( - in: headerFields, - name: "foo", - as: [String].self - ) - XCTAssertEqual(value, ["bar", "baz"]) - } - - func test_getOptionalHeaderFieldAsURI_arrayOfStrings_singleHeader() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: "bar,baz") - ] - let value: [String]? = try converter.getOptionalHeaderFieldAsURI( - in: headerFields, - name: "foo", - as: [String].self - ) - XCTAssertEqual(value, ["bar", "baz"]) - } - - func test_getOptionalHeaderFieldAsURI_date() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: testDateEscapedString) - ] - let value: Date? = try converter.getOptionalHeaderFieldAsURI( - in: headerFields, - name: "foo", - as: Date.self - ) - XCTAssertEqual(value, testDate) - } - - func test_getRequiredHeaderFieldAsURI_arrayOfDates() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: testDateString), // escaped - .init(name: "foo", value: testDateEscapedString), // unescaped - ] - let value: [Date] = try converter.getRequiredHeaderFieldAsURI( - in: headerFields, - name: "foo", - as: [Date].self - ) - XCTAssertEqual(value, [testDate, testDate]) - } - - func test_getOptionalHeaderFieldAsURI_struct() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: "name,Sprinkles") - ] - let value: TestPet? = try converter.getOptionalHeaderFieldAsURI( - in: headerFields, - name: "foo", - as: TestPet.self - ) - XCTAssertEqual(value, .init(name: "Sprinkles")) - } - - // | common | get | header field | JSON | optional | getOptionalHeaderFieldAsJSON | - func test_getOptionalHeaderFieldAsJSON_codable() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: testStructString) - ] - let value: TestPet? = try converter.getOptionalHeaderFieldAsJSON( - in: headerFields, - name: "foo", - as: TestPet.self - ) - XCTAssertEqual(value, testStruct) - } - - // | common | get | header field | JSON | required | getRequiredHeaderFieldAsJSON | - func test_getRequiredHeaderFieldAsJSON_codable() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: testStructString) - ] - let value: TestPet = try converter.getRequiredHeaderFieldAsJSON( - in: headerFields, - name: "foo", - as: TestPet.self - ) - XCTAssertEqual(value, testStruct) - } -} +// // MARK: Converter helper methods +// +// // | common | set | header field | URI | both | setHeaderFieldAsURI | +// func test_setHeaderFieldAsURI_string() throws { +// var headerFields: [HeaderField] = [] +// try converter.setHeaderFieldAsURI( +// in: &headerFields, +// name: "foo", +// value: "bar" +// ) +// XCTAssertEqual( +// headerFields, +// [ +// .init(name: "foo", value: "bar") +// ] +// ) +// } +// +// func test_setHeaderFieldAsURI_arrayOfStrings() throws { +// var headerFields: [HeaderField] = [] +// try converter.setHeaderFieldAsURI( +// in: &headerFields, +// name: "foo", +// value: ["bar", "baz"] as [String] +// ) +// XCTAssertEqual( +// headerFields, +// [ +// .init(name: "foo", value: "bar,baz") +// ] +// ) +// } +// +// func test_setHeaderFieldAsURI_date() throws { +// var headerFields: [HeaderField] = [] +// try converter.setHeaderFieldAsURI( +// in: &headerFields, +// name: "foo", +// value: testDate +// ) +// XCTAssertEqual( +// headerFields, +// [ +// .init(name: "foo", value: testDateEscapedString) +// ] +// ) +// } +// +// func test_setHeaderFieldAsURI_arrayOfDates() throws { +// var headerFields: [HeaderField] = [] +// try converter.setHeaderFieldAsURI( +// in: &headerFields, +// name: "foo", +// value: [testDate, testDate] +// ) +// XCTAssertEqual( +// headerFields, +// [ +// .init(name: "foo", value: "\(testDateEscapedString),\(testDateEscapedString)") +// ] +// ) +// } +// +// func test_setHeaderFieldAsURI_struct() throws { +// var headerFields: [HeaderField] = [] +// try converter.setHeaderFieldAsURI( +// in: &headerFields, +// name: "foo", +// value: testStruct +// ) +// XCTAssertEqual( +// headerFields, +// [ +// .init(name: "foo", value: "name,Fluffz") +// ] +// ) +// } +// +// // | common | set | header field | JSON | both | setHeaderFieldAsJSON | +// func test_setHeaderFieldAsJSON_codable() throws { +// var headerFields: [HeaderField] = [] +// try converter.setHeaderFieldAsJSON( +// in: &headerFields, +// name: "foo", +// value: testStruct +// ) +// XCTAssertEqual( +// headerFields, +// [ +// .init(name: "foo", value: testStructString) +// ] +// ) +// } +// +// func test_setHeaderFieldAsJSON_codable_string() throws { +// var headerFields: [HeaderField] = [] +// try converter.setHeaderFieldAsJSON( +// in: &headerFields, +// name: "foo", +// value: "hello" +// ) +// XCTAssertEqual( +// headerFields, +// [ +// .init(name: "foo", value: "\"hello\"") +// ] +// ) +// } +// +// // | common | get | header field | URI | optional | getOptionalHeaderFieldAsURI | +// func test_getOptionalHeaderFieldAsURI_string() throws { +// let headerFields: [HeaderField] = [ +// .init(name: "foo", value: "bar") +// ] +// let value: String? = try converter.getOptionalHeaderFieldAsURI( +// in: headerFields, +// name: "foo", +// as: String.self +// ) +// XCTAssertEqual(value, "bar") +// } +// +// // | common | get | header field | URI | required | getRequiredHeaderFieldAsURI | +// func test_getRequiredHeaderFieldAsURI_stringConvertible() throws { +// let headerFields: [HeaderField] = [ +// .init(name: "foo", value: "bar") +// ] +// let value: String = try converter.getRequiredHeaderFieldAsURI( +// in: headerFields, +// name: "foo", +// as: String.self +// ) +// XCTAssertEqual(value, "bar") +// } +// +// func test_getOptionalHeaderFieldAsURI_arrayOfStrings_splitHeaders() throws { +// let headerFields: [HeaderField] = [ +// .init(name: "foo", value: "bar"), +// .init(name: "foo", value: "baz"), +// ] +// let value: [String]? = try converter.getOptionalHeaderFieldAsURI( +// in: headerFields, +// name: "foo", +// as: [String].self +// ) +// XCTAssertEqual(value, ["bar", "baz"]) +// } +// +// func test_getOptionalHeaderFieldAsURI_arrayOfStrings_singleHeader() throws { +// let headerFields: [HeaderField] = [ +// .init(name: "foo", value: "bar,baz") +// ] +// let value: [String]? = try converter.getOptionalHeaderFieldAsURI( +// in: headerFields, +// name: "foo", +// as: [String].self +// ) +// XCTAssertEqual(value, ["bar", "baz"]) +// } +// +// func test_getOptionalHeaderFieldAsURI_date() throws { +// let headerFields: [HeaderField] = [ +// .init(name: "foo", value: testDateEscapedString) +// ] +// let value: Date? = try converter.getOptionalHeaderFieldAsURI( +// in: headerFields, +// name: "foo", +// as: Date.self +// ) +// XCTAssertEqual(value, testDate) +// } +// +// func test_getRequiredHeaderFieldAsURI_arrayOfDates() throws { +// let headerFields: [HeaderField] = [ +// .init(name: "foo", value: testDateString), // escaped +// .init(name: "foo", value: testDateEscapedString), // unescaped +// ] +// let value: [Date] = try converter.getRequiredHeaderFieldAsURI( +// in: headerFields, +// name: "foo", +// as: [Date].self +// ) +// XCTAssertEqual(value, [testDate, testDate]) +// } +// +// func test_getOptionalHeaderFieldAsURI_struct() throws { +// let headerFields: [HeaderField] = [ +// .init(name: "foo", value: "name,Sprinkles") +// ] +// let value: TestPet? = try converter.getOptionalHeaderFieldAsURI( +// in: headerFields, +// name: "foo", +// as: TestPet.self +// ) +// XCTAssertEqual(value, .init(name: "Sprinkles")) +// } +// +// // | common | get | header field | JSON | optional | getOptionalHeaderFieldAsJSON | +// func test_getOptionalHeaderFieldAsJSON_codable() throws { +// let headerFields: [HeaderField] = [ +// .init(name: "foo", value: testStructString) +// ] +// let value: TestPet? = try converter.getOptionalHeaderFieldAsJSON( +// in: headerFields, +// name: "foo", +// as: TestPet.self +// ) +// XCTAssertEqual(value, testStruct) +// } +// +// // | common | get | header field | JSON | required | getRequiredHeaderFieldAsJSON | +// func test_getRequiredHeaderFieldAsJSON_codable() throws { +// let headerFields: [HeaderField] = [ +// .init(name: "foo", value: testStructString) +// ] +// let value: TestPet = try converter.getRequiredHeaderFieldAsJSON( +// in: headerFields, +// name: "foo", +// as: TestPet.self +// ) +// XCTAssertEqual(value, testStruct) +// } +//} diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift index 713a2232..668eb6e7 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift @@ -1,384 +1,384 @@ -//===----------------------------------------------------------------------===// +////===----------------------------------------------------------------------===// +//// +//// 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 XCTest +//@_spi(Generated)@testable import OpenAPIRuntime // -// This source file is part of the SwiftOpenAPIGenerator open source project +//final class Test_ServerConverterExtensions: Test_Runtime { // -// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors -// Licensed under Apache License v2.0 +// func testExtractAccept() throws { +// let headerFields: [HeaderField] = [ +// .init(name: "accept", value: "application/json, */*; q=0.8") +// ] +// let accept: [AcceptHeaderContentType] = try converter.extractAcceptHeaderIfPresent( +// in: headerFields +// ) +// XCTAssertEqual( +// accept, +// [ +// .init(contentType: .json, quality: 1.0), +// .init(contentType: .other("*/*"), quality: 0.8), +// ] +// ) +// } // -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// // MARK: Miscs // -// SPDX-License-Identifier: Apache-2.0 +// func testValidateAccept() throws { +// let emptyHeaders: [HeaderField] = [] +// let wildcard: [HeaderField] = [ +// .init(name: "accept", value: "*/*") +// ] +// let partialWildcard: [HeaderField] = [ +// .init(name: "accept", value: "text/*") +// ] +// let short: [HeaderField] = [ +// .init(name: "accept", value: "text/plain") +// ] +// let long: [HeaderField] = [ +// .init( +// name: "accept", +// value: "text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8" +// ) +// ] +// let multiple: [HeaderField] = [ +// .init(name: "accept", value: "text/plain"), +// .init(name: "accept", value: "application/json"), +// ] +// let cases: [([HeaderField], String, Bool)] = [ +// // No Accept header, any string validates successfully +// (emptyHeaders, "foobar", true), // -//===----------------------------------------------------------------------===// -import XCTest -@_spi(Generated)@testable import OpenAPIRuntime - -final class Test_ServerConverterExtensions: Test_Runtime { - - func testExtractAccept() throws { - let headerFields: [HeaderField] = [ - .init(name: "accept", value: "application/json, */*; q=0.8") - ] - let accept: [AcceptHeaderContentType] = try converter.extractAcceptHeaderIfPresent( - in: headerFields - ) - XCTAssertEqual( - accept, - [ - .init(contentType: .json, quality: 1.0), - .init(contentType: .other("*/*"), quality: 0.8), - ] - ) - } - - // MARK: Miscs - - func testValidateAccept() throws { - let emptyHeaders: [HeaderField] = [] - let wildcard: [HeaderField] = [ - .init(name: "accept", value: "*/*") - ] - let partialWildcard: [HeaderField] = [ - .init(name: "accept", value: "text/*") - ] - let short: [HeaderField] = [ - .init(name: "accept", value: "text/plain") - ] - let long: [HeaderField] = [ - .init( - name: "accept", - value: "text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8" - ) - ] - let multiple: [HeaderField] = [ - .init(name: "accept", value: "text/plain"), - .init(name: "accept", value: "application/json"), - ] - let cases: [([HeaderField], String, Bool)] = [ - // No Accept header, any string validates successfully - (emptyHeaders, "foobar", true), - - // Accept: */*, any string validates successfully - (wildcard, "foobar", true), - - // Accept: text/*, so text/plain succeeds, application/json fails - (partialWildcard, "text/plain", true), - (partialWildcard, "application/json", false), - - // Accept: text/plain, text/plain succeeds, application/json fails - (short, "text/plain", true), - (short, "application/json", false), - - // A bunch of acceptable content types - (long, "text/html", true), - (long, "application/xhtml+xml", true), - (long, "application/xml", true), - (long, "image/webp", true), - (long, "application/json", true), - - // Multiple values - (multiple, "text/plain", true), - (multiple, "application/json", true), - (multiple, "application/xml", false), - ] - for (headers, contentType, success) in cases { - if success { - XCTAssertNoThrow( - try converter.validateAcceptIfPresent( - contentType, - in: headers - ), - "Unexpected error when validating string: \(contentType) against headers: \(headers)" - ) - } else { - let acceptHeader = - headers - .values(name: "accept") - .joined(separator: ", ") - XCTAssertThrowsError( - try converter.validateAcceptIfPresent( - contentType, - in: headers - ), - "Expected to throw error when validating string: \(contentType) against headers: \(headers)", - { error in - guard - let err = error as? RuntimeError, - case .unexpectedAcceptHeader(let string) = err - else { - XCTFail("Threw an unexpected error: \(error)") - return - } - XCTAssertEqual(string, acceptHeader) - } - ) - } - } - } - - // MARK: Converter helper methods - - // | server | get | request path | URI | required | getPathParameterAsURI | - func test_getPathParameterAsURI_various() throws { - let path: [String: String] = [ - "foo": "bar", - "number": "1", - "habitats": "land,air", - ] - do { - let value = try converter.getPathParameterAsURI( - in: path, - name: "foo", - as: String.self - ) - XCTAssertEqual(value, "bar") - } - do { - let value = try converter.getPathParameterAsURI( - in: path, - name: "number", - as: Int.self - ) - XCTAssertEqual(value, 1) - } - do { - let value = try converter.getPathParameterAsURI( - in: path, - name: "habitats", - as: [TestHabitat].self - ) - XCTAssertEqual(value, [.land, .air]) - } - } - - // | server | get | request query | URI | optional | getOptionalQueryItemAsURI | - func test_getOptionalQueryItemAsURI_string() throws { - let value: String? = try converter.getOptionalQueryItemAsURI( - in: "search=foo", - style: nil, - explode: nil, - name: "search", - as: String.self - ) - XCTAssertEqual(value, "foo") - } - - // | server | get | request query | URI | required | getRequiredQueryItemAsURI | - func test_getRequiredQueryItemAsURI_string() throws { - let value: String = try converter.getRequiredQueryItemAsURI( - in: "search=foo", - style: nil, - explode: nil, - name: "search", - as: String.self - ) - XCTAssertEqual(value, "foo") - } - - func test_getOptionalQueryItemAsURI_arrayOfStrings() throws { - let query = "search=foo&search=bar" - let value: [String]? = try converter.getOptionalQueryItemAsURI( - in: query, - style: nil, - explode: nil, - name: "search", - as: [String].self - ) - XCTAssertEqual(value, ["foo", "bar"]) - } - - func test_getRequiredQueryItemAsURI_arrayOfStrings() throws { - let query = "search=foo&search=bar" - let value: [String] = try converter.getRequiredQueryItemAsURI( - in: query, - style: nil, - explode: nil, - name: "search", - as: [String].self - ) - XCTAssertEqual(value, ["foo", "bar"]) - } - - func test_getRequiredQueryItemAsURI_arrayOfStrings_unexploded() throws { - let query = "search=foo,bar" - let value: [String] = try converter.getRequiredQueryItemAsURI( - in: query, - style: nil, - explode: false, - name: "search", - as: [String].self - ) - XCTAssertEqual(value, ["foo", "bar"]) - } - - func test_getOptionalQueryItemAsURI_date() throws { - let query = "search=\(testDateEscapedString)" - let value: Date? = try converter.getOptionalQueryItemAsURI( - in: query, - style: nil, - explode: nil, - name: "search", - as: Date.self - ) - XCTAssertEqual(value, testDate) - } - - func test_getRequiredQueryItemAsURI_arrayOfDates() throws { - let query = "search=\(testDateEscapedString)&search=\(testDateEscapedString)" - let value: [Date] = try converter.getRequiredQueryItemAsURI( - in: query, - style: nil, - explode: nil, - name: "search", - as: [Date].self - ) - XCTAssertEqual(value, [testDate, testDate]) - } - - // | server | get | request body | string | optional | getOptionalRequestBodyAsString | - func test_getOptionalRequestBodyAsText_string() throws { - let body: String? = try converter.getOptionalRequestBodyAsString( - String.self, - from: testStringData, - transforming: { $0 } - ) - XCTAssertEqual(body, testString) - } - - // | server | get | request body | string | required | getRequiredRequestBodyAsString | - func test_getRequiredRequestBodyAsText_stringConvertible() throws { - let body: String = try converter.getRequiredRequestBodyAsString( - String.self, - from: testStringData, - transforming: { $0 } - ) - XCTAssertEqual(body, testString) - } - - func test_getRequiredRequestBodyAsText_date() throws { - let body: Date = try converter.getRequiredRequestBodyAsString( - Date.self, - from: testDateStringData, - transforming: { $0 } - ) - XCTAssertEqual(body, testDate) - } - - // | server | get | request body | JSON | optional | getOptionalRequestBodyAsJSON | - func test_getOptionalRequestBodyAsJSON_codable() throws { - let body: TestPet? = try converter.getOptionalRequestBodyAsJSON( - TestPet.self, - from: testStructData, - transforming: { $0 } - ) - XCTAssertEqual(body, testStruct) - } - - func test_getOptionalRequestBodyAsJSON_codable_string() throws { - let body: String? = try converter.getOptionalRequestBodyAsJSON( - String.self, - from: testQuotedStringData, - transforming: { $0 } - ) - XCTAssertEqual(body, testString) - } - - // | server | get | request body | JSON | required | getRequiredRequestBodyAsJSON | - func test_getRequiredRequestBodyAsJSON_codable() throws { - let body: TestPet = try converter.getRequiredRequestBodyAsJSON( - TestPet.self, - from: testStructData, - transforming: { $0 } - ) - XCTAssertEqual(body, testStruct) - } - - // | server | get | request body | binary | optional | getOptionalRequestBodyAsBinary | - func test_getOptionalRequestBodyAsBinary_data() throws { - let body: Data? = try converter.getOptionalRequestBodyAsBinary( - Data.self, - from: testStringData, - transforming: { $0 } - ) - XCTAssertEqual(body, testStringData) - } - - // | server | get | request body | binary | required | getRequiredRequestBodyAsBinary | - func test_getRequiredRequestBodyAsBinary_data() throws { - let body: Data = try converter.getRequiredRequestBodyAsBinary( - Data.self, - from: testStringData, - transforming: { $0 } - ) - XCTAssertEqual(body, testStringData) - } - - // | server | set | response body | string | required | setResponseBodyAsString | - func test_setResponseBodyAsText_stringConvertible() throws { - var headers: [HeaderField] = [] - let data = try converter.setResponseBodyAsString( - testString, - headerFields: &headers, - contentType: "text/plain" - ) - XCTAssertEqual(data, testStringData) - XCTAssertEqual( - headers, - [ - .init(name: "content-type", value: "text/plain") - ] - ) - } - - // | server | set | response body | string | required | setResponseBodyAsString | - func test_setResponseBodyAsText_date() throws { - var headers: [HeaderField] = [] - let data = try converter.setResponseBodyAsString( - testDate, - headerFields: &headers, - contentType: "text/plain" - ) - XCTAssertEqual(data, testDateStringData) - XCTAssertEqual( - headers, - [ - .init(name: "content-type", value: "text/plain") - ] - ) - } - - // | server | set | response body | JSON | required | setResponseBodyAsJSON | - func test_setResponseBodyAsJSON_codable() throws { - var headers: [HeaderField] = [] - let data = try converter.setResponseBodyAsJSON( - testStruct, - headerFields: &headers, - contentType: "application/json" - ) - XCTAssertEqual(data, testStructPrettyData) - XCTAssertEqual( - headers, - [ - .init(name: "content-type", value: "application/json") - ] - ) - } - - // | server | set | response body | binary | required | setResponseBodyAsBinary | - func test_setResponseBodyAsBinary_data() throws { - var headers: [HeaderField] = [] - let data = try converter.setResponseBodyAsBinary( - testStringData, - headerFields: &headers, - contentType: "application/octet-stream" - ) - XCTAssertEqual(data, testStringData) - XCTAssertEqual( - headers, - [ - .init(name: "content-type", value: "application/octet-stream") - ] - ) - } -} +// // Accept: */*, any string validates successfully +// (wildcard, "foobar", true), +// +// // Accept: text/*, so text/plain succeeds, application/json fails +// (partialWildcard, "text/plain", true), +// (partialWildcard, "application/json", false), +// +// // Accept: text/plain, text/plain succeeds, application/json fails +// (short, "text/plain", true), +// (short, "application/json", false), +// +// // A bunch of acceptable content types +// (long, "text/html", true), +// (long, "application/xhtml+xml", true), +// (long, "application/xml", true), +// (long, "image/webp", true), +// (long, "application/json", true), +// +// // Multiple values +// (multiple, "text/plain", true), +// (multiple, "application/json", true), +// (multiple, "application/xml", false), +// ] +// for (headers, contentType, success) in cases { +// if success { +// XCTAssertNoThrow( +// try converter.validateAcceptIfPresent( +// contentType, +// in: headers +// ), +// "Unexpected error when validating string: \(contentType) against headers: \(headers)" +// ) +// } else { +// let acceptHeader = +// headers +// .values(name: "accept") +// .joined(separator: ", ") +// XCTAssertThrowsError( +// try converter.validateAcceptIfPresent( +// contentType, +// in: headers +// ), +// "Expected to throw error when validating string: \(contentType) against headers: \(headers)", +// { error in +// guard +// let err = error as? RuntimeError, +// case .unexpectedAcceptHeader(let string) = err +// else { +// XCTFail("Threw an unexpected error: \(error)") +// return +// } +// XCTAssertEqual(string, acceptHeader) +// } +// ) +// } +// } +// } +// +// // MARK: Converter helper methods +// +// // | server | get | request path | URI | required | getPathParameterAsURI | +// func test_getPathParameterAsURI_various() throws { +// let path: [String: String] = [ +// "foo": "bar", +// "number": "1", +// "habitats": "land,air", +// ] +// do { +// let value = try converter.getPathParameterAsURI( +// in: path, +// name: "foo", +// as: String.self +// ) +// XCTAssertEqual(value, "bar") +// } +// do { +// let value = try converter.getPathParameterAsURI( +// in: path, +// name: "number", +// as: Int.self +// ) +// XCTAssertEqual(value, 1) +// } +// do { +// let value = try converter.getPathParameterAsURI( +// in: path, +// name: "habitats", +// as: [TestHabitat].self +// ) +// XCTAssertEqual(value, [.land, .air]) +// } +// } +// +// // | server | get | request query | URI | optional | getOptionalQueryItemAsURI | +// func test_getOptionalQueryItemAsURI_string() throws { +// let value: String? = try converter.getOptionalQueryItemAsURI( +// in: "search=foo", +// style: nil, +// explode: nil, +// name: "search", +// as: String.self +// ) +// XCTAssertEqual(value, "foo") +// } +// +// // | server | get | request query | URI | required | getRequiredQueryItemAsURI | +// func test_getRequiredQueryItemAsURI_string() throws { +// let value: String = try converter.getRequiredQueryItemAsURI( +// in: "search=foo", +// style: nil, +// explode: nil, +// name: "search", +// as: String.self +// ) +// XCTAssertEqual(value, "foo") +// } +// +// func test_getOptionalQueryItemAsURI_arrayOfStrings() throws { +// let query = "search=foo&search=bar" +// let value: [String]? = try converter.getOptionalQueryItemAsURI( +// in: query, +// style: nil, +// explode: nil, +// name: "search", +// as: [String].self +// ) +// XCTAssertEqual(value, ["foo", "bar"]) +// } +// +// func test_getRequiredQueryItemAsURI_arrayOfStrings() throws { +// let query = "search=foo&search=bar" +// let value: [String] = try converter.getRequiredQueryItemAsURI( +// in: query, +// style: nil, +// explode: nil, +// name: "search", +// as: [String].self +// ) +// XCTAssertEqual(value, ["foo", "bar"]) +// } +// +// func test_getRequiredQueryItemAsURI_arrayOfStrings_unexploded() throws { +// let query = "search=foo,bar" +// let value: [String] = try converter.getRequiredQueryItemAsURI( +// in: query, +// style: nil, +// explode: false, +// name: "search", +// as: [String].self +// ) +// XCTAssertEqual(value, ["foo", "bar"]) +// } +// +// func test_getOptionalQueryItemAsURI_date() throws { +// let query = "search=\(testDateEscapedString)" +// let value: Date? = try converter.getOptionalQueryItemAsURI( +// in: query, +// style: nil, +// explode: nil, +// name: "search", +// as: Date.self +// ) +// XCTAssertEqual(value, testDate) +// } +// +// func test_getRequiredQueryItemAsURI_arrayOfDates() throws { +// let query = "search=\(testDateEscapedString)&search=\(testDateEscapedString)" +// let value: [Date] = try converter.getRequiredQueryItemAsURI( +// in: query, +// style: nil, +// explode: nil, +// name: "search", +// as: [Date].self +// ) +// XCTAssertEqual(value, [testDate, testDate]) +// } +// +// // | server | get | request body | string | optional | getOptionalRequestBodyAsString | +// func test_getOptionalRequestBodyAsText_string() throws { +// let body: String? = try converter.getOptionalRequestBodyAsString( +// String.self, +// from: testStringData, +// transforming: { $0 } +// ) +// XCTAssertEqual(body, testString) +// } +// +// // | server | get | request body | string | required | getRequiredRequestBodyAsString | +// func test_getRequiredRequestBodyAsText_stringConvertible() throws { +// let body: String = try converter.getRequiredRequestBodyAsString( +// String.self, +// from: testStringData, +// transforming: { $0 } +// ) +// XCTAssertEqual(body, testString) +// } +// +// func test_getRequiredRequestBodyAsText_date() throws { +// let body: Date = try converter.getRequiredRequestBodyAsString( +// Date.self, +// from: testDateStringData, +// transforming: { $0 } +// ) +// XCTAssertEqual(body, testDate) +// } +// +// // | server | get | request body | JSON | optional | getOptionalRequestBodyAsJSON | +// func test_getOptionalRequestBodyAsJSON_codable() throws { +// let body: TestPet? = try converter.getOptionalRequestBodyAsJSON( +// TestPet.self, +// from: testStructData, +// transforming: { $0 } +// ) +// XCTAssertEqual(body, testStruct) +// } +// +// func test_getOptionalRequestBodyAsJSON_codable_string() throws { +// let body: String? = try converter.getOptionalRequestBodyAsJSON( +// String.self, +// from: testQuotedStringData, +// transforming: { $0 } +// ) +// XCTAssertEqual(body, testString) +// } +// +// // | server | get | request body | JSON | required | getRequiredRequestBodyAsJSON | +// func test_getRequiredRequestBodyAsJSON_codable() throws { +// let body: TestPet = try converter.getRequiredRequestBodyAsJSON( +// TestPet.self, +// from: testStructData, +// transforming: { $0 } +// ) +// XCTAssertEqual(body, testStruct) +// } +// +// // | server | get | request body | binary | optional | getOptionalRequestBodyAsBinary | +// func test_getOptionalRequestBodyAsBinary_data() throws { +// let body: Data? = try converter.getOptionalRequestBodyAsBinary( +// Data.self, +// from: testStringData, +// transforming: { $0 } +// ) +// XCTAssertEqual(body, testStringData) +// } +// +// // | server | get | request body | binary | required | getRequiredRequestBodyAsBinary | +// func test_getRequiredRequestBodyAsBinary_data() throws { +// let body: Data = try converter.getRequiredRequestBodyAsBinary( +// Data.self, +// from: testStringData, +// transforming: { $0 } +// ) +// XCTAssertEqual(body, testStringData) +// } +// +// // | server | set | response body | string | required | setResponseBodyAsString | +// func test_setResponseBodyAsText_stringConvertible() throws { +// var headers: [HeaderField] = [] +// let data = try converter.setResponseBodyAsString( +// testString, +// headerFields: &headers, +// contentType: "text/plain" +// ) +// XCTAssertEqual(data, testStringData) +// XCTAssertEqual( +// headers, +// [ +// .init(name: "content-type", value: "text/plain") +// ] +// ) +// } +// +// // | server | set | response body | string | required | setResponseBodyAsString | +// func test_setResponseBodyAsText_date() throws { +// var headers: [HeaderField] = [] +// let data = try converter.setResponseBodyAsString( +// testDate, +// headerFields: &headers, +// contentType: "text/plain" +// ) +// XCTAssertEqual(data, testDateStringData) +// XCTAssertEqual( +// headers, +// [ +// .init(name: "content-type", value: "text/plain") +// ] +// ) +// } +// +// // | server | set | response body | JSON | required | setResponseBodyAsJSON | +// func test_setResponseBodyAsJSON_codable() throws { +// var headers: [HeaderField] = [] +// let data = try converter.setResponseBodyAsJSON( +// testStruct, +// headerFields: &headers, +// contentType: "application/json" +// ) +// XCTAssertEqual(data, testStructPrettyData) +// XCTAssertEqual( +// headers, +// [ +// .init(name: "content-type", value: "application/json") +// ] +// ) +// } +// +// // | server | set | response body | binary | required | setResponseBodyAsBinary | +// func test_setResponseBodyAsBinary_data() throws { +// var headers: [HeaderField] = [] +// let data = try converter.setResponseBodyAsBinary( +// testStringData, +// headerFields: &headers, +// contentType: "application/octet-stream" +// ) +// XCTAssertEqual(data, testStringData) +// XCTAssertEqual( +// headers, +// [ +// .init(name: "content-type", value: "application/octet-stream") +// ] +// ) +// } +//} diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_CurrencyExtensions.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_CurrencyExtensions.swift deleted file mode 100644 index c49bb682..00000000 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_CurrencyExtensions.swift +++ /dev/null @@ -1,86 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 XCTest -@testable import OpenAPIRuntime - -final class Test_CurrencyExtensions: Test_Runtime { - - func testHeaderFields_add_string() throws { - var headerFields: [HeaderField] = [] - headerFields.add(name: "foo", value: "bar") - XCTAssertEqual( - headerFields, - [ - .init(name: "foo", value: "bar") - ] - ) - } - - func testHeaderFields_add_nil() throws { - var headerFields: [HeaderField] = [] - let value: String? = nil - headerFields.add(name: "foo", value: value) - XCTAssertEqual(headerFields, []) - } - - func testHeaderFields_firstValue_found() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: "bar") - ] - XCTAssertEqual(headerFields.firstValue(name: "foo"), "bar") - } - - func testHeaderFields_firstValue_nil() throws { - let headerFields: [HeaderField] = [] - XCTAssertNil(headerFields.firstValue(name: "foo")) - } - - func testHeaderFields_values() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: "bar"), - .init(name: "foo", value: "baz"), - ] - XCTAssertEqual(headerFields.values(name: "foo"), ["bar", "baz"]) - } - - func testHeaderFields_removeAll_noMatches() throws { - var headerFields: [HeaderField] = [ - .init(name: "one", value: "one"), - .init(name: "two", value: "two"), - ] - headerFields.removeAll(named: "three") - XCTAssertEqual(headerFields.map(\.name), ["one", "two"]) - } - - func testHeaderFields_removeAll_oneMatch() throws { - var headerFields: [HeaderField] = [ - .init(name: "one", value: "one"), - .init(name: "two", value: "two"), - .init(name: "three", value: "three"), - ] - headerFields.removeAll(named: "three") - XCTAssertEqual(headerFields.map(\.name), ["one", "two"]) - } - - func testHeaderFields_removeAll_manyMatches() throws { - var headerFields: [HeaderField] = [ - .init(name: "one", value: "one"), - .init(name: "three", value: "3"), - .init(name: "two", value: "two"), - .init(name: "three", value: "three"), - ] - headerFields.removeAll(named: "three") - XCTAssertEqual(headerFields.map(\.name), ["one", "two"]) - } -} diff --git a/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift b/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift index 18646db6..645db617 100644 --- a/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift +++ b/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift @@ -1,51 +1,51 @@ -//===----------------------------------------------------------------------===// +////===----------------------------------------------------------------------===// +//// +//// 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 XCTest +//@_spi(Generated)@testable import OpenAPIRuntime // -// This source file is part of the SwiftOpenAPIGenerator open source project +//final class Test_UniversalServer: Test_Runtime { // -// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors -// Licensed under Apache License v2.0 +// struct MockHandler: Sendable {} // -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// func testApiPathComponentsWithServerPrefix_noPrefix() throws { +// let server = UniversalServer( +// handler: MockHandler() +// ) +// let components: [RouterPathComponent] = [ +// .constant("foo"), +// .parameter("bar"), +// ] +// let prefixed = try server.apiPathComponentsWithServerPrefix(components) +// // When no server path prefix, components stay the same +// XCTAssertEqual(prefixed, components) +// } // -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// -import XCTest -@_spi(Generated)@testable import OpenAPIRuntime - -final class Test_UniversalServer: Test_Runtime { - - struct MockHandler: Sendable {} - - func testApiPathComponentsWithServerPrefix_noPrefix() throws { - let server = UniversalServer( - handler: MockHandler() - ) - let components: [RouterPathComponent] = [ - .constant("foo"), - .parameter("bar"), - ] - let prefixed = try server.apiPathComponentsWithServerPrefix(components) - // When no server path prefix, components stay the same - XCTAssertEqual(prefixed, components) - } - - func testApiPathComponentsWithServerPrefix_withPrefix() throws { - let server = UniversalServer( - serverURL: try serverURL, - handler: MockHandler() - ) - let components: [RouterPathComponent] = [ - .constant("foo"), - .parameter("bar"), - ] - let prefixed = try server.apiPathComponentsWithServerPrefix(components) - let expected: [RouterPathComponent] = [ - .constant("api"), - .constant("foo"), - .parameter("bar"), - ] - XCTAssertEqual(prefixed, expected) - } -} +// func testApiPathComponentsWithServerPrefix_withPrefix() throws { +// let server = UniversalServer( +// serverURL: try serverURL, +// handler: MockHandler() +// ) +// let components: [RouterPathComponent] = [ +// .constant("foo"), +// .parameter("bar"), +// ] +// let prefixed = try server.apiPathComponentsWithServerPrefix(components) +// let expected: [RouterPathComponent] = [ +// .constant("api"), +// .constant("foo"), +// .parameter("bar"), +// ] +// XCTAssertEqual(prefixed, expected) +// } +//} diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index 155a45d7..7d0e433b 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -14,6 +14,7 @@ import XCTest @_spi(Generated) import OpenAPIRuntime +import HTTPTypes class Test_Runtime: XCTestCase { @@ -42,9 +43,9 @@ class Test_Runtime: XCTestCase { return components } - var testRequest: OpenAPIRuntime.Request { - .init(path: "/api", query: nil, method: .get) - } +// var testRequest: OpenAPIRuntime.Request { +// .init(path: "/api", query: nil, method: .get) +// } var testDate: Date { Date(timeIntervalSince1970: 1_674_036_251) @@ -153,35 +154,32 @@ struct AuthenticationMiddleware: ClientMiddleware { var token: String func intercept( - _ request: Request, + _ request: HTTPRequest, + body: HTTPBody?, baseURL: URL, operationID: String, - next: (Request, URL) async throws -> Response - ) async throws -> Response { + next: (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody) + ) async throws -> (HTTPResponse, HTTPBody) { var request = request - request.headerFields.append( - .init( - name: "Authorization", - value: "Bearer \(token)" - ) - ) - return try await next(request, baseURL) + request.headerFields[.authorization] = "Bearer \(token)" + return try await next(request, body, baseURL) } } /// Prints the request method + path and response status code. struct PrintingMiddleware: ClientMiddleware { func intercept( - _ request: Request, + _ request: HTTPRequest, + body: HTTPBody?, baseURL: URL, operationID: String, - next: (Request, URL) async throws -> Response - ) async throws -> Response { - print("Sending \(request.method.name) \(request.path)") + next: (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody) + ) async throws -> (HTTPResponse, HTTPBody) { + print("Sending \(request.method) \(request.path ?? "")") do { - let response = try await next(request, baseURL) - print("Received: \(response.statusCode)") - return response + let (response, responseBody) = try await next(request, body, baseURL) + print("Received: \(response.status)") + return (response, responseBody) } catch { print("Failed with error: \(error.localizedDescription)") throw error diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift index 8ae57d60..33ede7b9 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift @@ -128,7 +128,7 @@ final class Test_URIParser: Test_Runtime { ) throws { var parser = URIParser( configuration: variant.config, - data: input.string + data: input.string[...] ) let parsedNode = try parser.parseRoot() XCTAssertEqual( diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift index 77f0eb02..f623e141 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift @@ -335,7 +335,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { let decodedValue = try decoder.decode( T.self, forKey: key, - from: encodedString + from: encodedString[...] ) XCTAssertEqual( decodedValue, From 164f84e61ce4e7e875f8d0c05e3c2a255f662129 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 5 Sep 2023 10:51:44 +0200 Subject: [PATCH 05/55] wip --- .../Conversion/CurrencyExtensions.swift | 10 ++-- .../OpenAPIRuntime/Errors/ServerError.swift | 2 +- .../Interface/CurrencyTypes.swift | 13 +++- .../OpenAPIRuntime/Interface/HTTPBody.swift | 60 +++++++++---------- .../Interface/ServerTransport.swift | 4 +- .../Interface/UniversalServer.swift | 3 +- .../Conversion/Test_Converter+Client.swift | 26 ++++---- .../Conversion/Test_Converter+Common.swift | 26 ++++---- .../Conversion/Test_Converter+Server.swift | 26 ++++---- .../Interface/Test_UniversalServer.swift | 26 ++++---- Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 6 +- 11 files changed, 106 insertions(+), 96 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index 95effb05..18f08a14 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -66,12 +66,12 @@ extension HTTPRequest { } extension HTTPRequest { - + @_spi(Generated) public init(path: String, method: Method) { self.init(method: method, scheme: nil, authority: nil, path: path) } - + @_spi(Generated) public var query: Substring? { guard let path else { @@ -81,13 +81,13 @@ extension HTTPRequest { return nil } let queryEnd = path.firstIndex(of: "#") ?? path.endIndex - let query = path[path.index(after: queryStart) ..< queryEnd] + let query = path[path.index(after: queryStart)..( - dataChunks: S, + byteChunks: S, length: Length, iterationBehavior: IterationBehavior ) where S.Element == DataType { self.init( - sequence: .init(WrappedSyncSequence(sequence: dataChunks)), + sequence: .init(WrappedSyncSequence(sequence: byteChunks)), length: length, iterationBehavior: iterationBehavior ) } public convenience init( - dataChunks: C, + byteChunks: C, length: Length ) where C.Element == DataType { self.init( - sequence: .init(WrappedSyncSequence(sequence: dataChunks)), + sequence: .init(WrappedSyncSequence(sequence: byteChunks)), length: length, iterationBehavior: .multiple ) } public convenience init( - dataChunks: C + byteChunks: C ) where C.Element == DataType { self.init( - sequence: .init(WrappedSyncSequence(sequence: dataChunks)), - length: .known(dataChunks.map(\.count).reduce(0, +)), + sequence: .init(WrappedSyncSequence(sequence: byteChunks)), + length: .known(byteChunks.map(\.count).reduce(0, +)), iterationBehavior: .multiple ) } @@ -327,51 +327,51 @@ extension StringProtocol { extension HTTPBody { public convenience init( - data: some StringProtocol, + string: some StringProtocol, length: Length ) { self.init( - dataChunks: [data.asBodyChunk], + bytes: string.asBodyChunk, length: length ) } public convenience init( - data: some StringProtocol + string: some StringProtocol ) { self.init( - dataChunks: [data.asBodyChunk], - length: .known(data.count) + bytes: string.asBodyChunk, + length: .known(string.count) ) } public convenience init( - dataChunks: S, + byteChunks: S, length: Length, iterationBehavior: IterationBehavior ) where S.Element: StringProtocol { self.init( - dataChunks: dataChunks.map(\.asBodyChunk), + byteChunks: byteChunks.map(\.asBodyChunk), length: length, iterationBehavior: iterationBehavior ) } public convenience init( - dataChunks: C, + byteChunks: C, length: Length ) where C.Element: StringProtocol { self.init( - dataChunks: dataChunks.map(\.asBodyChunk), + byteChunks: byteChunks.map(\.asBodyChunk), length: length ) } public convenience init( - dataChunks: C + byteChunks: C ) where C.Element: StringProtocol { self.init( - dataChunks: dataChunks.map(\.asBodyChunk) + byteChunks: byteChunks.map(\.asBodyChunk) ) } @@ -430,14 +430,14 @@ extension HTTPBody { extension HTTPBody: ExpressibleByStringLiteral { public convenience init(stringLiteral value: String) { - self.init(data: value) + self.init(string: value) } } extension HTTPBody { - public convenience init(data: [UInt8]) { - self.init(data: data[...]) + public convenience init(bytes: [UInt8]) { + self.init(bytes: bytes[...]) } } @@ -446,14 +446,14 @@ extension HTTPBody: ExpressibleByArrayLiteral { public typealias ArrayLiteralElement = UInt8 public convenience init(arrayLiteral elements: UInt8...) { - self.init(data: elements) + self.init(bytes: elements) } } extension HTTPBody { public convenience init(data: Data) { - self.init(data: ArraySlice(data)) + self.init(bytes: ArraySlice(data)) } /// Accumulates the full body in-memory into a single buffer diff --git a/Sources/OpenAPIRuntime/Interface/ServerTransport.swift b/Sources/OpenAPIRuntime/Interface/ServerTransport.swift index 4af9aaac..baeee2f5 100644 --- a/Sources/OpenAPIRuntime/Interface/ServerTransport.swift +++ b/Sources/OpenAPIRuntime/Interface/ServerTransport.swift @@ -17,7 +17,9 @@ import HTTPTypes public protocol ServerTransport { func register( - _ handler: @Sendable @escaping (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody), + _ handler: @Sendable @escaping (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> ( + HTTPResponse, HTTPBody + ), method: HTTPRequest.Method, path: String ) throws diff --git a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift index 0b1a40d1..eee7a816 100644 --- a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift +++ b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift @@ -65,7 +65,8 @@ public struct UniversalServer: Sendable { metadata: ServerRequestMetadata, forOperation operationID: String, using handlerMethod: @Sendable @escaping (APIHandler) -> ((OperationInput) async throws -> OperationOutput), - deserializer: @Sendable @escaping (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> OperationInput, + deserializer: @Sendable @escaping (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> + OperationInput, serializer: @Sendable @escaping (OperationOutput, HTTPRequest) throws -> (HTTPResponse, HTTPBody) ) async throws -> (HTTPResponse, HTTPBody) where OperationInput: Sendable, OperationOutput: Sendable { @Sendable diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift index d70176c8..a2f25115 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -1,16 +1,16 @@ -////===----------------------------------------------------------------------===// -//// -//// 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 -//// -////===----------------------------------------------------------------------===// +//===----------------------------------------------------------------------===// +// +// 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 XCTest //@_spi(Generated)@testable import OpenAPIRuntime // diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift index 7ef9c100..b42d47ff 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift @@ -1,16 +1,16 @@ -////===----------------------------------------------------------------------===// -//// -//// 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 -//// -////===----------------------------------------------------------------------===// +//===----------------------------------------------------------------------===// +// +// 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 XCTest //@_spi(Generated)@testable import OpenAPIRuntime // diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift index 668eb6e7..9f96b4ee 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift @@ -1,16 +1,16 @@ -////===----------------------------------------------------------------------===// -//// -//// 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 -//// -////===----------------------------------------------------------------------===// +//===----------------------------------------------------------------------===// +// +// 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 XCTest //@_spi(Generated)@testable import OpenAPIRuntime // diff --git a/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift b/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift index 645db617..d1a00664 100644 --- a/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift +++ b/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift @@ -1,16 +1,16 @@ -////===----------------------------------------------------------------------===// -//// -//// 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 -//// -////===----------------------------------------------------------------------===// +//===----------------------------------------------------------------------===// +// +// 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 XCTest //@_spi(Generated)@testable import OpenAPIRuntime // diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index 7d0e433b..46e95be4 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -43,9 +43,9 @@ class Test_Runtime: XCTestCase { return components } -// var testRequest: OpenAPIRuntime.Request { -// .init(path: "/api", query: nil, method: .get) -// } + // var testRequest: OpenAPIRuntime.Request { + // .init(path: "/api", query: nil, method: .get) + // } var testDate: Date { Date(timeIntervalSince1970: 1_674_036_251) From 3db782e568ae9cc4108a35238116e0173a657865 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 5 Sep 2023 15:16:17 +0200 Subject: [PATCH 06/55] Remove mapChunks, rename initializer parameters --- .../Conversion/CurrencyExtensions.swift | 29 --------- .../Interface/CurrencyTypes.swift | 39 +++++++++++- .../OpenAPIRuntime/Interface/HTTPBody.swift | 47 ++------------ .../Interface/Test_HTTPBody.swift | 62 ++++--------------- 4 files changed, 55 insertions(+), 122 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index 18f08a14..b3965017 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -65,35 +65,6 @@ extension HTTPRequest { } } -extension HTTPRequest { - - @_spi(Generated) - public init(path: String, method: Method) { - self.init(method: method, scheme: nil, authority: nil, path: path) - } - - @_spi(Generated) - public var query: Substring? { - guard let path else { - return nil - } - guard let queryStart = path.firstIndex(of: "?") else { - return nil - } - let queryEnd = path.firstIndex(of: "#") ?? path.endIndex - let query = path[path.index(after: queryStart).. Element - ) -> HTTPBody { - let validatedTransform: @Sendable (Element) async -> Element - switch length { - case .known: - validatedTransform = { element in - let transformedElement = await transform(element) - guard transformedElement.count == element.count else { - fatalError( - "OpenAPIRuntime.HTTPBody.mapChunks transform closure attempted to change the length of a chunk in a body which has a total length specified, this is not allowed." - ) - } - return transformedElement - } - case .unknown: - validatedTransform = transform - } - return HTTPBody( - sequence: map(validatedTransform), - length: length, - iterationBehavior: iterationBehavior - ) - } -} - // MARK: - Consumption utils extension HTTPBody { @@ -346,32 +311,32 @@ extension HTTPBody { } public convenience init( - byteChunks: S, + stringChunks: S, length: Length, iterationBehavior: IterationBehavior ) where S.Element: StringProtocol { self.init( - byteChunks: byteChunks.map(\.asBodyChunk), + byteChunks: stringChunks.map(\.asBodyChunk), length: length, iterationBehavior: iterationBehavior ) } public convenience init( - byteChunks: C, + stringChunks: C, length: Length ) where C.Element: StringProtocol { self.init( - byteChunks: byteChunks.map(\.asBodyChunk), + byteChunks: stringChunks.map(\.asBodyChunk), length: length ) } public convenience init( - byteChunks: C + stringChunks: C ) where C.Element: StringProtocol { self.init( - byteChunks: byteChunks.map(\.asBodyChunk) + byteChunks: stringChunks.map(\.asBodyChunk) ) } diff --git a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift index 5b19e7f9..4f500afc 100644 --- a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift +++ b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift @@ -21,7 +21,7 @@ final class Test_Body: Test_Runtime { // A single string. do { - let body: HTTPBody = HTTPBody(data: "hello") + let body: HTTPBody = HTTPBody(string: "hello") try await _testConsume( body, expected: "hello" @@ -39,7 +39,7 @@ final class Test_Body: Test_Runtime { // A sequence of strings. do { - let body: HTTPBody = HTTPBody(dataChunks: ["hel", "lo"]) + let body: HTTPBody = HTTPBody(stringChunks: ["hel", "lo"]) try await _testConsume( body, expected: "hello" @@ -48,18 +48,18 @@ final class Test_Body: Test_Runtime { // A single substring. do { - let body: HTTPBody = HTTPBody(data: "hello"[...]) + let body: HTTPBody = HTTPBody(string: "hello") try await _testConsume( body, - expected: "hello"[...] + expected: "hello" ) } // A sequence of substrings. do { - let body: HTTPBody = HTTPBody(dataChunks: [ - "hel"[...], - "lo"[...], + let body: HTTPBody = HTTPBody(stringChunks: [ + "hel", + "lo", ]) try await _testConsume( body, @@ -69,7 +69,7 @@ final class Test_Body: Test_Runtime { // A single array of bytes. do { - let body: HTTPBody = HTTPBody(data: [0]) + let body: HTTPBody = HTTPBody(bytes: [0]) try await _testConsume( body, expected: [0] @@ -96,7 +96,7 @@ final class Test_Body: Test_Runtime { // A sequence of arrays of bytes. do { - let body: HTTPBody = HTTPBody(dataChunks: [[0], [1]]) + let body: HTTPBody = HTTPBody(byteChunks: [[0], [1]]) try await _testConsume( body, expected: [0, 1] @@ -105,7 +105,7 @@ final class Test_Body: Test_Runtime { // A single slice of an array of bytes. do { - let body: HTTPBody = HTTPBody(data: [0][...]) + let body: HTTPBody = HTTPBody(bytes: [0][...]) try await _testConsume( body, expected: [0][...] @@ -114,7 +114,7 @@ final class Test_Body: Test_Runtime { // A sequence of slices of an array of bytes. do { - let body: HTTPBody = HTTPBody(dataChunks: [ + let body: HTTPBody = HTTPBody(byteChunks: [ [0][...], [1][...], ]) @@ -206,46 +206,6 @@ final class Test_Body: Test_Runtime { } XCTAssertEqual(chunks, ["hel", "lo"].map { Array($0.utf8)[...] }) } - - func testMapChunks() async throws { - let body: HTTPBody = HTTPBody( - stream: AsyncStream( - String.self, - { continuation in - continuation.yield("hello") - continuation.yield(" ") - continuation.yield("world") - continuation.finish() - } - ), - length: .known(5) - ) - actor Chunker { - private var iterator: Array.Iterator - init(expectedChunks: [HTTPBody.DataType]) { - self.iterator = expectedChunks.makeIterator() - } - func checkNextChunk(_ actual: HTTPBody.DataType) { - XCTAssertEqual(actual, iterator.next()) - } - } - let chunker = Chunker( - expectedChunks: [ - "hello", - " ", - "world", - ] - .map { Array($0.utf8)[...] } - ) - let finalString = - try await body - .mapChunks { element in - await chunker.checkNextChunk(element) - return element.reversed()[...] - } - .collectAsString(upTo: .max) - XCTAssertEqual(finalString, "olleh dlrow") - } } extension Test_Body { From 6c97d48a3cd3de32347f02c36089b64494fc1a48 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 5 Sep 2023 16:41:05 +0200 Subject: [PATCH 07/55] wip --- .swift-format | 2 +- .../Interface/CurrencyTypes.swift | 1 - .../OpenAPIRuntime/Interface/HTTPBody.swift | 53 +++++++++++++++++-- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/.swift-format b/.swift-format index e6a70edf..2afa7389 100644 --- a/.swift-format +++ b/.swift-format @@ -47,7 +47,7 @@ "UseLetInEveryBoundCaseVariable" : false, "UseShorthandTypeNames" : true, "UseSingleLinePropertyGetter" : false, - "UseSynthesizedInitializer" : true, + "UseSynthesizedInitializer" : false, "UseTripleSlashForDocumentationComments" : true, "UseWhereClausesInForLoops" : false, "ValidateDocumentationComments" : false diff --git a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift index bdad5ded..6fb6f662 100644 --- a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift +++ b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift @@ -67,4 +67,3 @@ extension HTTPResponse { self.init(status: .init(code: statusCode)) } } - diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index 6d2239c4..64120c6f 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -67,7 +67,8 @@ public final class HTTPBody: @unchecked Sendable { /// Whether an iterator has already been created. private var locked_iteratorCreated: Bool = false - private init( + @usableFromInline + init( sequence: BodySequence, length: Length, iterationBehavior: IterationBehavior @@ -97,6 +98,7 @@ extension HTTPBody: Hashable { extension HTTPBody { + @inlinable public convenience init() { self.init( byteChunks: [], @@ -105,6 +107,7 @@ extension HTTPBody { ) } + @inlinable public convenience init( bytes: DataType, length: Length @@ -115,6 +118,7 @@ extension HTTPBody { ) } + @inlinable public convenience init( bytes: DataType ) { @@ -124,6 +128,7 @@ extension HTTPBody { ) } + @inlinable public convenience init( byteChunks: S, length: Length, @@ -136,6 +141,7 @@ extension HTTPBody { ) } + @inlinable public convenience init( byteChunks: C, length: Length @@ -147,6 +153,7 @@ extension HTTPBody { ) } + @inlinable public convenience init( byteChunks: C ) where C.Element == DataType { @@ -157,6 +164,7 @@ extension HTTPBody { ) } + @inlinable public convenience init( stream: AsyncThrowingStream, length: HTTPBody.Length @@ -168,6 +176,7 @@ extension HTTPBody { ) } + @inlinable public convenience init( stream: AsyncStream, length: HTTPBody.Length @@ -179,6 +188,7 @@ extension HTTPBody { ) } + @inlinable public convenience init( sequence: S, length: HTTPBody.Length, @@ -284,13 +294,16 @@ extension HTTPBody { // MARK: - String-based bodies extension StringProtocol { - fileprivate var asBodyChunk: HTTPBody.DataType { + + @inlinable + var asBodyChunk: HTTPBody.DataType { Array(utf8)[...] } } extension HTTPBody { + @inlinable public convenience init( string: some StringProtocol, length: Length @@ -301,6 +314,7 @@ extension HTTPBody { ) } + @inlinable public convenience init( string: some StringProtocol ) { @@ -310,6 +324,7 @@ extension HTTPBody { ) } + @inlinable public convenience init( stringChunks: S, length: Length, @@ -322,6 +337,7 @@ extension HTTPBody { ) } + @inlinable public convenience init( stringChunks: C, length: Length @@ -332,6 +348,7 @@ extension HTTPBody { ) } + @inlinable public convenience init( stringChunks: C ) where C.Element: StringProtocol { @@ -340,6 +357,7 @@ extension HTTPBody { ) } + @inlinable public convenience init( stream: AsyncThrowingStream, length: HTTPBody.Length @@ -351,6 +369,7 @@ extension HTTPBody { ) } + @inlinable public convenience init( stream: AsyncStream, length: HTTPBody.Length @@ -362,6 +381,7 @@ extension HTTPBody { ) } + @inlinable public convenience init( sequence: S, length: HTTPBody.Length, @@ -401,6 +421,7 @@ extension HTTPBody: ExpressibleByStringLiteral { extension HTTPBody { + @inlinable public convenience init(bytes: [UInt8]) { self.init(bytes: bytes[...]) } @@ -445,6 +466,7 @@ extension HTTPBody { private let produceNext: () async throws -> Element? + @usableFromInline init( _ iterator: Iterator ) where Iterator.Element == Element { @@ -463,44 +485,65 @@ extension HTTPBody { extension HTTPBody { /// A type-erased async sequence that wraps input sequences. - private struct BodySequence: AsyncSequence { + @usableFromInline + struct BodySequence: AsyncSequence { + @usableFromInline typealias AsyncIterator = HTTPBody.Iterator + + @usableFromInline typealias Element = DataType - private let produceIterator: () -> AsyncIterator + @usableFromInline + let produceIterator: () -> AsyncIterator + @inlinable init(_ sequence: S) where S.Element == Element { self.produceIterator = { .init(sequence.makeAsyncIterator()) } } + @usableFromInline func makeAsyncIterator() -> AsyncIterator { produceIterator() } } /// A wrapper for a sync sequence. - private struct WrappedSyncSequence: AsyncSequence + @usableFromInline + struct WrappedSyncSequence: AsyncSequence where S.Element == DataType, S.Iterator.Element == DataType { + @usableFromInline typealias AsyncIterator = Iterator + + @usableFromInline typealias Element = DataType + @usableFromInline struct Iterator: AsyncIteratorProtocol { + @usableFromInline typealias Element = DataType var iterator: any IteratorProtocol + @usableFromInline mutating func next() async throws -> HTTPBody.DataType? { iterator.next() } } + @usableFromInline let sequence: S + @inlinable + init(sequence: S) { + self.sequence = sequence + } + + @usableFromInline func makeAsyncIterator() -> Iterator { Iterator(iterator: sequence.makeIterator()) } From 466b70152f195265d257df2b78e657bb7faa4295 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 5 Sep 2023 17:24:09 +0200 Subject: [PATCH 08/55] Remove String coder, fix up client unit tests --- .../Conversion/CurrencyExtensions.swift | 25 - .../StringCoder/StringDecoder.swift | 448 --------------- .../StringCoder/StringEncoder.swift | 446 --------------- .../Conversion/Test_Converter+Client.swift | 509 ++++++++---------- .../Test_StringCodingRoundtrip.swift | 132 ----- Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 36 +- 6 files changed, 245 insertions(+), 1351 deletions(-) delete mode 100644 Sources/OpenAPIRuntime/StringCoder/StringDecoder.swift delete mode 100644 Sources/OpenAPIRuntime/StringCoder/StringEncoder.swift delete mode 100644 Tests/OpenAPIRuntimeTests/StringCoder/Test_StringCodingRoundtrip.swift diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index b3965017..7e51c4a9 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -151,31 +151,6 @@ extension Converter { return try decoder.decode(T.self, from: data) } - func convertFromStringData( - _ body: HTTPBody - ) async throws -> T { - let data = try await body.collect(upTo: .max) - let encodedString = String(decoding: data, as: UTF8.self) - let decoder = StringDecoder( - dateTranscoder: configuration.dateTranscoder - ) - let value = try decoder.decode( - T.self, - from: encodedString - ) - return value - } - - func convertToStringData( - _ value: T - ) throws -> HTTPBody { - let encoder = StringEncoder( - dateTranscoder: configuration.dateTranscoder - ) - let encodedString = try encoder.encode(value) - return HTTPBody(bytes: Array(encodedString.utf8)) - } - func convertBinaryToData( _ binary: HTTPBody ) throws -> HTTPBody { diff --git a/Sources/OpenAPIRuntime/StringCoder/StringDecoder.swift b/Sources/OpenAPIRuntime/StringCoder/StringDecoder.swift deleted file mode 100644 index 22add7bf..00000000 --- a/Sources/OpenAPIRuntime/StringCoder/StringDecoder.swift +++ /dev/null @@ -1,448 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 - -/// A type that decodes a `Decodable` objects from a string -/// using `LosslessStringConvertible`. -struct StringDecoder: Sendable { - - /// The coder used to serialize Date values. - let dateTranscoder: any DateTranscoder -} - -extension StringDecoder { - - /// Attempt to decode an object from a string. - /// - Parameters: - /// - type: The type to decode. - /// - data: The encoded string. - /// - Returns: The decoded value. - func decode( - _ type: T.Type = T.self, - from data: String - ) throws -> T { - let decoder = LosslessStringConvertibleDecoder( - dateTranscoder: dateTranscoder, - encodedString: data - ) - // We have to catch the special values early, otherwise we fall - // back to their Codable implementations, which don't give us - // a chance to customize the coding in the containers. - let value: T - switch type { - case is Date.Type: - value = try decoder.singleValueContainer().decode(Date.self) as! T - default: - value = try T.init(from: decoder) - } - return value - } -} - -/// The decoder used by `StringDecoder`. -private struct LosslessStringConvertibleDecoder { - - /// The coder used to serialize Date values. - let dateTranscoder: any DateTranscoder - - /// The underlying encoded string. - let encodedString: String -} - -extension LosslessStringConvertibleDecoder { - - /// A decoder error. - enum DecoderError: Swift.Error { - - /// The `LosslessStringConvertible` initializer returned nil for the - /// provided raw string. - case failedToDecodeValue - - /// The decoder tried to decode a nested container, which are not - /// supported. - case containersNotSupported - } -} - -extension LosslessStringConvertibleDecoder: Decoder { - - var codingPath: [any CodingKey] { - [] - } - - var userInfo: [CodingUserInfoKey: Any] { - [:] - } - - func container( - keyedBy type: Key.Type - ) throws -> KeyedDecodingContainer where Key: CodingKey { - KeyedDecodingContainer(KeyedContainer(decoder: self)) - } - - func unkeyedContainer() throws -> any UnkeyedDecodingContainer { - UnkeyedContainer(decoder: self) - } - - func singleValueContainer() throws -> any SingleValueDecodingContainer { - SingleValueContainer(decoder: self) - } -} - -extension LosslessStringConvertibleDecoder { - - /// A single value container used by `LosslessStringConvertibleDecoder`. - struct SingleValueContainer { - - /// The underlying decoder. - let decoder: LosslessStringConvertibleDecoder - - /// Decodes a value of type conforming to `LosslessStringConvertible`. - /// - Returns: The decoded value. - private func _decodeLosslessStringConvertible( - _: T.Type = T.self - ) throws -> T { - guard let parsedValue = T(String(decoder.encodedString)) else { - throw DecodingError.typeMismatch( - T.self, - .init( - codingPath: codingPath, - debugDescription: "Failed to convert to the requested type." - ) - ) - } - return parsedValue - } - } - - /// An unkeyed container used by `LosslessStringConvertibleDecoder`. - struct UnkeyedContainer { - - /// The underlying decoder. - let decoder: LosslessStringConvertibleDecoder - } - - /// A keyed container used by `LosslessStringConvertibleDecoder`. - struct KeyedContainer { - - /// The underlying decoder. - let decoder: LosslessStringConvertibleDecoder - } -} - -extension LosslessStringConvertibleDecoder.SingleValueContainer: SingleValueDecodingContainer { - - var codingPath: [any CodingKey] { - [] - } - - func decodeNil() -> Bool { - false - } - - func decode(_ type: Bool.Type) throws -> Bool { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: String.Type) throws -> String { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: Double.Type) throws -> Double { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: Float.Type) throws -> Float { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: Int.Type) throws -> Int { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: Int8.Type) throws -> Int8 { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: Int16.Type) throws -> Int16 { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: Int32.Type) throws -> Int32 { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: Int64.Type) throws -> Int64 { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: UInt.Type) throws -> UInt { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: UInt8.Type) throws -> UInt8 { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: UInt16.Type) throws -> UInt16 { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: UInt32.Type) throws -> UInt32 { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: UInt64.Type) throws -> UInt64 { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: T.Type) throws -> T where T: Decodable { - switch type { - case is Bool.Type: - return try decode(Bool.self) as! T - case is String.Type: - return try decode(String.self) as! T - case is Double.Type: - return try decode(Double.self) as! T - case is Float.Type: - return try decode(Float.self) as! T - case is Int.Type: - return try decode(Int.self) as! T - case is Int8.Type: - return try decode(Int8.self) as! T - case is Int16.Type: - return try decode(Int16.self) as! T - case is Int32.Type: - return try decode(Int32.self) as! T - case is Int64.Type: - return try decode(Int64.self) as! T - case is UInt.Type: - return try decode(UInt.self) as! T - case is UInt8.Type: - return try decode(UInt8.self) as! T - case is UInt16.Type: - return try decode(UInt16.self) as! T - case is UInt32.Type: - return try decode(UInt32.self) as! T - case is UInt64.Type: - return try decode(UInt64.self) as! T - case is Date.Type: - return try decoder - .dateTranscoder - .decode(String(decoder.encodedString)) as! T - default: - guard let convertileType = T.self as? any LosslessStringConvertible.Type else { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - return try _decodeLosslessStringConvertible(convertileType) as! T - } - } -} - -extension LosslessStringConvertibleDecoder.UnkeyedContainer: UnkeyedDecodingContainer { - - var codingPath: [any CodingKey] { - [] - } - - var count: Int? { - nil - } - - var isAtEnd: Bool { - true - } - - var currentIndex: Int { - 0 - } - - mutating func decodeNil() throws -> Bool { - false - } - - mutating func decode(_ type: Bool.Type) throws -> Bool { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: String.Type) throws -> String { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: Double.Type) throws -> Double { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: Float.Type) throws -> Float { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: Int.Type) throws -> Int { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: Int8.Type) throws -> Int8 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: Int16.Type) throws -> Int16 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: Int32.Type) throws -> Int32 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: Int64.Type) throws -> Int64 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: UInt.Type) throws -> UInt { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: UInt8.Type) throws -> UInt8 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: UInt16.Type) throws -> UInt16 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: UInt32.Type) throws -> UInt32 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: UInt64.Type) throws -> UInt64 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: T.Type) throws -> T where T: Decodable { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func nestedContainer( - keyedBy type: NestedKey.Type - ) throws -> KeyedDecodingContainer where NestedKey: CodingKey { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func nestedUnkeyedContainer() throws -> any UnkeyedDecodingContainer { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func superDecoder() throws -> any Decoder { - decoder - } - -} - -extension LosslessStringConvertibleDecoder.KeyedContainer: KeyedDecodingContainerProtocol { - - var codingPath: [any CodingKey] { - [] - } - - var allKeys: [Key] { - [] - } - - func contains(_ key: Key) -> Bool { - false - } - - func decodeNil(forKey key: Key) throws -> Bool { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: String.Type, forKey key: Key) throws -> String { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: Double.Type, forKey key: Key) throws -> Double { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: Float.Type, forKey key: Key) throws -> Float { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: Int.Type, forKey key: Key) throws -> Int { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func nestedContainer( - keyedBy type: NestedKey.Type, - forKey key: Key - ) throws -> KeyedDecodingContainer where NestedKey: CodingKey { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func nestedUnkeyedContainer(forKey key: Key) throws -> any UnkeyedDecodingContainer { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func superDecoder() throws -> any Decoder { - decoder - } - - func superDecoder(forKey key: Key) throws -> any Decoder { - decoder - } -} diff --git a/Sources/OpenAPIRuntime/StringCoder/StringEncoder.swift b/Sources/OpenAPIRuntime/StringCoder/StringEncoder.swift deleted file mode 100644 index ba3fc943..00000000 --- a/Sources/OpenAPIRuntime/StringCoder/StringEncoder.swift +++ /dev/null @@ -1,446 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 - -/// A type that encodes an `Encodable` objects to a string, if it conforms -/// to `CustomStringConvertible`. -struct StringEncoder: Sendable { - - /// The coder used to serialize Date values. - let dateTranscoder: any DateTranscoder -} - -extension StringEncoder { - - /// Attempt to encode a value into a string using `CustomStringConvertible`. - /// - /// - Parameters: - /// - value: The value to encode. - /// - Returns: The encoded string. - func encode(_ value: some Encodable) throws -> String { - let encoder = CustomStringConvertibleEncoder( - dateTranscoder: dateTranscoder - ) - - // We have to catch the special values early, otherwise we fall - // back to their Codable implementations, which don't give us - // a chance to customize the coding in the containers. - if let date = value as? Date { - var container = encoder.singleValueContainer() - try container.encode(date) - } else { - try value.encode(to: encoder) - } - - return try encoder.nonNilEncodedString() - } -} - -/// The encoded used by `StringEncoder`. -private final class CustomStringConvertibleEncoder { - - /// The coder used to serialize Date values. - let dateTranscoder: any DateTranscoder - - /// The underlying encoded string. - /// - /// Nil before the encoder set the value. - private(set) var encodedString: String? - - /// Creates a new encoder. - /// - Parameter dateTranscoder: The coder used to serialize Date values. - init(dateTranscoder: any DateTranscoder) { - self.dateTranscoder = dateTranscoder - self.encodedString = nil - } -} - -extension CustomStringConvertibleEncoder { - - /// An encoder error. - enum EncoderError: Swift.Error { - - /// No value was set during the `encode(to:)` of the provided value. - case valueNotSet - - /// The encoder set a nil values, which is not supported. - case nilNotSupported - - /// The encoder encoded a container, which is not supported. - case containersNotSupported - - /// The encoder set a value multiple times, which is not supported. - case cannotEncodeMultipleValues - } - - /// Sets the provided value as the underlying string. - /// - Parameter value: The encoded string. - /// - Throws: An error if a value was already set previously. - func setEncodedString(_ value: String) throws { - guard encodedString == nil else { - throw EncoderError.cannotEncodeMultipleValues - } - encodedString = value - } - - /// Checks that the underlying string was set, and returns it. - /// - Returns: The underlying string. - /// - Throws: If the underlying string is nil. - func nonNilEncodedString() throws -> String { - guard let encodedString else { - throw EncoderError.valueNotSet - } - return encodedString - } -} - -extension CustomStringConvertibleEncoder: Encoder { - - var codingPath: [any CodingKey] { - [] - } - - var userInfo: [CodingUserInfoKey: Any] { - [:] - } - - func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key: CodingKey { - KeyedEncodingContainer(CustomStringConvertibleEncoder.KeyedContainer(encoder: self)) - } - - func unkeyedContainer() -> any UnkeyedEncodingContainer { - CustomStringConvertibleEncoder.UnkeyedContainer(encoder: self) - } - - func singleValueContainer() -> any SingleValueEncodingContainer { - SingleValueContainer(encoder: self) - } -} - -extension CustomStringConvertibleEncoder { - - /// A single value container used by `CustomStringConvertibleEncoder`. - struct SingleValueContainer { - - /// The underlying encoder. - let encoder: CustomStringConvertibleEncoder - - /// Converts the provided value to string and sets the result as the - /// underlying encoder's encoded value. - /// - Parameter value: The value to be encoded. - mutating func _encodeCustomStringConvertible(_ value: some CustomStringConvertible) throws { - try encoder.setEncodedString(value.description) - } - } - - /// An unkeyed container used by `CustomStringConvertibleEncoder`. - struct UnkeyedContainer { - - /// The underlying encoder. - let encoder: CustomStringConvertibleEncoder - } - - /// A keyed container used by `CustomStringConvertibleEncoder`. - struct KeyedContainer { - - /// The underlying encoder. - let encoder: CustomStringConvertibleEncoder - } -} - -extension CustomStringConvertibleEncoder.SingleValueContainer: SingleValueEncodingContainer { - - var codingPath: [any CodingKey] { - [] - } - - mutating func encodeNil() throws { - throw CustomStringConvertibleEncoder.EncoderError.nilNotSupported - } - - mutating func encode(_ value: Bool) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: String) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: Double) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: Float) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: Int) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: Int8) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: Int16) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: Int32) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: Int64) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: UInt) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: UInt8) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: UInt16) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: UInt32) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: UInt64) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: T) throws where T: Encodable { - switch value { - case let value as UInt8: - try encode(value) - case let value as Int8: - try encode(value) - case let value as UInt16: - try encode(value) - case let value as Int16: - try encode(value) - case let value as UInt32: - try encode(value) - case let value as Int32: - try encode(value) - case let value as UInt64: - try encode(value) - case let value as Int64: - try encode(value) - case let value as Int: - try encode(value) - case let value as UInt: - try encode(value) - case let value as Float: - try encode(value) - case let value as Double: - try encode(value) - case let value as String: - try encode(value) - case let value as Bool: - try encode(value) - case let value as Date: - try _encodeCustomStringConvertible(encoder.dateTranscoder.encode(value)) - default: - guard let customStringConvertible = value as? any CustomStringConvertible else { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - try _encodeCustomStringConvertible(customStringConvertible) - } - } -} - -extension CustomStringConvertibleEncoder.UnkeyedContainer: UnkeyedEncodingContainer { - - var codingPath: [any CodingKey] { - [] - } - - var count: Int { - 0 - } - - mutating func encodeNil() throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Bool) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: String) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Double) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Float) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int8) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int16) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int32) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int64) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt8) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt16) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt32) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt64) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: T) throws where T: Encodable { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer - where NestedKey: CodingKey { - encoder.container(keyedBy: NestedKey.self) - } - - mutating func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { - encoder.unkeyedContainer() - } - - mutating func superEncoder() -> any Encoder { - encoder - } -} - -extension CustomStringConvertibleEncoder.KeyedContainer: KeyedEncodingContainerProtocol { - - var codingPath: [any CodingKey] { - [] - } - - mutating func superEncoder() -> any Encoder { - encoder - } - - mutating func encodeNil(forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Bool, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: String, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Double, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Float, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int8, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int16, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int32, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int64, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt8, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt16, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt32, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt64, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: T, forKey key: Key) throws where T: Encodable { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func nestedContainer( - keyedBy keyType: NestedKey.Type, - forKey key: Key - ) -> KeyedEncodingContainer where NestedKey: CodingKey { - encoder.container(keyedBy: NestedKey.self) - } - - mutating func nestedUnkeyedContainer(forKey key: Key) -> any UnkeyedEncodingContainer { - encoder.unkeyedContainer() - } - - mutating func superEncoder(forKey key: Key) -> any Encoder { - encoder - } -} diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift index a2f25115..44e333b2 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -11,300 +11,215 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// -//import XCTest -//@_spi(Generated)@testable import OpenAPIRuntime -// -//final class Test_ClientConverterExtensions: Test_Runtime { -// -// func test_setAcceptHeader() throws { -// var headerFields: [HeaderField] = [] -// converter.setAcceptHeader( -// in: &headerFields, -// contentTypes: [.init(contentType: TestAcceptable.json, quality: 0.8)] -// ) -// XCTAssertEqual( -// headerFields, -// [ -// .init(name: "accept", value: "application/json; q=0.800") -// ] -// ) -// } -// -// // MARK: Converter helper methods -// -// // | client | set | request path | URI | required | renderedPath | -// func test_renderedPath_string() throws { -// let renderedPath = try converter.renderedPath( -// template: "/items/{}/detail/{}/habitats/{}", -// parameters: [ -// 1 as Int, -// "foo" as String, -// [.land, .air] as [TestHabitat], -// ] -// ) -// XCTAssertEqual(renderedPath, "/items/1/detail/foo/habitats/land,air") -// } -// -// // | client | set | request query | URI | both | setQueryItemAsURI | -// func test_setQueryItemAsURI_string() throws { -// var request = testRequest -// try converter.setQueryItemAsURI( -// in: &request, -// style: nil, -// explode: nil, -// name: "search", -// value: "foo" -// ) -// XCTAssertEqual(request.query, "search=foo") -// } -// -// func test_setQueryItemAsURI_stringConvertible_needsEncoding() throws { -// var request = testRequest -// try converter.setQueryItemAsURI( -// in: &request, -// style: nil, -// explode: nil, -// name: "search", -// value: "h%llo" -// ) -// XCTAssertEqual(request.query, "search=h%25llo") -// } -// -// func test_setQueryItemAsURI_arrayOfStrings() throws { -// var request = testRequest -// try converter.setQueryItemAsURI( -// in: &request, -// style: nil, -// explode: nil, -// name: "search", -// value: ["foo", "bar"] -// ) -// XCTAssertEqual(request.query, "search=foo&search=bar") -// } -// -// func test_setQueryItemAsURI_arrayOfStrings_unexploded() throws { -// var request = testRequest -// try converter.setQueryItemAsURI( -// in: &request, -// style: nil, -// explode: false, -// name: "search", -// value: ["foo", "bar"] -// ) -// XCTAssertEqual(request.query, "search=foo,bar") -// } -// -// func test_setQueryItemAsURI_date() throws { -// var request = testRequest -// try converter.setQueryItemAsURI( -// in: &request, -// style: nil, -// explode: nil, -// name: "search", -// value: testDate -// ) -// XCTAssertEqual(request.query, "search=2023-01-18T10%3A04%3A11Z") -// } -// -// func test_setQueryItemAsURI_arrayOfDates() throws { -// var request = testRequest -// try converter.setQueryItemAsURI( -// in: &request, -// style: nil, -// explode: nil, -// name: "search", -// value: [testDate, testDate] -// ) -// XCTAssertEqual(request.query, "search=2023-01-18T10%3A04%3A11Z&search=2023-01-18T10%3A04%3A11Z") -// } -// -// // | client | set | request body | string | optional | setOptionalRequestBodyAsString | -// func test_setOptionalRequestBodyAsString_string() throws { -// var headerFields: [HeaderField] = [] -// let body = try converter.setOptionalRequestBodyAsString( -// testString, -// headerFields: &headerFields, -// contentType: "text/plain" -// ) -// XCTAssertEqual(body, testStringData) -// XCTAssertEqual( -// headerFields, -// [ -// .init(name: "content-type", value: "text/plain") -// ] -// ) -// } -// -// // | client | set | request body | string | required | setRequiredRequestBodyAsString | -// func test_setRequiredRequestBodyAsString_string() throws { -// var headerFields: [HeaderField] = [] -// let body = try converter.setRequiredRequestBodyAsString( -// testString, -// headerFields: &headerFields, -// contentType: "text/plain" -// ) -// XCTAssertEqual(body, testStringData) -// XCTAssertEqual( -// headerFields, -// [ -// .init(name: "content-type", value: "text/plain") -// ] -// ) -// } -// -// func test_setOptionalRequestBodyAsString_date() throws { -// var headerFields: [HeaderField] = [] -// let body = try converter.setOptionalRequestBodyAsString( -// testDate, -// headerFields: &headerFields, -// contentType: "text/plain" -// ) -// XCTAssertEqual(body, testDateStringData) -// XCTAssertEqual( -// headerFields, -// [ -// .init(name: "content-type", value: "text/plain") -// ] -// ) -// } -// -// func test_setRequiredRequestBodyAsString_date() throws { -// var headerFields: [HeaderField] = [] -// let body = try converter.setRequiredRequestBodyAsString( -// testDate, -// headerFields: &headerFields, -// contentType: "text/plain" -// ) -// XCTAssertEqual(body, testDateStringData) -// XCTAssertEqual( -// headerFields, -// [ -// .init(name: "content-type", value: "text/plain") -// ] -// ) -// } -// -// // | client | set | request body | JSON | optional | setOptionalRequestBodyAsJSON | -// func test_setOptionalRequestBodyAsJSON_codable() throws { -// var headerFields: [HeaderField] = [] -// let body = try converter.setOptionalRequestBodyAsJSON( -// testStruct, -// headerFields: &headerFields, -// contentType: "application/json" -// ) -// XCTAssertEqual(body, testStructPrettyData) -// XCTAssertEqual( -// headerFields, -// [ -// .init(name: "content-type", value: "application/json") -// ] -// ) -// } -// -// func test_setOptionalRequestBodyAsJSON_codable_string() throws { -// var headerFields: [HeaderField] = [] -// let body = try converter.setOptionalRequestBodyAsJSON( -// testString, -// headerFields: &headerFields, -// contentType: "application/json" -// ) -// XCTAssertEqual(body, testQuotedStringData) -// XCTAssertEqual( -// headerFields, -// [ -// .init(name: "content-type", value: "application/json") -// ] -// ) -// } -// -// // | client | set | request body | JSON | required | setRequiredRequestBodyAsJSON | -// func test_setRequiredRequestBodyAsJSON_codable() throws { -// var headerFields: [HeaderField] = [] -// let body = try converter.setRequiredRequestBodyAsJSON( -// testStruct, -// headerFields: &headerFields, -// contentType: "application/json" -// ) -// XCTAssertEqual(body, testStructPrettyData) -// XCTAssertEqual( -// headerFields, -// [ -// .init(name: "content-type", value: "application/json") -// ] -// ) -// } -// -// // | client | set | request body | binary | optional | setOptionalRequestBodyAsBinary | -// func test_setOptionalRequestBodyAsBinary_data() throws { -// var headerFields: [HeaderField] = [] -// let body = try converter.setOptionalRequestBodyAsBinary( -// testStringData, -// headerFields: &headerFields, -// contentType: "application/octet-stream" -// ) -// XCTAssertEqual(body, testStringData) -// XCTAssertEqual( -// headerFields, -// [ -// .init(name: "content-type", value: "application/octet-stream") -// ] -// ) -// } -// -// // | client | set | request body | binary | required | setRequiredRequestBodyAsBinary | -// func test_setRequiredRequestBodyAsBinary_data() throws { -// var headerFields: [HeaderField] = [] -// let body = try converter.setRequiredRequestBodyAsBinary( -// testStringData, -// headerFields: &headerFields, -// contentType: "application/octet-stream" -// ) -// XCTAssertEqual(body, testStringData) -// XCTAssertEqual( -// headerFields, -// [ -// .init(name: "content-type", value: "application/octet-stream") -// ] -// ) -// } -// -// // | client | get | response body | string | required | getResponseBodyAsString | -// func test_getResponseBodyAsString_stringConvertible() throws { -// let value: String = try converter.getResponseBodyAsString( -// String.self, -// from: testStringData, -// transforming: { $0 } -// ) -// XCTAssertEqual(value, testString) -// } -// -// // | client | get | response body | string | required | getResponseBodyAsString | -// func test_getResponseBodyAsString_date() throws { -// let value: Date = try converter.getResponseBodyAsString( -// Date.self, -// from: testDateStringData, -// transforming: { $0 } -// ) -// XCTAssertEqual(value, testDate) -// } -// -// // | client | get | response body | JSON | required | getResponseBodyAsJSON | -// func test_getResponseBodyAsJSON_codable() throws { -// let value: TestPet = try converter.getResponseBodyAsJSON( -// TestPet.self, -// from: testStructData, -// transforming: { $0 } -// ) -// XCTAssertEqual(value, testStruct) -// } -// -// // | client | get | response body | binary | required | getResponseBodyAsBinary | -// func test_getResponseBodyAsBinary_data() throws { -// let value: Data = try converter.getResponseBodyAsBinary( -// Data.self, -// from: testStringData, -// transforming: { $0 } -// ) -// XCTAssertEqual(value, testStringData) -// } -//} +import XCTest +@_spi(Generated)@testable import OpenAPIRuntime +import HTTPTypes + +final class Test_ClientConverterExtensions: Test_Runtime { + + func test_setAcceptHeader() throws { + var headerFields: HTTPFields = [:] + converter.setAcceptHeader( + in: &headerFields, + contentTypes: [.init(contentType: TestAcceptable.json, quality: 0.8)] + ) + XCTAssertEqual( + headerFields, + [ + .accept: "application/json; q=0.800" + ] + ) + } + + // MARK: Converter helper methods + + // | client | set | request path | URI | required | renderedPath | + func test_renderedPath_string() throws { + let renderedPath = try converter.renderedPath( + template: "/items/{}/detail/{}/habitats/{}", + parameters: [ + 1 as Int, + "foo" as String, + [.land, .air] as [TestHabitat], + ] + ) + XCTAssertEqual(renderedPath, "/items/1/detail/foo/habitats/land,air") + } + + // | client | set | request query | URI | both | setQueryItemAsURI | + func test_setQueryItemAsURI_string() throws { + var request = testRequest + try converter.setQueryItemAsURI( + in: &request, + style: nil, + explode: nil, + name: "search", + value: "foo" + ) + XCTAssertEqual(request.query, "search=foo") + } + + func test_setQueryItemAsURI_stringConvertible_needsEncoding() throws { + var request = testRequest + try converter.setQueryItemAsURI( + in: &request, + style: nil, + explode: nil, + name: "search", + value: "h%llo" + ) + XCTAssertEqual(request.query, "search=h%25llo") + } + + func test_setQueryItemAsURI_arrayOfStrings() throws { + var request = testRequest + try converter.setQueryItemAsURI( + in: &request, + style: nil, + explode: nil, + name: "search", + value: ["foo", "bar"] + ) + XCTAssertEqual(request.query, "search=foo&search=bar") + } + + func test_setQueryItemAsURI_arrayOfStrings_unexploded() throws { + var request = testRequest + try converter.setQueryItemAsURI( + in: &request, + style: nil, + explode: false, + name: "search", + value: ["foo", "bar"] + ) + XCTAssertEqual(request.query, "search=foo,bar") + } + + func test_setQueryItemAsURI_date() throws { + var request = testRequest + try converter.setQueryItemAsURI( + in: &request, + style: nil, + explode: nil, + name: "search", + value: testDate + ) + XCTAssertEqual(request.query, "search=2023-01-18T10%3A04%3A11Z") + } + + func test_setQueryItemAsURI_arrayOfDates() throws { + var request = testRequest + try converter.setQueryItemAsURI( + in: &request, + style: nil, + explode: nil, + name: "search", + value: [testDate, testDate] + ) + XCTAssertEqual(request.query, "search=2023-01-18T10%3A04%3A11Z&search=2023-01-18T10%3A04%3A11Z") + } + + // | client | set | request body | JSON | optional | setOptionalRequestBodyAsJSON | + func test_setOptionalRequestBodyAsJSON_codable() async throws { + var headerFields: HTTPFields = [:] + let body = try converter.setOptionalRequestBodyAsJSON( + testStruct, + headerFields: &headerFields, + contentType: "application/json" + ) + try await XCTAssertEqualStringifiedData(body, testStructPrettyString) + XCTAssertEqual( + headerFields, + [ + .contentType: "application/json" + ] + ) + } + + func test_setOptionalRequestBodyAsJSON_codable_string() async throws { + var headerFields: HTTPFields = [:] + let body = try converter.setOptionalRequestBodyAsJSON( + testString, + headerFields: &headerFields, + contentType: "application/json" + ) + try await XCTAssertEqualStringifiedData(body, testQuotedString) + XCTAssertEqual( + headerFields, + [ + .contentType: "application/json" + ] + ) + } + + // | client | set | request body | JSON | required | setRequiredRequestBodyAsJSON | + func test_setRequiredRequestBodyAsJSON_codable() async throws { + var headerFields: HTTPFields = [:] + let body = try converter.setRequiredRequestBodyAsJSON( + testStruct, + headerFields: &headerFields, + contentType: "application/json" + ) + try await XCTAssertEqualStringifiedData(body, testStructPrettyString) + XCTAssertEqual( + headerFields, + [ + .contentType: "application/json" + ] + ) + } + + // | client | set | request body | binary | optional | setOptionalRequestBodyAsBinary | + func test_setOptionalRequestBodyAsBinary_data() async throws { + var headerFields: HTTPFields = [:] + let body = try converter.setOptionalRequestBodyAsBinary( + .init(data: testStringData), + headerFields: &headerFields, + contentType: "application/octet-stream" + ) + try await XCTAssertEqualStringifiedData(body, testString) + XCTAssertEqual( + headerFields, + [ + .contentType: "application/octet-stream" + ] + ) + } + + // | client | set | request body | binary | required | setRequiredRequestBodyAsBinary | + func test_setRequiredRequestBodyAsBinary_data() async throws { + var headerFields: HTTPFields = [:] + let body = try converter.setRequiredRequestBodyAsBinary( + .init(string: testString), + headerFields: &headerFields, + contentType: "application/octet-stream" + ) + try await XCTAssertEqualStringifiedData(body, testString) + XCTAssertEqual( + headerFields, + [ + .contentType: "application/octet-stream" + ] + ) + } + + // | client | get | response body | JSON | required | getResponseBodyAsJSON | + func test_getResponseBodyAsJSON_codable() async throws { + let value: TestPet = try await converter.getResponseBodyAsJSON( + TestPet.self, + from: .init(data: testStructData), + transforming: { $0 } + ) + XCTAssertEqual(value, testStruct) + } + + // | client | get | response body | binary | required | getResponseBodyAsBinary | + func test_getResponseBodyAsBinary_data() async throws { + let value: HTTPBody = try converter.getResponseBodyAsBinary( + HTTPBody.self, + from: .init(string: testString), + transforming: { $0 } + ) + try await XCTAssertEqualStringifiedData(value, testString) + } +} diff --git a/Tests/OpenAPIRuntimeTests/StringCoder/Test_StringCodingRoundtrip.swift b/Tests/OpenAPIRuntimeTests/StringCoder/Test_StringCodingRoundtrip.swift deleted file mode 100644 index f4ba5188..00000000 --- a/Tests/OpenAPIRuntimeTests/StringCoder/Test_StringCodingRoundtrip.swift +++ /dev/null @@ -1,132 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 XCTest -@testable import OpenAPIRuntime - -final class Test_StringCodingRoundtrip: Test_Runtime { - - func testRoundtrip() throws { - - enum SimpleEnum: String, Codable, Equatable { - case red - case green - case blue - } - - struct CustomValue: LosslessStringConvertible, Codable, Equatable { - var innerString: String - - init(innerString: String) { - self.innerString = innerString - } - - init?(_ description: String) { - self.init(innerString: description) - } - - var description: String { - innerString - } - - func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(innerString) - } - - enum CodingKeys: CodingKey { - case innerString - } - - init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - self.innerString = try container.decode(String.self) - } - } - - // An empty string. - try _test( - "", - "" - ) - - // An string with a space. - try _test( - "Hello World!", - "Hello World!" - ) - - // An enum. - try _test( - SimpleEnum.red, - "red" - ) - - // A custom value. - try _test( - CustomValue(innerString: "hello"), - "hello" - ) - - // An integer. - try _test( - 1234, - "1234" - ) - - // A float. - try _test( - 12.34, - "12.34" - ) - - // A bool. - try _test( - true, - "true" - ) - - // A Date. - try _test( - Date(timeIntervalSince1970: 1_692_948_899), - "2023-08-25T07:34:59Z" - ) - } - - func _test( - _ value: T, - _ expectedString: String, - file: StaticString = #file, - line: UInt = #line - ) throws { - let encoder = StringEncoder(dateTranscoder: .iso8601) - let encodedString = try encoder.encode(value) - XCTAssertEqual( - encodedString, - expectedString, - file: file, - line: line - ) - let decoder = StringDecoder(dateTranscoder: .iso8601) - let decodedValue = try decoder.decode( - T.self, - from: encodedString - ) - XCTAssertEqual( - decodedValue, - value, - file: file, - line: line - ) - } -} diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index 46e95be4..d62adc72 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -43,9 +43,9 @@ class Test_Runtime: XCTestCase { return components } - // var testRequest: OpenAPIRuntime.Request { - // .init(path: "/api", query: nil, method: .get) - // } + var testRequest: HTTPRequest { + .init(path: "/api", method: .get) + } var testDate: Date { Date(timeIntervalSince1970: 1_674_036_251) @@ -186,3 +186,33 @@ struct PrintingMiddleware: ClientMiddleware { } } } + +public func XCTAssertEqualStringifiedData( + _ expression1: @autoclosure () throws -> S?, + _ expression2: @autoclosure () throws -> String, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line +) where S.Element == UInt8 { + do { + guard let value1 = try expression1() else { + XCTFail("First value is nil", file: file, line: line) + return + } + let actualString = String(decoding: Array(value1), as: UTF8.self) + XCTAssertEqual(actualString, try expression2(), file: file, line: line) + } catch { + XCTFail(error.localizedDescription, file: file, line: line) + } +} + +public func XCTAssertEqualStringifiedData( + _ expression1: @autoclosure () throws -> HTTPBody?, + _ expression2: @autoclosure () throws -> String, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line +) async throws { + let data = try await expression1()?.collectAsData(upTo: .max) + XCTAssertEqualStringifiedData(data, try expression2(), message(), file: file, line: line) +} From 00ec1890648912dc2801d1225cc5554dea9970f8 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 5 Sep 2023 17:42:05 +0200 Subject: [PATCH 09/55] Got the other tests working --- .../Interface/UniversalServer.swift | 10 +- .../Conversion/Test_Converter+Common.swift | 511 +++++++------ .../Conversion/Test_Converter+Server.swift | 673 ++++++++---------- .../Interface/Test_UniversalServer.swift | 66 +- 4 files changed, 589 insertions(+), 671 deletions(-) diff --git a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift index eee7a816..4ca6d545 100644 --- a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift +++ b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift @@ -139,12 +139,16 @@ public struct UniversalServer: Sendable { public func apiPathComponentsWithServerPrefix( _ path: String ) throws -> String { - // Operation path is for example [pets, 42] + // Operation path is for example "/pets/42" // Server may be configured with a prefix, for example http://localhost/foo/bar/v1 - // Goal is to return something like [foo, bar, v1, pets, 42] + // Goal is to return something like "/foo/bar/v1/pets/42". guard let components = URLComponents(url: serverURL, resolvingAgainstBaseURL: false) else { throw RuntimeError.invalidServerURL(serverURL.absoluteString) } - return components.path + path + let prefixPath = components.path + guard prefixPath == "/" else { + return prefixPath + path + } + return path } } diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift index b42d47ff..b7e50cef 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift @@ -11,262 +11,255 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// -//import XCTest -//@_spi(Generated)@testable import OpenAPIRuntime -// -//final class Test_CommonConverterExtensions: Test_Runtime { -// -// // MARK: Miscs -// -// func testContentTypeMatching() throws { -// let cases: [(received: String, expected: String, isMatch: Bool)] = [ -// ("application/json", "application/json", true), -// ("APPLICATION/JSON", "application/json", true), -// ("application/json", "application/*", true), -// ("application/json", "*/*", true), -// ("application/json", "text/*", false), -// ("application/json", "application/xml", false), -// ("application/json", "text/plain", false), -// -// ("text/plain; charset=UTF-8", "text/plain", true), -// ("TEXT/PLAIN; CHARSET=UTF-8", "text/plain", true), -// ("text/plain; charset=UTF-8", "text/*", true), -// ("text/plain; charset=UTF-8", "*/*", true), -// ("text/plain; charset=UTF-8", "application/*", false), -// ("text/plain; charset=UTF-8", "text/html", false), -// ] -// for testCase in cases { -// XCTAssertEqual( -// try converter.isMatchingContentType( -// received: .init(testCase.received), -// expectedRaw: testCase.expected -// ), -// testCase.isMatch, -// "Wrong result for (\(testCase.received), \(testCase.expected), \(testCase.isMatch))" -// ) -// } -// } -// -// // MARK: Converter helper methods -// -// // | common | set | header field | URI | both | setHeaderFieldAsURI | -// func test_setHeaderFieldAsURI_string() throws { -// var headerFields: [HeaderField] = [] -// try converter.setHeaderFieldAsURI( -// in: &headerFields, -// name: "foo", -// value: "bar" -// ) -// XCTAssertEqual( -// headerFields, -// [ -// .init(name: "foo", value: "bar") -// ] -// ) -// } -// -// func test_setHeaderFieldAsURI_arrayOfStrings() throws { -// var headerFields: [HeaderField] = [] -// try converter.setHeaderFieldAsURI( -// in: &headerFields, -// name: "foo", -// value: ["bar", "baz"] as [String] -// ) -// XCTAssertEqual( -// headerFields, -// [ -// .init(name: "foo", value: "bar,baz") -// ] -// ) -// } -// -// func test_setHeaderFieldAsURI_date() throws { -// var headerFields: [HeaderField] = [] -// try converter.setHeaderFieldAsURI( -// in: &headerFields, -// name: "foo", -// value: testDate -// ) -// XCTAssertEqual( -// headerFields, -// [ -// .init(name: "foo", value: testDateEscapedString) -// ] -// ) -// } -// -// func test_setHeaderFieldAsURI_arrayOfDates() throws { -// var headerFields: [HeaderField] = [] -// try converter.setHeaderFieldAsURI( -// in: &headerFields, -// name: "foo", -// value: [testDate, testDate] -// ) -// XCTAssertEqual( -// headerFields, -// [ -// .init(name: "foo", value: "\(testDateEscapedString),\(testDateEscapedString)") -// ] -// ) -// } -// -// func test_setHeaderFieldAsURI_struct() throws { -// var headerFields: [HeaderField] = [] -// try converter.setHeaderFieldAsURI( -// in: &headerFields, -// name: "foo", -// value: testStruct -// ) -// XCTAssertEqual( -// headerFields, -// [ -// .init(name: "foo", value: "name,Fluffz") -// ] -// ) -// } -// -// // | common | set | header field | JSON | both | setHeaderFieldAsJSON | -// func test_setHeaderFieldAsJSON_codable() throws { -// var headerFields: [HeaderField] = [] -// try converter.setHeaderFieldAsJSON( -// in: &headerFields, -// name: "foo", -// value: testStruct -// ) -// XCTAssertEqual( -// headerFields, -// [ -// .init(name: "foo", value: testStructString) -// ] -// ) -// } -// -// func test_setHeaderFieldAsJSON_codable_string() throws { -// var headerFields: [HeaderField] = [] -// try converter.setHeaderFieldAsJSON( -// in: &headerFields, -// name: "foo", -// value: "hello" -// ) -// XCTAssertEqual( -// headerFields, -// [ -// .init(name: "foo", value: "\"hello\"") -// ] -// ) -// } -// -// // | common | get | header field | URI | optional | getOptionalHeaderFieldAsURI | -// func test_getOptionalHeaderFieldAsURI_string() throws { -// let headerFields: [HeaderField] = [ -// .init(name: "foo", value: "bar") -// ] -// let value: String? = try converter.getOptionalHeaderFieldAsURI( -// in: headerFields, -// name: "foo", -// as: String.self -// ) -// XCTAssertEqual(value, "bar") -// } -// -// // | common | get | header field | URI | required | getRequiredHeaderFieldAsURI | -// func test_getRequiredHeaderFieldAsURI_stringConvertible() throws { -// let headerFields: [HeaderField] = [ -// .init(name: "foo", value: "bar") -// ] -// let value: String = try converter.getRequiredHeaderFieldAsURI( -// in: headerFields, -// name: "foo", -// as: String.self -// ) -// XCTAssertEqual(value, "bar") -// } -// -// func test_getOptionalHeaderFieldAsURI_arrayOfStrings_splitHeaders() throws { -// let headerFields: [HeaderField] = [ -// .init(name: "foo", value: "bar"), -// .init(name: "foo", value: "baz"), -// ] -// let value: [String]? = try converter.getOptionalHeaderFieldAsURI( -// in: headerFields, -// name: "foo", -// as: [String].self -// ) -// XCTAssertEqual(value, ["bar", "baz"]) -// } -// -// func test_getOptionalHeaderFieldAsURI_arrayOfStrings_singleHeader() throws { -// let headerFields: [HeaderField] = [ -// .init(name: "foo", value: "bar,baz") -// ] -// let value: [String]? = try converter.getOptionalHeaderFieldAsURI( -// in: headerFields, -// name: "foo", -// as: [String].self -// ) -// XCTAssertEqual(value, ["bar", "baz"]) -// } -// -// func test_getOptionalHeaderFieldAsURI_date() throws { -// let headerFields: [HeaderField] = [ -// .init(name: "foo", value: testDateEscapedString) -// ] -// let value: Date? = try converter.getOptionalHeaderFieldAsURI( -// in: headerFields, -// name: "foo", -// as: Date.self -// ) -// XCTAssertEqual(value, testDate) -// } -// -// func test_getRequiredHeaderFieldAsURI_arrayOfDates() throws { -// let headerFields: [HeaderField] = [ -// .init(name: "foo", value: testDateString), // escaped -// .init(name: "foo", value: testDateEscapedString), // unescaped -// ] -// let value: [Date] = try converter.getRequiredHeaderFieldAsURI( -// in: headerFields, -// name: "foo", -// as: [Date].self -// ) -// XCTAssertEqual(value, [testDate, testDate]) -// } -// -// func test_getOptionalHeaderFieldAsURI_struct() throws { -// let headerFields: [HeaderField] = [ -// .init(name: "foo", value: "name,Sprinkles") -// ] -// let value: TestPet? = try converter.getOptionalHeaderFieldAsURI( -// in: headerFields, -// name: "foo", -// as: TestPet.self -// ) -// XCTAssertEqual(value, .init(name: "Sprinkles")) -// } -// -// // | common | get | header field | JSON | optional | getOptionalHeaderFieldAsJSON | -// func test_getOptionalHeaderFieldAsJSON_codable() throws { -// let headerFields: [HeaderField] = [ -// .init(name: "foo", value: testStructString) -// ] -// let value: TestPet? = try converter.getOptionalHeaderFieldAsJSON( -// in: headerFields, -// name: "foo", -// as: TestPet.self -// ) -// XCTAssertEqual(value, testStruct) -// } -// -// // | common | get | header field | JSON | required | getRequiredHeaderFieldAsJSON | -// func test_getRequiredHeaderFieldAsJSON_codable() throws { -// let headerFields: [HeaderField] = [ -// .init(name: "foo", value: testStructString) -// ] -// let value: TestPet = try converter.getRequiredHeaderFieldAsJSON( -// in: headerFields, -// name: "foo", -// as: TestPet.self -// ) -// XCTAssertEqual(value, testStruct) -// } -//} +import XCTest +@_spi(Generated)@testable import OpenAPIRuntime +import HTTPTypes + +extension HTTPField.Name { + static var foo: Self { + .init("foo")! + } +} + +final class Test_CommonConverterExtensions: Test_Runtime { + + // MARK: Miscs + + func testContentTypeMatching() throws { + let cases: [(received: String, expected: String, isMatch: Bool)] = [ + ("application/json", "application/json", true), + ("APPLICATION/JSON", "application/json", true), + ("application/json", "application/*", true), + ("application/json", "*/*", true), + ("application/json", "text/*", false), + ("application/json", "application/xml", false), + ("application/json", "text/plain", false), + + ("text/plain; charset=UTF-8", "text/plain", true), + ("TEXT/PLAIN; CHARSET=UTF-8", "text/plain", true), + ("text/plain; charset=UTF-8", "text/*", true), + ("text/plain; charset=UTF-8", "*/*", true), + ("text/plain; charset=UTF-8", "application/*", false), + ("text/plain; charset=UTF-8", "text/html", false), + ] + for testCase in cases { + XCTAssertEqual( + try converter.isMatchingContentType( + received: .init(testCase.received), + expectedRaw: testCase.expected + ), + testCase.isMatch, + "Wrong result for (\(testCase.received), \(testCase.expected), \(testCase.isMatch))" + ) + } + } + + // MARK: Converter helper methods + + // | common | set | header field | URI | both | setHeaderFieldAsURI | + func test_setHeaderFieldAsURI_string() throws { + var headerFields: HTTPFields = [:] + try converter.setHeaderFieldAsURI( + in: &headerFields, + name: "foo", + value: "bar" + ) + XCTAssertEqual( + headerFields, + [ + .foo: "bar" + ] + ) + } + + func test_setHeaderFieldAsURI_arrayOfStrings() throws { + var headerFields: HTTPFields = [:] + try converter.setHeaderFieldAsURI( + in: &headerFields, + name: "foo", + value: ["bar", "baz"] as [String] + ) + XCTAssertEqual( + headerFields, + [ + .foo: "bar,baz" + ] + ) + } + + func test_setHeaderFieldAsURI_date() throws { + var headerFields: HTTPFields = [:] + try converter.setHeaderFieldAsURI( + in: &headerFields, + name: "foo", + value: testDate + ) + XCTAssertEqual( + headerFields, + [ + .foo: testDateEscapedString + ] + ) + } + + func test_setHeaderFieldAsURI_arrayOfDates() throws { + var headerFields: HTTPFields = [:] + try converter.setHeaderFieldAsURI( + in: &headerFields, + name: "foo", + value: [testDate, testDate] + ) + XCTAssertEqual( + headerFields, + [ + .foo: "\(testDateEscapedString),\(testDateEscapedString)" + ] + ) + } + + func test_setHeaderFieldAsURI_struct() throws { + var headerFields: HTTPFields = [:] + try converter.setHeaderFieldAsURI( + in: &headerFields, + name: "foo", + value: testStruct + ) + XCTAssertEqual( + headerFields, + [ + .foo: "name,Fluffz" + ] + ) + } + + // | common | set | header field | JSON | both | setHeaderFieldAsJSON | + func test_setHeaderFieldAsJSON_codable() throws { + var headerFields: HTTPFields = [:] + try converter.setHeaderFieldAsJSON( + in: &headerFields, + name: "foo", + value: testStruct + ) + XCTAssertEqual( + headerFields, + [ + .foo: testStructString + ] + ) + } + + func test_setHeaderFieldAsJSON_codable_string() throws { + var headerFields: HTTPFields = [:] + try converter.setHeaderFieldAsJSON( + in: &headerFields, + name: "foo", + value: "hello" + ) + XCTAssertEqual( + headerFields, + [ + .foo: "\"hello\"" + ] + ) + } + + // | common | get | header field | URI | optional | getOptionalHeaderFieldAsURI | + func test_getOptionalHeaderFieldAsURI_string() throws { + let headerFields: HTTPFields = [ + .foo: "bar" + ] + let value: String? = try converter.getOptionalHeaderFieldAsURI( + in: headerFields, + name: "foo", + as: String.self + ) + XCTAssertEqual(value, "bar") + } + + // | common | get | header field | URI | required | getRequiredHeaderFieldAsURI | + func test_getRequiredHeaderFieldAsURI_stringConvertible() throws { + let headerFields: HTTPFields = [ + .foo: "bar" + ] + let value: String = try converter.getRequiredHeaderFieldAsURI( + in: headerFields, + name: "foo", + as: String.self + ) + XCTAssertEqual(value, "bar") + } + + func test_getOptionalHeaderFieldAsURI_arrayOfStrings_singleHeader() throws { + let headerFields: HTTPFields = [ + .foo: "bar,baz" + ] + let value: [String]? = try converter.getOptionalHeaderFieldAsURI( + in: headerFields, + name: "foo", + as: [String].self + ) + XCTAssertEqual(value, ["bar", "baz"]) + } + + func test_getOptionalHeaderFieldAsURI_date() throws { + let headerFields: HTTPFields = [ + .foo: testDateEscapedString + ] + let value: Date? = try converter.getOptionalHeaderFieldAsURI( + in: headerFields, + name: "foo", + as: Date.self + ) + XCTAssertEqual(value, testDate) + } + + func test_getRequiredHeaderFieldAsURI_arrayOfDates() throws { + let headerFields: HTTPFields = [ + .foo: "\(testDateString),\(testDateEscapedString)" // escaped and unescaped + ] + let value: [Date] = try converter.getRequiredHeaderFieldAsURI( + in: headerFields, + name: "foo", + as: [Date].self + ) + XCTAssertEqual(value, [testDate, testDate]) + } + + func test_getOptionalHeaderFieldAsURI_struct() throws { + let headerFields: HTTPFields = [ + .foo: "name,Sprinkles" + ] + let value: TestPet? = try converter.getOptionalHeaderFieldAsURI( + in: headerFields, + name: "foo", + as: TestPet.self + ) + XCTAssertEqual(value, .init(name: "Sprinkles")) + } + + // | common | get | header field | JSON | optional | getOptionalHeaderFieldAsJSON | + func test_getOptionalHeaderFieldAsJSON_codable() throws { + let headerFields: HTTPFields = [ + .foo: testStructString + ] + let value: TestPet? = try converter.getOptionalHeaderFieldAsJSON( + in: headerFields, + name: "foo", + as: TestPet.self + ) + XCTAssertEqual(value, testStruct) + } + + // | common | get | header field | JSON | required | getRequiredHeaderFieldAsJSON | + func test_getRequiredHeaderFieldAsJSON_codable() throws { + let headerFields: HTTPFields = [ + .foo: testStructString + ] + let value: TestPet = try converter.getRequiredHeaderFieldAsJSON( + in: headerFields, + name: "foo", + as: TestPet.self + ) + XCTAssertEqual(value, testStruct) + } +} diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift index 9f96b4ee..749d16f3 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift @@ -11,374 +11,305 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// -//import XCTest -//@_spi(Generated)@testable import OpenAPIRuntime -// -//final class Test_ServerConverterExtensions: Test_Runtime { -// -// func testExtractAccept() throws { -// let headerFields: [HeaderField] = [ -// .init(name: "accept", value: "application/json, */*; q=0.8") -// ] -// let accept: [AcceptHeaderContentType] = try converter.extractAcceptHeaderIfPresent( -// in: headerFields -// ) -// XCTAssertEqual( -// accept, -// [ -// .init(contentType: .json, quality: 1.0), -// .init(contentType: .other("*/*"), quality: 0.8), -// ] -// ) -// } -// -// // MARK: Miscs -// -// func testValidateAccept() throws { -// let emptyHeaders: [HeaderField] = [] -// let wildcard: [HeaderField] = [ -// .init(name: "accept", value: "*/*") -// ] -// let partialWildcard: [HeaderField] = [ -// .init(name: "accept", value: "text/*") -// ] -// let short: [HeaderField] = [ -// .init(name: "accept", value: "text/plain") -// ] -// let long: [HeaderField] = [ -// .init( -// name: "accept", -// value: "text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8" -// ) -// ] -// let multiple: [HeaderField] = [ -// .init(name: "accept", value: "text/plain"), -// .init(name: "accept", value: "application/json"), -// ] -// let cases: [([HeaderField], String, Bool)] = [ -// // No Accept header, any string validates successfully -// (emptyHeaders, "foobar", true), -// -// // Accept: */*, any string validates successfully -// (wildcard, "foobar", true), -// -// // Accept: text/*, so text/plain succeeds, application/json fails -// (partialWildcard, "text/plain", true), -// (partialWildcard, "application/json", false), -// -// // Accept: text/plain, text/plain succeeds, application/json fails -// (short, "text/plain", true), -// (short, "application/json", false), -// -// // A bunch of acceptable content types -// (long, "text/html", true), -// (long, "application/xhtml+xml", true), -// (long, "application/xml", true), -// (long, "image/webp", true), -// (long, "application/json", true), -// -// // Multiple values -// (multiple, "text/plain", true), -// (multiple, "application/json", true), -// (multiple, "application/xml", false), -// ] -// for (headers, contentType, success) in cases { -// if success { -// XCTAssertNoThrow( -// try converter.validateAcceptIfPresent( -// contentType, -// in: headers -// ), -// "Unexpected error when validating string: \(contentType) against headers: \(headers)" -// ) -// } else { -// let acceptHeader = -// headers -// .values(name: "accept") -// .joined(separator: ", ") -// XCTAssertThrowsError( -// try converter.validateAcceptIfPresent( -// contentType, -// in: headers -// ), -// "Expected to throw error when validating string: \(contentType) against headers: \(headers)", -// { error in -// guard -// let err = error as? RuntimeError, -// case .unexpectedAcceptHeader(let string) = err -// else { -// XCTFail("Threw an unexpected error: \(error)") -// return -// } -// XCTAssertEqual(string, acceptHeader) -// } -// ) -// } -// } -// } -// -// // MARK: Converter helper methods -// -// // | server | get | request path | URI | required | getPathParameterAsURI | -// func test_getPathParameterAsURI_various() throws { -// let path: [String: String] = [ -// "foo": "bar", -// "number": "1", -// "habitats": "land,air", -// ] -// do { -// let value = try converter.getPathParameterAsURI( -// in: path, -// name: "foo", -// as: String.self -// ) -// XCTAssertEqual(value, "bar") -// } -// do { -// let value = try converter.getPathParameterAsURI( -// in: path, -// name: "number", -// as: Int.self -// ) -// XCTAssertEqual(value, 1) -// } -// do { -// let value = try converter.getPathParameterAsURI( -// in: path, -// name: "habitats", -// as: [TestHabitat].self -// ) -// XCTAssertEqual(value, [.land, .air]) -// } -// } -// -// // | server | get | request query | URI | optional | getOptionalQueryItemAsURI | -// func test_getOptionalQueryItemAsURI_string() throws { -// let value: String? = try converter.getOptionalQueryItemAsURI( -// in: "search=foo", -// style: nil, -// explode: nil, -// name: "search", -// as: String.self -// ) -// XCTAssertEqual(value, "foo") -// } -// -// // | server | get | request query | URI | required | getRequiredQueryItemAsURI | -// func test_getRequiredQueryItemAsURI_string() throws { -// let value: String = try converter.getRequiredQueryItemAsURI( -// in: "search=foo", -// style: nil, -// explode: nil, -// name: "search", -// as: String.self -// ) -// XCTAssertEqual(value, "foo") -// } -// -// func test_getOptionalQueryItemAsURI_arrayOfStrings() throws { -// let query = "search=foo&search=bar" -// let value: [String]? = try converter.getOptionalQueryItemAsURI( -// in: query, -// style: nil, -// explode: nil, -// name: "search", -// as: [String].self -// ) -// XCTAssertEqual(value, ["foo", "bar"]) -// } -// -// func test_getRequiredQueryItemAsURI_arrayOfStrings() throws { -// let query = "search=foo&search=bar" -// let value: [String] = try converter.getRequiredQueryItemAsURI( -// in: query, -// style: nil, -// explode: nil, -// name: "search", -// as: [String].self -// ) -// XCTAssertEqual(value, ["foo", "bar"]) -// } -// -// func test_getRequiredQueryItemAsURI_arrayOfStrings_unexploded() throws { -// let query = "search=foo,bar" -// let value: [String] = try converter.getRequiredQueryItemAsURI( -// in: query, -// style: nil, -// explode: false, -// name: "search", -// as: [String].self -// ) -// XCTAssertEqual(value, ["foo", "bar"]) -// } -// -// func test_getOptionalQueryItemAsURI_date() throws { -// let query = "search=\(testDateEscapedString)" -// let value: Date? = try converter.getOptionalQueryItemAsURI( -// in: query, -// style: nil, -// explode: nil, -// name: "search", -// as: Date.self -// ) -// XCTAssertEqual(value, testDate) -// } -// -// func test_getRequiredQueryItemAsURI_arrayOfDates() throws { -// let query = "search=\(testDateEscapedString)&search=\(testDateEscapedString)" -// let value: [Date] = try converter.getRequiredQueryItemAsURI( -// in: query, -// style: nil, -// explode: nil, -// name: "search", -// as: [Date].self -// ) -// XCTAssertEqual(value, [testDate, testDate]) -// } -// -// // | server | get | request body | string | optional | getOptionalRequestBodyAsString | -// func test_getOptionalRequestBodyAsText_string() throws { -// let body: String? = try converter.getOptionalRequestBodyAsString( -// String.self, -// from: testStringData, -// transforming: { $0 } -// ) -// XCTAssertEqual(body, testString) -// } -// -// // | server | get | request body | string | required | getRequiredRequestBodyAsString | -// func test_getRequiredRequestBodyAsText_stringConvertible() throws { -// let body: String = try converter.getRequiredRequestBodyAsString( -// String.self, -// from: testStringData, -// transforming: { $0 } -// ) -// XCTAssertEqual(body, testString) -// } -// -// func test_getRequiredRequestBodyAsText_date() throws { -// let body: Date = try converter.getRequiredRequestBodyAsString( -// Date.self, -// from: testDateStringData, -// transforming: { $0 } -// ) -// XCTAssertEqual(body, testDate) -// } -// -// // | server | get | request body | JSON | optional | getOptionalRequestBodyAsJSON | -// func test_getOptionalRequestBodyAsJSON_codable() throws { -// let body: TestPet? = try converter.getOptionalRequestBodyAsJSON( -// TestPet.self, -// from: testStructData, -// transforming: { $0 } -// ) -// XCTAssertEqual(body, testStruct) -// } -// -// func test_getOptionalRequestBodyAsJSON_codable_string() throws { -// let body: String? = try converter.getOptionalRequestBodyAsJSON( -// String.self, -// from: testQuotedStringData, -// transforming: { $0 } -// ) -// XCTAssertEqual(body, testString) -// } -// -// // | server | get | request body | JSON | required | getRequiredRequestBodyAsJSON | -// func test_getRequiredRequestBodyAsJSON_codable() throws { -// let body: TestPet = try converter.getRequiredRequestBodyAsJSON( -// TestPet.self, -// from: testStructData, -// transforming: { $0 } -// ) -// XCTAssertEqual(body, testStruct) -// } -// -// // | server | get | request body | binary | optional | getOptionalRequestBodyAsBinary | -// func test_getOptionalRequestBodyAsBinary_data() throws { -// let body: Data? = try converter.getOptionalRequestBodyAsBinary( -// Data.self, -// from: testStringData, -// transforming: { $0 } -// ) -// XCTAssertEqual(body, testStringData) -// } -// -// // | server | get | request body | binary | required | getRequiredRequestBodyAsBinary | -// func test_getRequiredRequestBodyAsBinary_data() throws { -// let body: Data = try converter.getRequiredRequestBodyAsBinary( -// Data.self, -// from: testStringData, -// transforming: { $0 } -// ) -// XCTAssertEqual(body, testStringData) -// } -// -// // | server | set | response body | string | required | setResponseBodyAsString | -// func test_setResponseBodyAsText_stringConvertible() throws { -// var headers: [HeaderField] = [] -// let data = try converter.setResponseBodyAsString( -// testString, -// headerFields: &headers, -// contentType: "text/plain" -// ) -// XCTAssertEqual(data, testStringData) -// XCTAssertEqual( -// headers, -// [ -// .init(name: "content-type", value: "text/plain") -// ] -// ) -// } -// -// // | server | set | response body | string | required | setResponseBodyAsString | -// func test_setResponseBodyAsText_date() throws { -// var headers: [HeaderField] = [] -// let data = try converter.setResponseBodyAsString( -// testDate, -// headerFields: &headers, -// contentType: "text/plain" -// ) -// XCTAssertEqual(data, testDateStringData) -// XCTAssertEqual( -// headers, -// [ -// .init(name: "content-type", value: "text/plain") -// ] -// ) -// } -// -// // | server | set | response body | JSON | required | setResponseBodyAsJSON | -// func test_setResponseBodyAsJSON_codable() throws { -// var headers: [HeaderField] = [] -// let data = try converter.setResponseBodyAsJSON( -// testStruct, -// headerFields: &headers, -// contentType: "application/json" -// ) -// XCTAssertEqual(data, testStructPrettyData) -// XCTAssertEqual( -// headers, -// [ -// .init(name: "content-type", value: "application/json") -// ] -// ) -// } -// -// // | server | set | response body | binary | required | setResponseBodyAsBinary | -// func test_setResponseBodyAsBinary_data() throws { -// var headers: [HeaderField] = [] -// let data = try converter.setResponseBodyAsBinary( -// testStringData, -// headerFields: &headers, -// contentType: "application/octet-stream" -// ) -// XCTAssertEqual(data, testStringData) -// XCTAssertEqual( -// headers, -// [ -// .init(name: "content-type", value: "application/octet-stream") -// ] -// ) -// } -//} +import XCTest +@_spi(Generated)@testable import OpenAPIRuntime +import HTTPTypes + +final class Test_ServerConverterExtensions: Test_Runtime { + + func testExtractAccept() throws { + let headerFields: HTTPFields = [ + .accept: "application/json, */*; q=0.8" + ] + let accept: [AcceptHeaderContentType] = try converter.extractAcceptHeaderIfPresent( + in: headerFields + ) + XCTAssertEqual( + accept, + [ + .init(contentType: .json, quality: 1.0), + .init(contentType: .other("*/*"), quality: 0.8), + ] + ) + } + + // MARK: Miscs + + func testValidateAccept() throws { + let emptyHeaders: HTTPFields = [:] + let wildcard: HTTPFields = [ + .accept: "*/*" + ] + let partialWildcard: HTTPFields = [ + .accept: "text/*" + ] + let short: HTTPFields = [ + .accept: "text/plain" + ] + let long: HTTPFields = [ + .accept: "text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8" + ] + let multiple: HTTPFields = [ + .accept: "text/plain, application/json" + ] + let cases: [(HTTPFields, String, Bool)] = [ + // No Accept header, any string validates successfully + (emptyHeaders, "foobar", true), + + // Accept: */*, any string validates successfully + (wildcard, "foobar", true), + + // Accept: text/*, so text/plain succeeds, application/json fails + (partialWildcard, "text/plain", true), + (partialWildcard, "application/json", false), + + // Accept: text/plain, text/plain succeeds, application/json fails + (short, "text/plain", true), + (short, "application/json", false), + + // A bunch of acceptable content types + (long, "text/html", true), + (long, "application/xhtml+xml", true), + (long, "application/xml", true), + (long, "image/webp", true), + (long, "application/json", true), + + // Multiple values + (multiple, "text/plain", true), + (multiple, "application/json", true), + (multiple, "application/xml", false), + ] + for (headers, contentType, success) in cases { + if success { + XCTAssertNoThrow( + try converter.validateAcceptIfPresent( + contentType, + in: headers + ), + "Unexpected error when validating string: \(contentType) against headers: \(headers)" + ) + } else { + let acceptHeader = try XCTUnwrap(headers[.accept]) + XCTAssertThrowsError( + try converter.validateAcceptIfPresent( + contentType, + in: headers + ), + "Expected to throw error when validating string: \(contentType) against headers: \(headers)", + { error in + guard + let err = error as? RuntimeError, + case .unexpectedAcceptHeader(let string) = err + else { + XCTFail("Threw an unexpected error: \(error)") + return + } + XCTAssertEqual(string, acceptHeader) + } + ) + } + } + } + + // MARK: Converter helper methods + + // | server | get | request path | URI | required | getPathParameterAsURI | + func test_getPathParameterAsURI_various() throws { + let path: [String: Substring] = [ + "foo": "bar", + "number": "1", + "habitats": "land,air", + ] + do { + let value = try converter.getPathParameterAsURI( + in: path, + name: "foo", + as: String.self + ) + XCTAssertEqual(value, "bar") + } + do { + let value = try converter.getPathParameterAsURI( + in: path, + name: "number", + as: Int.self + ) + XCTAssertEqual(value, 1) + } + do { + let value = try converter.getPathParameterAsURI( + in: path, + name: "habitats", + as: [TestHabitat].self + ) + XCTAssertEqual(value, [.land, .air]) + } + } + + // | server | get | request query | URI | optional | getOptionalQueryItemAsURI | + func test_getOptionalQueryItemAsURI_string() throws { + let value: String? = try converter.getOptionalQueryItemAsURI( + in: "search=foo", + style: nil, + explode: nil, + name: "search", + as: String.self + ) + XCTAssertEqual(value, "foo") + } + + // | server | get | request query | URI | required | getRequiredQueryItemAsURI | + func test_getRequiredQueryItemAsURI_string() throws { + let value: String = try converter.getRequiredQueryItemAsURI( + in: "search=foo", + style: nil, + explode: nil, + name: "search", + as: String.self + ) + XCTAssertEqual(value, "foo") + } + + func test_getOptionalQueryItemAsURI_arrayOfStrings() throws { + let query: Substring = "search=foo&search=bar" + let value: [String]? = try converter.getOptionalQueryItemAsURI( + in: query, + style: nil, + explode: nil, + name: "search", + as: [String].self + ) + XCTAssertEqual(value, ["foo", "bar"]) + } + + func test_getRequiredQueryItemAsURI_arrayOfStrings() throws { + let query: Substring = "search=foo&search=bar" + let value: [String] = try converter.getRequiredQueryItemAsURI( + in: query, + style: nil, + explode: nil, + name: "search", + as: [String].self + ) + XCTAssertEqual(value, ["foo", "bar"]) + } + + func test_getRequiredQueryItemAsURI_arrayOfStrings_unexploded() throws { + let query: Substring = "search=foo,bar" + let value: [String] = try converter.getRequiredQueryItemAsURI( + in: query, + style: nil, + explode: false, + name: "search", + as: [String].self + ) + XCTAssertEqual(value, ["foo", "bar"]) + } + + func test_getOptionalQueryItemAsURI_date() throws { + let query: Substring = "search=\(testDateEscapedString)" + let value: Date? = try converter.getOptionalQueryItemAsURI( + in: query, + style: nil, + explode: nil, + name: "search", + as: Date.self + ) + XCTAssertEqual(value, testDate) + } + + func test_getRequiredQueryItemAsURI_arrayOfDates() throws { + let query: Substring = "search=\(testDateEscapedString)&search=\(testDateEscapedString)" + let value: [Date] = try converter.getRequiredQueryItemAsURI( + in: query, + style: nil, + explode: nil, + name: "search", + as: [Date].self + ) + XCTAssertEqual(value, [testDate, testDate]) + } + + // | server | get | request body | JSON | optional | getOptionalRequestBodyAsJSON | + func test_getOptionalRequestBodyAsJSON_codable() async throws { + let body: TestPet? = try await converter.getOptionalRequestBodyAsJSON( + TestPet.self, + from: .init(data: testStructData), + transforming: { $0 } + ) + XCTAssertEqual(body, testStruct) + } + + func test_getOptionalRequestBodyAsJSON_codable_string() async throws { + let body: String? = try await converter.getOptionalRequestBodyAsJSON( + String.self, + from: .init(data: testQuotedStringData), + transforming: { $0 } + ) + XCTAssertEqual(body, testString) + } + + // | server | get | request body | JSON | required | getRequiredRequestBodyAsJSON | + func test_getRequiredRequestBodyAsJSON_codable() async throws { + let body: TestPet = try await converter.getRequiredRequestBodyAsJSON( + TestPet.self, + from: .init(data: testStructData), + transforming: { $0 } + ) + XCTAssertEqual(body, testStruct) + } + + // | server | get | request body | binary | optional | getOptionalRequestBodyAsBinary | + func test_getOptionalRequestBodyAsBinary_data() async throws { + let body: HTTPBody? = try converter.getOptionalRequestBodyAsBinary( + HTTPBody.self, + from: .init(data: testStringData), + transforming: { $0 } + ) + try await XCTAssertEqualStringifiedData(body, testString) + } + + // | server | get | request body | binary | required | getRequiredRequestBodyAsBinary | + func test_getRequiredRequestBodyAsBinary_data() async throws { + let body: HTTPBody = try converter.getRequiredRequestBodyAsBinary( + HTTPBody.self, + from: .init(data: testStringData), + transforming: { $0 } + ) + try await XCTAssertEqualStringifiedData(body, testString) + } + + // | server | set | response body | JSON | required | setResponseBodyAsJSON | + func test_setResponseBodyAsJSON_codable() async throws { + var headers: HTTPFields = [:] + let data = try converter.setResponseBodyAsJSON( + testStruct, + headerFields: &headers, + contentType: "application/json" + ) + try await XCTAssertEqualStringifiedData(data, testStructPrettyString) + XCTAssertEqual( + headers, + [ + .contentType: "application/json" + ] + ) + } + + // | server | set | response body | binary | required | setResponseBodyAsBinary | + func test_setResponseBodyAsBinary_data() async throws { + var headers: HTTPFields = [:] + let data = try converter.setResponseBodyAsBinary( + .init(data: testStringData), + headerFields: &headers, + contentType: "application/octet-stream" + ) + try await XCTAssertEqualStringifiedData(data, testString) + XCTAssertEqual( + headers, + [ + .contentType: "application/octet-stream" + ] + ) + } +} diff --git a/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift b/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift index d1a00664..b3992464 100644 --- a/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift +++ b/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift @@ -11,41 +11,31 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// -//import XCTest -//@_spi(Generated)@testable import OpenAPIRuntime -// -//final class Test_UniversalServer: Test_Runtime { -// -// struct MockHandler: Sendable {} -// -// func testApiPathComponentsWithServerPrefix_noPrefix() throws { -// let server = UniversalServer( -// handler: MockHandler() -// ) -// let components: [RouterPathComponent] = [ -// .constant("foo"), -// .parameter("bar"), -// ] -// let prefixed = try server.apiPathComponentsWithServerPrefix(components) -// // When no server path prefix, components stay the same -// XCTAssertEqual(prefixed, components) -// } -// -// func testApiPathComponentsWithServerPrefix_withPrefix() throws { -// let server = UniversalServer( -// serverURL: try serverURL, -// handler: MockHandler() -// ) -// let components: [RouterPathComponent] = [ -// .constant("foo"), -// .parameter("bar"), -// ] -// let prefixed = try server.apiPathComponentsWithServerPrefix(components) -// let expected: [RouterPathComponent] = [ -// .constant("api"), -// .constant("foo"), -// .parameter("bar"), -// ] -// XCTAssertEqual(prefixed, expected) -// } -//} +import XCTest +@_spi(Generated)@testable import OpenAPIRuntime + +final class Test_UniversalServer: Test_Runtime { + + struct MockHandler: Sendable {} + + func testApiPathComponentsWithServerPrefix_noPrefix() throws { + let server = UniversalServer( + handler: MockHandler() + ) + let components = "/foo/{bar}" + let prefixed = try server.apiPathComponentsWithServerPrefix(components) + // When no server path prefix, components stay the same + XCTAssertEqual(prefixed, components) + } + + func testApiPathComponentsWithServerPrefix_withPrefix() throws { + let server = UniversalServer( + serverURL: try serverURL, + handler: MockHandler() + ) + let components = "/foo/{bar}" + let prefixed = try server.apiPathComponentsWithServerPrefix(components) + let expected = "/api/foo/{bar}" + XCTAssertEqual(prefixed, expected) + } +} From fe08164d816a1bc191e650a6347659c5ecb5910c Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 5 Sep 2023 17:46:13 +0200 Subject: [PATCH 10/55] Just use Substring in more places --- .../Conversion/CurrencyExtensions.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index 7e51c4a9..bfe54df9 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -105,7 +105,7 @@ extension Converter { explode: Bool, inBody: Bool, key: String, - encodedValue: some StringProtocol + encodedValue: Substring ) throws -> T { let decoder = URIDecoder( configuration: uriCoderConfiguration( @@ -117,7 +117,7 @@ extension Converter { let value = try decoder.decode( T.self, forKey: key, - from: Substring(encodedValue) + from: encodedValue ) return value } @@ -145,7 +145,7 @@ extension Converter { } func convertJSONToHeaderFieldCodable( - _ stringValue: String + _ stringValue: Substring ) throws -> T { let data = Data(stringValue.utf8) return try decoder.decode(T.self, from: data) @@ -193,7 +193,7 @@ extension Converter { in headerFields: HTTPFields, name: String, as type: T.Type, - convert: (String) throws -> T + convert: (Substring) throws -> T ) throws -> T? { guard let stringValue = try getHeaderFieldValuesString( @@ -203,14 +203,14 @@ extension Converter { else { return nil } - return try convert(stringValue) + return try convert(stringValue[...]) } func getRequiredHeaderField( in headerFields: HTTPFields, name: String, as type: T.Type, - convert: (String) throws -> T + convert: (Substring) throws -> T ) throws -> T { guard let stringValue = try getHeaderFieldValuesString( @@ -220,7 +220,7 @@ extension Converter { else { throw RuntimeError.missingRequiredHeaderField(name) } - return try convert(stringValue) + return try convert(stringValue[...]) } func setEscapedQueryItem( From 63f05d63ba8b7955df8c9710661593cf34af518c Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 6 Sep 2023 08:18:21 +0200 Subject: [PATCH 11/55] wip --- Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift index 6fb6f662..1a057c38 100644 --- a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift +++ b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift @@ -46,6 +46,11 @@ extension HTTPRequest { /// The query substring of the request's path. @_spi(Generated) public var query: Substring? { + soar_query + } + + /// The query substring of the request's path. + public var soar_query: Substring? { guard let path else { return nil } From 982fe13ced76942156207fcb1cef038e75fe7250 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 6 Sep 2023 15:00:37 +0200 Subject: [PATCH 12/55] wip --- Package.swift | 4 ++-- Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 2672bc5e..1981f283 100644 --- a/Package.swift +++ b/Package.swift @@ -34,7 +34,7 @@ let package = Package( .library( name: "OpenAPIRuntime", targets: ["OpenAPIRuntime"] - ), + ) ], dependencies: [ .package(url: "https://github.com/apple/swift-http-types", branch: "main"), @@ -44,7 +44,7 @@ let package = Package( .target( name: "OpenAPIRuntime", dependencies: [ - .product(name: "HTTPTypes", package: "swift-http-types"), + .product(name: "HTTPTypes", package: "swift-http-types") ], swiftSettings: swiftSettings ), diff --git a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift index 1a057c38..e2b32eb1 100644 --- a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift +++ b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift @@ -48,7 +48,7 @@ extension HTTPRequest { public var query: Substring? { soar_query } - + /// The query substring of the request's path. public var soar_query: Substring? { guard let path else { @@ -61,6 +61,15 @@ extension HTTPRequest { let query = path[path.index(after: queryStart).. Date: Wed, 6 Sep 2023 17:55:06 +0200 Subject: [PATCH 13/55] Added more docs --- .../Conversion/Converter+Client.swift | 6 +- .../Conversion/Converter+Server.swift | 6 +- .../Conversion/CurrencyExtensions.swift | 236 +++++++++++++++--- 3 files changed, 204 insertions(+), 44 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index f9699435..b0f5d9a1 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -118,7 +118,7 @@ extension Converter { value, headerFields: &headerFields, contentType: contentType, - convert: convertDataToBinary + convert: { $0 } ) } @@ -132,7 +132,7 @@ extension Converter { value, headerFields: &headerFields, contentType: contentType, - convert: convertDataToBinary + convert: { $0 } ) } @@ -160,7 +160,7 @@ extension Converter { type, from: data, transforming: transform, - convert: convertBinaryToData + convert: { $0 } ) } } diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index 0a1d5ba1..58c914a0 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -218,7 +218,7 @@ extension Converter { type, from: data, transforming: transform, - convert: convertBinaryToData + convert: { $0 } ) } @@ -232,7 +232,7 @@ extension Converter { type, from: data, transforming: transform, - convert: convertBinaryToData + convert: { $0 } ) } @@ -260,7 +260,7 @@ extension Converter { value, headerFields: &headerFields, contentType: contentType, - convert: convertDataToBinary + convert: { $0 } ) } } diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index bfe54df9..69f1aaeb 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -19,9 +19,11 @@ extension ParameterStyle { /// Returns the parameter style and explode parameter that should be used /// based on the provided inputs, taking defaults into considerations. /// - Parameters: + /// - name: The name of the query item for which to resolve the inputs. /// - style: The provided parameter style, if any. /// - explode: The provided explode value, if any. /// - Throws: For an unsupported input combination. + /// - Returns: A tuple of the style and explode values. static func resolvedQueryStyleAndExplode( name: String, style: ParameterStyle?, @@ -43,7 +45,9 @@ extension ParameterStyle { extension HTTPField.Name { - // TODO: Docs + /// Creates a new name for the provided string. + /// - Parameter name: A field name. + /// - Throws: If the name isn't a valid field name. init(validated name: String) throws { guard let fieldName = Self.init(name) else { throw RuntimeError.invalidHeaderFieldName(name) @@ -54,7 +58,7 @@ extension HTTPField.Name { extension HTTPRequest { - // TODO: Docs + /// Returns the path of the request, and throws an error if it's nil. var requiredPath: Substring { get throws { guard let path else { @@ -69,6 +73,14 @@ extension Converter { // MARK: Converter helpers + /// Creates a new configuration for the URI coder. + /// - Parameters: + /// - style: A parameter style. + /// - explode: An explode value. + /// - inBody: A Boolean value indicating whether the URI coder is being + /// used for encoding a body URI. Specify `false` if used for a query, + /// header, and so on. + /// - Returns: A new URI coder configuration. func uriCoderConfiguration( style: ParameterStyle, explode: Bool, @@ -82,6 +94,16 @@ extension Converter { ) } + /// Returns a URI encoded string for the provided inputs. + /// - Parameters: + /// - style: A parameter style. + /// - explode: An explode value. + /// - inBody: A Boolean value indicating whether the URI coder is being + /// used for encoding a body URI. Specify `false` if used for a query, + /// header, and so on. + /// - key: The key to be encoded with the value. + /// - value: The value to be encoded. + /// - Returns: A URI encoded string. func convertToURI( style: ParameterStyle, explode: Bool, @@ -100,6 +122,16 @@ extension Converter { return encodedString } + /// Returns a value decoded from a URI encoded string. + /// - Parameters: + /// - style: A parameter style. + /// - explode: An explode value. + /// - inBody: A Boolean value indicating whether the URI coder is being + /// used for encoding a body URI. Specify `false` if used for a query, + /// header, and so on. + /// - key: The key for which the value was decoded. + /// - encodedValue: The encoded value to be decoded. + /// - Returns: A decoded value. func convertFromURI( style: ParameterStyle, explode: Bool, @@ -122,6 +154,9 @@ extension Converter { return value } + /// Returns a value decoded from a JSON body. + /// - Parameter body: The body containing the raw JSON bytes. + /// - Returns: A decoded value. func convertJSONToBodyCodable( _ body: HTTPBody ) async throws -> T { @@ -129,6 +164,9 @@ extension Converter { return try decoder.decode(T.self, from: data) } + /// Returns a JSON body for the provided encodable value. + /// - Parameter value: The value to encode as JSON. + /// - Returns: The raw JSON body. func convertBodyCodableToJSON( _ value: T ) throws -> HTTPBody { @@ -136,6 +174,9 @@ extension Converter { return HTTPBody(data: data) } + /// Returns a JSON string for the provided encodable value. + /// - Parameter value: The value to encode. + /// - Returns: A JSON string. func convertHeaderFieldCodableToJSON( _ value: T ) throws -> String { @@ -144,6 +185,9 @@ extension Converter { return stringValue } + /// Returns a value decoded from the provided JSON string. + /// - Parameter stringValue: A JSON string. + /// - Returns: The decoded value. func convertJSONToHeaderFieldCodable( _ stringValue: Substring ) throws -> T { @@ -151,20 +195,14 @@ extension Converter { return try decoder.decode(T.self, from: data) } - func convertBinaryToData( - _ binary: HTTPBody - ) throws -> HTTPBody { - binary - } - - func convertDataToBinary( - _ data: HTTPBody - ) throws -> HTTPBody { - data - } - // MARK: - Helpers for specific types of parameters + /// Sets the provided header field into the header field storage. + /// - Parameters: + /// - headerFields: The header field storage. + /// - name: The name of the header to set. + /// - value: The value of the header to set. + /// - convert: The closure used to serialize the header value to string. func setHeaderField( in headerFields: inout HTTPFields, name: String, @@ -182,6 +220,12 @@ extension Converter { ) } + /// Returns the value of the header with the provided name from the provided + /// header field storage. + /// - Parameters: + /// - headerFields: The header field storage. + /// - name: The name of the header field. + /// - Returns: The value of the header field, if found. Nil otherwise. func getHeaderFieldValuesString( in headerFields: HTTPFields, name: String @@ -189,6 +233,13 @@ extension Converter { try headerFields[.init(validated: name)] } + /// Returns a decoded value for the header field with the provided name. + /// - Parameters: + /// - headerFields: The header field storage. + /// - name: The name of the header field. + /// - type: The type to decode the value as. + /// - convert: The closure to convert the value from string. + /// - Returns: The decoded value, if found. Nil otherwise. func getOptionalHeaderField( in headerFields: HTTPFields, name: String, @@ -206,6 +257,13 @@ extension Converter { return try convert(stringValue[...]) } + /// Returns a decoded value for the header field with the provided name. + /// - Parameters: + /// - headerFields: The header field storage. + /// - name: The name of the header field. + /// - type: The type to decode the value as. + /// - convert: The closure to convert the value from string. + /// - Returns: The decoded value. func getRequiredHeaderField( in headerFields: HTTPFields, name: String, @@ -223,6 +281,15 @@ extension Converter { return try convert(stringValue[...]) } + /// Sets a query parameter with the provided inputs. + /// - Parameters: + /// - request: The request to set the query parameter on. + /// - style: A parameter style. + /// - explode: An explode value. + /// - name: The name of the query parameter. + /// - value: The value of the query parameter. Must already be + /// percent-escaped. + /// - convert: The closure that converts the provided value to string. func setEscapedQueryItem( in request: inout HTTPRequest, style: ParameterStyle?, @@ -266,6 +333,16 @@ extension Converter { request.path = path.appending("?\(query)&\(escapedUriSnippet)\(fragment)") } + /// Returns the decoded value for the provided name of a query parameter. + /// - Parameters: + /// - query: The full encoded query string from which to extract the + /// parameter. + /// - style: A parameter style. + /// - explode: An explode value. + /// - name: The name of the query parameter. + /// - type: The type to decode the string value as. + /// - convert: The closure that decodes the value from string. + /// - Returns: A decoded value, if found. Nil otherwise. func getOptionalQueryItem( in query: Substring?, style: ParameterStyle?, @@ -285,6 +362,16 @@ extension Converter { return try convert(query, resolvedStyle, resolvedExplode) } + /// Returns the decoded value for the provided name of a query parameter. + /// - Parameters: + /// - query: The full encoded query string from which to extract the + /// parameter. + /// - style: A parameter style. + /// - explode: An explode value. + /// - name: The name of the query parameter. + /// - type: The type to decode the string value as. + /// - convert: The closure that decodes the value from string. + /// - Returns: A decoded value. func getRequiredQueryItem( in query: Substring?, style: ParameterStyle?, @@ -308,6 +395,14 @@ extension Converter { return value } + /// Sets the provided request body and the appropriate content type. + /// - Parameters: + /// - value: The value to encode into the body. + /// - headerFields: The header fields storage where to save the content + /// type. + /// - contentType: The content type value. + /// - convert: The closure that encodes the value into a raw body. + /// - Returns: The body. func setRequiredRequestBody( _ value: T, headerFields: inout HTTPFields, @@ -318,6 +413,14 @@ extension Converter { return try convert(value) } + /// Sets the provided request body and the appropriate content type. + /// - Parameters: + /// - value: The value to encode into the body. + /// - headerFields: The header fields storage where to save the content + /// type. + /// - contentType: The content type value. + /// - convert: The closure that encodes the value into a raw body. + /// - Returns: The body, if value was not nil. func setOptionalRequestBody( _ value: T?, headerFields: inout HTTPFields, @@ -335,42 +438,43 @@ extension Converter { ) } + /// Returns a value decoded from the provided body. + /// - Parameters: + /// - type: The type to decode. + /// - body: The body to decode the value from. + /// - transform: The closure that wraps the body in its generated type. + /// - convert: The closure that decodes the body. + /// - Returns: A decoded wrapped type, if body is not nil. func getOptionalBufferingRequestBody( _ type: T.Type, - from data: HTTPBody?, + from body: HTTPBody?, transforming transform: (T) -> C, convert: (HTTPBody) async throws -> T ) async throws -> C? { - guard let data else { - return nil - } - let decoded = try await convert(data) - return transform(decoded) - } - - func getOptionalRequestBody( - _ type: T.Type, - from data: HTTPBody?, - transforming transform: (T) -> C, - convert: (HTTPBody) throws -> T - ) throws -> C? { - guard let data else { + guard let body else { return nil } - let decoded = try convert(data) + let decoded = try await convert(body) return transform(decoded) } + /// Returns a value decoded from the provided body. + /// - Parameters: + /// - type: The type to decode. + /// - body: The body to decode the value from. + /// - transform: The closure that wraps the body in its generated type. + /// - convert: The closure that decodes the body. + /// - Returns: A decoded wrapped type. func getRequiredBufferingRequestBody( _ type: T.Type, - from data: HTTPBody?, + from body: HTTPBody?, transforming transform: (T) -> C, convert: (HTTPBody) async throws -> T ) async throws -> C { guard let body = try await getOptionalBufferingRequestBody( type, - from: data, + from: body, transforming: transform, convert: convert ) @@ -380,16 +484,43 @@ extension Converter { return body } + /// Returns a value decoded from the provided body. + /// - Parameters: + /// - type: The type to decode. + /// - body: The body to decode the value from. + /// - transform: The closure that wraps the body in its generated type. + /// - convert: The closure that decodes the body. + /// - Returns: A decoded wrapped type, if body is not nil. + func getOptionalRequestBody( + _ type: T.Type, + from body: HTTPBody?, + transforming transform: (T) -> C, + convert: (HTTPBody) throws -> T + ) throws -> C? { + guard let body else { + return nil + } + let decoded = try convert(body) + return transform(decoded) + } + + /// Returns a value decoded from the provided body. + /// - Parameters: + /// - type: The type to decode. + /// - body: The body to decode the value from. + /// - transform: The closure that wraps the body in its generated type. + /// - convert: The closure that decodes the body. + /// - Returns: A decoded wrapped type. func getRequiredRequestBody( _ type: T.Type, - from data: HTTPBody?, + from body: HTTPBody?, transforming transform: (T) -> C, convert: (HTTPBody) throws -> T ) throws -> C { guard let body = try getOptionalRequestBody( type, - from: data, + from: body, transforming: transform, convert: convert ) @@ -399,28 +530,50 @@ extension Converter { return body } + /// Returns a value decoded from the provided body. + /// - Parameters: + /// - type: The type to decode. + /// - body: The body to decode the value from. + /// - transform: The closure that wraps the body in its generated type. + /// - convert: The closure that decodes the body. + /// - Returns: A decoded wrapped type. func getBufferingResponseBody( _ type: T.Type, - from data: HTTPBody, + from body: HTTPBody, transforming transform: (T) -> C, convert: (HTTPBody) async throws -> T ) async throws -> C { - let parsedValue = try await convert(data) + let parsedValue = try await convert(body) let transformedValue = transform(parsedValue) return transformedValue } + /// Returns a value decoded from the provided body. + /// - Parameters: + /// - type: The type to decode. + /// - body: The body to decode the value from. + /// - transform: The closure that wraps the body in its generated type. + /// - convert: The closure that decodes the body. + /// - Returns: A decoded wrapped type. func getResponseBody( _ type: T.Type, - from data: HTTPBody, + from body: HTTPBody, transforming transform: (T) -> C, convert: (HTTPBody) throws -> T ) throws -> C { - let parsedValue = try convert(data) + let parsedValue = try convert(body) let transformedValue = transform(parsedValue) return transformedValue } + /// Sets the provided request body and the appropriate content type. + /// - Parameters: + /// - value: The value to encode into the body. + /// - headerFields: The header fields storage where to save the content + /// type. + /// - contentType: The content type value. + /// - convert: The closure that encodes the value into a raw body. + /// - Returns: The body, if value was not nil. func setResponseBody( _ value: T, headerFields: inout HTTPFields, @@ -431,6 +584,13 @@ extension Converter { return try convert(value) } + /// Returns a decoded value for the provided path parameter. + /// - Parameters: + /// - pathParameters: The storage of path parameters. + /// - name: The name of the path parameter. + /// - type: The type to decode the value as. + /// - convert: The closure that decodes the value from string. + /// - Returns: A decoded value. func getRequiredRequestPath( in pathParameters: [String: Substring], name: String, From faba5ce7d5ac8b3cefec3a4f8e09ab18e5e78a05 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 6 Sep 2023 18:36:43 +0200 Subject: [PATCH 14/55] Add back docs --- .../OpenAPIRuntime/Errors/ClientError.swift | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Errors/ClientError.swift b/Sources/OpenAPIRuntime/Errors/ClientError.swift index c69a0bd5..f8020100 100644 --- a/Sources/OpenAPIRuntime/Errors/ClientError.swift +++ b/Sources/OpenAPIRuntime/Errors/ClientError.swift @@ -13,7 +13,6 @@ //===----------------------------------------------------------------------===// import HTTPTypes - #if canImport(Darwin) import Foundation #else @@ -21,24 +20,65 @@ import Foundation @preconcurrency import protocol Foundation.LocalizedError #endif + +/// An error thrown by a client performing an OpenAPI operation. +/// +/// Use a `ClientError` to inspect details about the request and response +/// that resulted in an error. +/// +/// You don't create or throw instances of `ClientError` yourself; they are +/// created and thrown on your behalf by the runtime library when a client +/// operation fails. public struct ClientError: Error { + /// The identifier of the operation, as defined in the OpenAPI document. public var operationID: String + /// The operation-specific Input value. public var operationInput: any Sendable + /// The HTTP request created during the operation. + /// + /// Will be nil if the error resulted before the request was generated, + /// for example if generating the request from the Input failed. public var request: HTTPRequest? + /// The HTTP request body created during the operation. + /// + /// Will be nil if the error resulted before the request was generated, + /// for example if generating the request from the Input failed. public var requestBody: HTTPBody? + /// The base URL for HTTP requests. + /// + /// Will be nil if the error resulted before the request was generated, + /// for example if generating the request from the Input failed. public var baseURL: URL? + /// The HTTP response received during the operation. + /// + /// Will be nil if the error resulted before the response was received. public var response: HTTPResponse? + /// The HTTP response body received during the operation. + /// + /// Will be nil if the error resulted before the response was received. public var responseBody: HTTPBody? + /// The underlying error that caused the operation to fail. public var underlyingError: any Error + /// Creates a new error. + /// - Parameters: + /// - operationID: The OpenAPI operation identifier. + /// - operationInput: The operation-specific Input value. + /// - request: The HTTP request created during the operation. + /// - request: The HTTP request body created during the operation. + /// - baseURL: The base URL for HTTP requests. + /// - response: The HTTP response received during the operation. + /// - response: The HTTP response body received during the operation. + /// - underlyingError: The underlying error that caused the operation + /// to fail. public init( operationID: String, operationInput: any Sendable, From 6a71613b42e9f76c4d2e33c25d0f71baf4cac667 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 7 Sep 2023 10:01:58 +0200 Subject: [PATCH 15/55] Improve loggability of the currency types --- .../OpenAPIRuntime/Errors/ClientError.swift | 10 ++---- .../OpenAPIRuntime/Errors/ServerError.swift | 4 +-- .../Interface/CurrencyTypes.swift | 32 +++++++++++++++++++ 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/Sources/OpenAPIRuntime/Errors/ClientError.swift b/Sources/OpenAPIRuntime/Errors/ClientError.swift index f8020100..7663688a 100644 --- a/Sources/OpenAPIRuntime/Errors/ClientError.swift +++ b/Sources/OpenAPIRuntime/Errors/ClientError.swift @@ -20,13 +20,12 @@ import Foundation @preconcurrency import protocol Foundation.LocalizedError #endif - /// An error thrown by a client performing an OpenAPI operation. /// -/// Use a `ClientError` to inspect details about the request and response +/// Use a `ClientError` to inspect details about the request and response /// that resulted in an error. /// -/// You don't create or throw instances of `ClientError` yourself; they are +/// You don't create or throw instances of `ClientError` yourself; they are /// created and thrown on your behalf by the runtime library when a client /// operation fails. public struct ClientError: Error { @@ -109,12 +108,9 @@ public struct ClientError: Error { } } -// TODO: Adopt pretty descriptions here (except the bodies). - extension ClientError: CustomStringConvertible { public var description: String { - // TODO: Bring back all the fields for easier debugging. - "Client error - operationID: \(operationID), underlying error: \(underlyingErrorDescription)" + "Client error - operationID: \(operationID), operationInput: \(String(describing: operationInput)), request: \(request?.prettyDescription ?? ""), requestBody: \(requestBody?.prettyDescription ?? ""), baseURL: \(baseURL?.absoluteString ?? ""), response: \(response?.prettyDescription ?? ""), responseBody: \(responseBody?.prettyDescription ?? "") , underlying error: \(underlyingErrorDescription)" } } diff --git a/Sources/OpenAPIRuntime/Errors/ServerError.swift b/Sources/OpenAPIRuntime/Errors/ServerError.swift index 4374ebfc..cbf057bb 100644 --- a/Sources/OpenAPIRuntime/Errors/ServerError.swift +++ b/Sources/OpenAPIRuntime/Errors/ServerError.swift @@ -59,11 +59,9 @@ public struct ServerError: Error { } } -// TODO: Make pretty printable (except the body) - extension ServerError: CustomStringConvertible { public var description: String { - "Server error - operationID: \(operationID), underlying error: \(underlyingErrorDescription)" + "Server error - operationID: \(operationID), request: \(request.prettyDescription), requestBody: \(requestBody?.prettyDescription ?? ""), metadata: \(metadata.description), operationInput: \(operationInput.map { String(describing: $0) } ?? ""), operationOutput: \(operationOutput.map { String(describing: $0) } ?? ""), underlying error: \(underlyingErrorDescription)" } } diff --git a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift index e2b32eb1..063616a4 100644 --- a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift +++ b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift @@ -81,3 +81,35 @@ extension HTTPResponse { self.init(status: .init(code: statusCode)) } } + +extension ServerRequestMetadata: CustomStringConvertible { + public var description: String { + "path parameters: \(pathParameters.description)" + } +} + +extension HTTPFields: PrettyStringConvertible { + var prettyDescription: String { + sorted(by: { $0.name.canonicalName.localizedCompare($1.name.canonicalName) == .orderedAscending }) + .map { "\($0.name.canonicalName): \($0.value)" } + .joined(separator: "; ") + } +} + +extension HTTPRequest: PrettyStringConvertible { + var prettyDescription: String { + "\(method.rawValue) \(path ?? "") [\(headerFields.prettyDescription)]" + } +} + +extension HTTPResponse: PrettyStringConvertible { + var prettyDescription: String { + "\(status.code) [\(headerFields.prettyDescription)]" + } +} + +extension HTTPBody: PrettyStringConvertible { + var prettyDescription: String { + String(describing: self) + } +} From 1c07795beefcf3ef3d9a88dece144b753587fad7 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 7 Sep 2023 10:04:32 +0200 Subject: [PATCH 16/55] Bring back comments on ServerError --- .../OpenAPIRuntime/Errors/ServerError.swift | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Sources/OpenAPIRuntime/Errors/ServerError.swift b/Sources/OpenAPIRuntime/Errors/ServerError.swift index cbf057bb..250fa8d1 100644 --- a/Sources/OpenAPIRuntime/Errors/ServerError.swift +++ b/Sources/OpenAPIRuntime/Errors/ServerError.swift @@ -15,22 +15,44 @@ import HTTPTypes import protocol Foundation.LocalizedError +/// An error thrown by a server handling an OpenAPI operation. public struct ServerError: Error { + /// Identifier of the operation that threw the error. public var operationID: String + /// HTTP request provided to the server. public var request: HTTPRequest + /// HTTP request body provided to the server. public var requestBody: HTTPBody? + /// Request metadata extracted by the server. public var metadata: ServerRequestMetadata + /// Operation-specific Input value. + /// + /// Is nil if error was thrown during request -> Input conversion. public var operationInput: (any Sendable)? + /// Operation-specific Output value. + /// + /// Is nil if error was thrown before/during Output -> response conversion. public var operationOutput: (any Sendable)? + /// The underlying error that caused the operation to fail. public var underlyingError: any Error + /// Creates a new error. + /// - Parameters: + /// - operationID: The OpenAPI operation identifier. + /// - request: HTTP request provided to the server. + /// - requestBody: HTTP request body provided to the server. + /// - metadata: Request metadata extracted by the server. + /// - operationInput: Operation-specific Input value. + /// - operationOutput: Operation-specific Output value. + /// - underlyingError: The underlying error that caused the operation + /// to fail. public init( operationID: String, request: HTTPRequest, From 2540b77dbbb9dd4d9f4116b2f7f7e4eee6df895c Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 7 Sep 2023 10:58:36 +0200 Subject: [PATCH 17/55] WIP --- .../Interface/ClientTransport.swift | 107 ++++++++++++++++++ .../OpenAPIRuntime/Interface/HTTPBody.swift | 33 +++++- 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift index c86de753..50062bd6 100644 --- a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift +++ b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift @@ -19,6 +19,113 @@ import struct Foundation.URL @preconcurrency import struct Foundation.URL #endif +/// A type that performs HTTP operations. +/// +/// Decouples an underlying HTTP library from generated client code. +/// +/// ### Choose between a transport and a middleware +/// +/// The ``ClientTransport`` and ``ClientMiddleware`` protocols look similar, +/// however each serves a different purpose. +/// +/// A _transport_ abstracts over the underlying HTTP library that actually +/// performs the HTTP operation by using the network. A generated `Client` +/// requires an exactly one client transport. +/// +/// A _middleware_ intercepts the HTTP request and response, without being +/// responsible for performing the HTTP operation itself. That's why +/// middlewares take the extra `next` parameter, to delegate making the HTTP +/// call to the transport at the top of the middleware stack. +/// +/// ### Use an existing client transport +/// +/// Instantiate the transport using the parameters required by the specific +/// implementation. For example, using the client transport for the +/// `URLSession` HTTP client provided by the Foundation framework: +/// +/// let transport = URLSessionTransport() +/// +/// Create the base URL of the server to call using your client. If the server +/// URL was defined in the OpenAPI document, you find a generated method for it +/// on the `Servers` type, for example: +/// +/// let serverURL = try Servers.server1() +/// +/// Instantiate the `Client` type generated by the Swift OpenAPI Generator for +/// your provided OpenAPI document. For example: +/// +/// let client = Client( +/// serverURL: serverURL, +/// transport: transport +/// ) +/// +/// Use the client to make HTTP calls defined in your OpenAPI document. For +/// example, if the OpenAPI document contains an HTTP operation with +/// the identifier `checkHealth`, call it from Swift with: +/// +/// let response = try await client.checkHealth(.init()) +/// switch response { +/// case .ok(let okPayload): +/// // ... +/// +/// // Handle any HTTP status code not documented in +/// // your OpenAPI document. +/// case .undocumented(let statusCode, _): +/// // ... +/// } +/// +/// The generated operation method takes an `Input` type unique to +/// the operation, and returns an `Output` type unique to the operation. +/// +/// > Note: You use the `Input` type to provide parameters such as HTTP request headers, +/// query items, path parameters, and request bodies; and inspect the `Output` +/// type to handle the received HTTP response status code, response header and +/// body. +/// +/// ### Implement a custom client transport +/// +/// If a client transport implementation for your preferred HTTP library doesn't +/// yet exist, or you need to simulate rare network conditions in your tests, +/// consider implementing a custom client transport. +/// +/// For example, to implement a test client transport that allows you +/// to test both a healthy and unhealthy response from a `checkHealth` +/// operation, define a new struct that conforms to the `ClientTransport` +/// protocol: +/// +/// struct TestTransport: ClientTransport { +/// var isHealthy: Bool = true +/// func send( +/// _ request: HTTPRequest, +/// body: HTTPBody?, +/// baseURL: URL, +/// operationID: String +/// ) async throws -> (HTTPResponse, HTTPBody) { +/// ( +/// HTTPResponse(status: isHealthy ? .ok : .internalServerError), +/// HTTPBody() +/// ) +/// } +/// } +/// +/// Then in your test code, instantiate and provide the test transport to your +/// generated client instead: +/// +/// let transport = TestTransport() +/// transport.isHealthy = true // for HTTP status code 200 (success) +/// transport.isHealthy = false // for HTTP status code 500 (failure) +/// let serverURL = try Servers.server1() +/// let client = Client( +/// serverURL: serverURL, +/// transport: transport +/// ) +/// let response = try await client.checkHealth(.init()) +/// // ... +/// +/// Implementing a test client transport is just one way to help test your +/// code that integrates with a generated client. Another is to implement +/// a type conforming to the generated protocol `APIProtocol`, and to implement +/// a custom ``ClientMiddleware``. public protocol ClientTransport: Sendable { func send( diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index 64120c6f..a33741cc 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -101,7 +101,7 @@ extension HTTPBody { @inlinable public convenience init() { self.init( - byteChunks: [], + sequence: .init(EmptySequence()), length: .known(0), iterationBehavior: .multiple ) @@ -548,4 +548,35 @@ extension HTTPBody { Iterator(iterator: sequence.makeIterator()) } } + + /// A wrapper for a sync sequence. + @usableFromInline + struct EmptySequence: AsyncSequence { + + @usableFromInline + typealias AsyncIterator = EmptyIterator + + @usableFromInline + typealias Element = DataType + + @usableFromInline + struct EmptyIterator: AsyncIteratorProtocol { + + @usableFromInline + typealias Element = DataType + + @usableFromInline + mutating func next() async throws -> HTTPBody.DataType? { + nil + } + } + + @inlinable + init() {} + + @usableFromInline + func makeAsyncIterator() -> EmptyIterator { + EmptyIterator() + } + } } From a165558abf022f934b4052dc8932f617de08878d Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 7 Sep 2023 11:13:29 +0200 Subject: [PATCH 18/55] Add comments back --- .../Interface/ClientTransport.swift | 111 +++++++++++++++++- 1 file changed, 107 insertions(+), 4 deletions(-) diff --git a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift index 50062bd6..5fbd06d8 100644 --- a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift +++ b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift @@ -77,10 +77,10 @@ import struct Foundation.URL /// The generated operation method takes an `Input` type unique to /// the operation, and returns an `Output` type unique to the operation. /// -/// > Note: You use the `Input` type to provide parameters such as HTTP request headers, -/// query items, path parameters, and request bodies; and inspect the `Output` -/// type to handle the received HTTP response status code, response header and -/// body. +/// > Note: You use the `Input` type to provide parameters such as HTTP request +/// headers, query items, path parameters, and request bodies; and inspect +/// the `Output` type to handle the received HTTP response status code, response +/// header and body. /// /// ### Implement a custom client transport /// @@ -128,6 +128,14 @@ import struct Foundation.URL /// a custom ``ClientMiddleware``. public protocol ClientTransport: Sendable { + /// Sends the underlying HTTP request and returns the received + /// HTTP response. + /// - Parameters: + /// - request: An HTTP request. + /// - body: HTTP request body. + /// - baseURL: A server base URL. + /// - operationID: The identifier of the OpenAPI operation. + /// - Returns: An HTTP response and its body. func send( _ request: HTTPRequest, body: HTTPBody?, @@ -136,8 +144,103 @@ public protocol ClientTransport: Sendable { ) async throws -> (HTTPResponse, HTTPBody) } +/// A type that intercepts HTTP requests and responses. +/// +/// It allows you to read and modify the request before it is received by +/// the transport and the response after it is returned by the transport. +/// +/// Appropriate for handling authentication, logging, metrics, tracing, +/// injecting custom headers such as "user-agent", and more. +/// +/// ### Choose between a transport and a middleware +/// +/// The ``ClientTransport`` and ``ClientMiddleware`` protocols look similar, +/// however each serves a different purpose. +/// +/// A _transport_ abstracts over the underlying HTTP library that actually +/// performs the HTTP operation by using the network. A generated `Client` +/// requires an exactly one client transport. +/// +/// A _middleware_ intercepts the HTTP request and response, without being +/// responsible for performing the HTTP operation itself. That's why +/// middlewares take the extra `next` parameter, to delegate making the HTTP +/// call to the transport at the top of the middleware stack. +/// +/// ### Use an existing client middleware +/// +/// Instantiate the middleware using the parameters required by the specific +/// implementation. For example, using a hypothetical existing middleware +/// that logs every request and response: +/// +/// let loggingMiddleware = LoggingMiddleware() +/// +/// Similarly to the process of using an existing ``ClientTransport``, provide +/// the middleware to the initializer of the generated `Client` type: +/// +/// let client = Client( +/// serverURL: serverURL, +/// transport: transport, +/// middlewares: [ +/// loggingMiddleware, +/// ] +/// ) +/// +/// Then make a call to one of the generated client methods: +/// +/// let response = try await client.checkHealth(.init()) +/// // ... +/// +/// As part of the invocation of `checkHealth`, the client first invokes +/// the middlewares in the order you provided them, and then passes the request +/// to the transport. When a response is received, the last middleware handles +/// it first, in the reverse order of the `middlewares` array. +/// +/// ### Implement a custom client middleware +/// +/// If a client middleware implementation with your desired behavior doesn't +/// yet exist, or you need to simulate rare network conditions your tests, +/// consider implementing a custom client middleware. +/// +/// For example, to implement a middleware that injects the "Authorization" +/// header to every outgoing request, define a new struct that conforms to +/// the `ClientMiddleware` protocol: +/// +/// /// Injects an authorization header to every request. +/// struct AuthenticationMiddleware: ClientMiddleware { +/// +/// /// The token value. +/// var bearerToken: String +/// +/// func intercept( +/// _ request: HTTPRequest, +/// body: HTTPBody?, +/// baseURL: URL, +/// operationID: String, +/// next: (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody) +/// ) async throws -> (HTTPResponse, HTTPBody) { +/// var request = request +/// request.headerFields[.authorization] = "Bearer \(bearerToken)" +/// return try await next(request, body, baseURL) +/// } +/// } +/// +/// An alternative use case for a middleware is to inject random failures +/// when calling a real server, to test your retry and error-handling logic. +/// +/// Implementing a test client middleware is just one way to help test your +/// code that integrates with a generated client. Another is to implement +/// a type conforming to the generated protocol `APIProtocol`, and to implement +/// a custom ``ClientTransport``. public protocol ClientMiddleware: Sendable { + /// Intercepts an outgoing HTTP request and an incoming HTTP response. + /// - Parameters: + /// - request: An HTTP request. + /// - body: HTTP request body. + /// - baseURL: baseURL: A server base URL. + /// - operationID: The identifier of the OpenAPI operation. + /// - next: A closure that calls the next middleware, or the transport. + /// - Returns: An HTTP response and its body. func intercept( _ request: HTTPRequest, body: HTTPBody?, From c4186f829208af49ad4bdf5b1874a69edceae090 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 7 Sep 2023 11:17:07 +0200 Subject: [PATCH 19/55] Prefix extensions on types we don't own --- .../OpenAPIRuntime/Interface/CurrencyTypes.swift | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift index 063616a4..b1bc35ea 100644 --- a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift +++ b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift @@ -24,7 +24,7 @@ public struct ServerRequestMetadata: Hashable, Sendable { /// Creates a new metadata wrapper with the specified path and query parameters. /// - Parameters: /// - pathParameters: Path parameters parsed from the URL of the HTTP - /// request. + /// request. public init( pathParameters: [String: Substring] = [:] ) { @@ -39,16 +39,10 @@ extension HTTPRequest { /// - path: The URL path of the resource. /// - method: The HTTP method. @_spi(Generated) - public init(path: String, method: Method) { + public init(soar_path path: String, method: Method) { self.init(method: method, scheme: nil, authority: nil, path: path) } - /// The query substring of the request's path. - @_spi(Generated) - public var query: Substring? { - soar_query - } - /// The query substring of the request's path. public var soar_query: Substring? { guard let path else { @@ -77,14 +71,14 @@ extension HTTPResponse { /// Creates a new response. /// - Parameter statusCode: The status code of the response.AsString @_spi(Generated) - public init(statusCode: Int) { + public init(soar_statusCode statusCode: Int) { self.init(status: .init(code: statusCode)) } } extension ServerRequestMetadata: CustomStringConvertible { public var description: String { - "path parameters: \(pathParameters.description)" + "Path parameters: \(pathParameters.description)" } } From 5e1d21e3c6197269ab6334d5321e1141a4fa5993 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 7 Sep 2023 13:35:16 +0200 Subject: [PATCH 20/55] WIP on documentation --- .../OpenAPIRuntime/Interface/HTTPBody.swift | 342 +++++++++++------- .../Conversion/Test_Converter+Client.swift | 12 +- .../Interface/Test_HTTPBody.swift | 4 +- Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 2 +- 4 files changed, 221 insertions(+), 139 deletions(-) diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index a33741cc..c6a111b1 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -16,13 +16,35 @@ import class Foundation.NSLock import protocol Foundation.LocalizedError import struct Foundation.Data // only for convenience initializers -/// The type representing a request or response body. +/// A body of an HTTP request or HTTP response. +/// +/// Under the hood, it represents an async sequence of byte chunks. +/// +/// ## Consuming a body +/// TODO +/// +/// ## Creating a body +/// There are convenience initializers to create a body from common types, such +/// as `Data`, `[UInt8]`, `ArraySlice`, and `String`. +/// +/// Create a body from a byte chunk: +/// ```swift +/// let bytes: ArraySlice = ... +/// let body = HTTPBody(bytes: bytes) +/// ``` +/// +/// ## Overriding the body length +/// TODO +/// +/// ## Specifying the iteration behavior +/// TODO +/// public final class HTTPBody: @unchecked Sendable { - /// The underlying data type. - public typealias DataType = ArraySlice + /// The underlying byte chunk type. + public typealias ByteChunk = ArraySlice - /// How many times the provided sequence can be iterated. + /// Describes how many times the provided sequence can be iterated. public enum IterationBehavior: Sendable { /// The input sequence can only be iterated once. @@ -38,10 +60,11 @@ public final class HTTPBody: @unchecked Sendable { case multiple } - /// How many times the provided sequence can be iterated. + /// The body's iteration behavior, which controls how many times + /// the input sequence can be iterated. public let iterationBehavior: IterationBehavior - /// The total length of the body, if known. + /// Describes the total length of the body, if known. public enum Length: Sendable { /// Total length not known yet. @@ -64,11 +87,17 @@ public final class HTTPBody: @unchecked Sendable { return lock }() - /// Whether an iterator has already been created. + /// A flag indicating whether an iterator has already been created. private var locked_iteratorCreated: Bool = false - @usableFromInline - init( + /// Creates a new body. + /// - Parameters: + /// - sequence: The input sequence providing the byte chunks. + /// - length: The total length of the body, in other words the accumulated + /// length of all the byte chunks. + /// - iterationBehavior: The sequence's iteration behavior, which + /// indicates whether the sequence can be iterated multiple times. + @usableFromInline init( sequence: BodySequence, length: Length, iterationBehavior: IterationBehavior @@ -98,8 +127,8 @@ extension HTTPBody: Hashable { extension HTTPBody { - @inlinable - public convenience init() { + /// Creates a new empty body. + @inlinable public convenience init() { self.init( sequence: .init(EmptySequence()), length: .known(0), @@ -107,9 +136,12 @@ extension HTTPBody { ) } - @inlinable - public convenience init( - bytes: DataType, + /// Creates a new body with the provided single byte chunk. + /// - Parameters: + /// - bytes: A byte chunk. + /// - length: The total length of the body. + @inlinable public convenience init( + bytes: ByteChunk, length: Length ) { self.init( @@ -118,9 +150,10 @@ extension HTTPBody { ) } - @inlinable - public convenience init( - bytes: DataType + /// Creates a new body with the provided single byte chunk. + /// - Parameter bytes: A byte chunk. + @inlinable public convenience init( + bytes: ByteChunk ) { self.init( byteChunks: [bytes], @@ -128,12 +161,17 @@ extension HTTPBody { ) } - @inlinable - public convenience init( + /// Creates a new body with the provided sequence of byte chunks. + /// - Parameters: + /// - byteChunks: A sequence of byte chunks. + /// - length: The total length of the body. + /// - iterationBehavior: The iteration behavior of the sequence, which + /// indicates whether it can be iterated multiple times. + @inlinable public convenience init( byteChunks: S, length: Length, iterationBehavior: IterationBehavior - ) where S.Element == DataType { + ) where S.Element == ByteChunk { self.init( sequence: .init(WrappedSyncSequence(sequence: byteChunks)), length: length, @@ -141,11 +179,14 @@ extension HTTPBody { ) } - @inlinable - public convenience init( + /// Creates a new body with the provided collection of byte chunks. + /// - Parameters: + /// - byteChunks: A collection of byte chunks. + /// - length: The total length of the body. + @inlinable public convenience init( byteChunks: C, length: Length - ) where C.Element == DataType { + ) where C.Element == ByteChunk { self.init( sequence: .init(WrappedSyncSequence(sequence: byteChunks)), length: length, @@ -153,10 +194,11 @@ extension HTTPBody { ) } - @inlinable - public convenience init( + /// Creates a new body with the provided collection of byte chunks. + /// - byteChunks: A collection of byte chunks. + @inlinable public convenience init( byteChunks: C - ) where C.Element == DataType { + ) where C.Element == ByteChunk { self.init( sequence: .init(WrappedSyncSequence(sequence: byteChunks)), length: .known(byteChunks.map(\.count).reduce(0, +)), @@ -164,9 +206,12 @@ extension HTTPBody { ) } - @inlinable - public convenience init( - stream: AsyncThrowingStream, + /// Creates a new body with the provided async throwing stream. + /// - Parameters: + /// - stream: An async throwing stream that provides the byte chunks. + /// - length: The total length of the body. + @inlinable public convenience init( + stream: AsyncThrowingStream, length: HTTPBody.Length ) { self.init( @@ -176,9 +221,12 @@ extension HTTPBody { ) } - @inlinable - public convenience init( - stream: AsyncStream, + /// Creates a new body with the provided async stream. + /// - Parameters: + /// - stream: An async stream that provides the byte chunks. + /// - length: The total length of the body. + @inlinable public convenience init( + stream: AsyncStream, length: HTTPBody.Length ) { self.init( @@ -188,12 +236,17 @@ extension HTTPBody { ) } - @inlinable - public convenience init( + /// Creates a new body with the provided async sequence. + /// - Parameters: + /// - sequence: An async sequence that provides the byte chunks. + /// - length: The total lenght of the body. + /// - iterationBehavior: The iteration behavior of the sequence, which + /// indicates whether it can be iterated multiple times. + @inlinable public convenience init( sequence: S, length: HTTPBody.Length, iterationBehavior: IterationBehavior - ) where S.Element == DataType { + ) where S.Element == ByteChunk { self.init( sequence: .init(sequence), length: length, @@ -205,7 +258,7 @@ extension HTTPBody { // MARK: - Consuming the body extension HTTPBody: AsyncSequence { - public typealias Element = DataType + public typealias Element = ByteChunk public typealias AsyncIterator = Iterator public func makeAsyncIterator() -> AsyncIterator { if iterationBehavior == .single { @@ -224,13 +277,13 @@ extension HTTPBody: AsyncSequence { } } -// MARK: - Consumption utils - extension HTTPBody { /// An error thrown by the `collect` function when the body contains more /// than the maximum allowed number of bytes. private struct TooManyBytesError: Error, CustomStringConvertible, LocalizedError { + + /// The maximum number of bytes acceptable by the user. let maxBytes: Int var description: String { @@ -256,13 +309,14 @@ extension HTTPBody { } /// Accumulates the full body in-memory into a single buffer - /// up to `maxBytes` and returns it. + /// up to the provided maximum number of bytes and returns it. /// - Parameters: /// - maxBytes: The maximum number of bytes this method is allowed /// to accumulate in memory before it throws an error. /// - Throws: `TooManyBytesError` if the the sequence contains more /// than `maxBytes`. - public func collect(upTo maxBytes: Int) async throws -> DataType { + /// - Returns: A single byte chunk containing all the accumulated bytes. + public func collect(upTo maxBytes: Int) async throws -> ByteChunk { // As a courtesy, check if another iteration is allowed, and throw // an error instead of fatalError here if the user is trying to @@ -280,7 +334,7 @@ extension HTTPBody { }() } - var buffer = DataType.init() + var buffer = ByteChunk.init() for try await chunk in self { guard buffer.count + chunk.count <= maxBytes else { throw TooManyBytesError(maxBytes: maxBytes) @@ -293,18 +347,13 @@ extension HTTPBody { // MARK: - String-based bodies -extension StringProtocol { - - @inlinable - var asBodyChunk: HTTPBody.DataType { - Array(utf8)[...] - } -} - extension HTTPBody { - @inlinable - public convenience init( + /// Creates a new body with the provided string encoded as UTF-8 bytes. + /// - Parameters: + /// - string: A string to encode as bytes. + /// - length: The total length of the body. + @inlinable public convenience init( string: some StringProtocol, length: Length ) { @@ -314,8 +363,10 @@ extension HTTPBody { ) } - @inlinable - public convenience init( + /// Creates a new body with the provided string encoded as UTF-8 bytes. + /// - Parameters: + /// - string: A string to encode as bytes. + @inlinable public convenience init( string: some StringProtocol ) { self.init( @@ -323,9 +374,14 @@ extension HTTPBody { length: .known(string.count) ) } - - @inlinable - public convenience init( + + /// Creates a new body with the provided strings encoded as UTF-8 bytes. + /// - Parameters: + /// - stringChunks: A sequence of string chunks. + /// - length: The total length of the body. + /// - iterationBehavior: The iteration behavior of the sequence, which + /// indicates whether it can be iterated multiple times. + @inlinable public convenience init( stringChunks: S, length: Length, iterationBehavior: IterationBehavior @@ -337,8 +393,11 @@ extension HTTPBody { ) } - @inlinable - public convenience init( + /// Creates a new body with the provided strings encoded as UTF-8 bytes. + /// - Parameters: + /// - stringChunks: A collection of string chunks. + /// - length: The total length of the body. + @inlinable public convenience init( stringChunks: C, length: Length ) where C.Element: StringProtocol { @@ -348,8 +407,10 @@ extension HTTPBody { ) } - @inlinable - public convenience init( + /// Creates a new body with the provided strings encoded as UTF-8 bytes. + /// - Parameters: + /// - stringChunks: A collection of string chunks. + @inlinable public convenience init( stringChunks: C ) where C.Element: StringProtocol { self.init( @@ -357,8 +418,11 @@ extension HTTPBody { ) } - @inlinable - public convenience init( + /// Creates a new body with the provided async throwing stream of strings. + /// - Parameters: + /// - stream: An async throwing stream that provides the string chunks. + /// - length: The total length of the body. + @inlinable public convenience init( stream: AsyncThrowingStream, length: HTTPBody.Length ) { @@ -369,8 +433,11 @@ extension HTTPBody { ) } - @inlinable - public convenience init( + /// Creates a new body with the provided async stream of strings. + /// - Parameters: + /// - stream: An async stream that provides the string chunks. + /// - length: The total length of the body. + @inlinable public convenience init( stream: AsyncStream, length: HTTPBody.Length ) { @@ -381,8 +448,13 @@ extension HTTPBody { ) } - @inlinable - public convenience init( + /// Creates a new body with the provided async sequence of string chunks. + /// - Parameters: + /// - sequence: An async sequence that provides the string chunks. + /// - length: The total lenght of the body. + /// - iterationBehavior: The iteration behavior of the sequence, which + /// indicates whether it can be iterated multiple times. + @inlinable public convenience init( sequence: S, length: HTTPBody.Length, iterationBehavior: IterationBehavior @@ -395,17 +467,27 @@ extension HTTPBody { } } +extension StringProtocol { + + /// Returns the string as a byte chunk compatible with the `HTTPBody` type. + @inlinable var asBodyChunk: HTTPBody.ByteChunk { + Array(utf8)[...] + } +} + extension HTTPBody { - /// Accumulates the full body in-memory into a single buffer - /// up to `maxBytes`, converts it to String, and returns it. + /// Accumulates the full body in-memory into a single buffer up to + /// the provided maximum number of bytes, converts it to string from + /// the UTF-8 bytes, and returns it. /// - Parameters: /// - maxBytes: The maximum number of bytes this method is allowed /// to accumulate in memory before it throws an error. /// - Throws: `TooManyBytesError` if the the body contains more /// than `maxBytes`. + /// - Returns: The string decoded from the UTF-8 bytes. public func collectAsString(upTo maxBytes: Int) async throws -> String { - let bytes: DataType = try await collect(upTo: maxBytes) + let bytes: ByteChunk = try await collect(upTo: maxBytes) return String(decoding: bytes, as: UTF8.self) } } @@ -413,7 +495,6 @@ extension HTTPBody { // MARK: - HTTPBody conversions extension HTTPBody: ExpressibleByStringLiteral { - public convenience init(stringLiteral value: String) { self.init(string: value) } @@ -421,16 +502,15 @@ extension HTTPBody: ExpressibleByStringLiteral { extension HTTPBody { - @inlinable - public convenience init(bytes: [UInt8]) { + /// Creates a new body from the provided array of bytes. + /// - Parameter bytes: An array of bytes. + @inlinable public convenience init(bytes: [UInt8]) { self.init(bytes: bytes[...]) } } extension HTTPBody: ExpressibleByArrayLiteral { - public typealias ArrayLiteralElement = UInt8 - public convenience init(arrayLiteral elements: UInt8...) { self.init(bytes: elements) } @@ -438,19 +518,23 @@ extension HTTPBody: ExpressibleByArrayLiteral { extension HTTPBody { + /// Creates a new body from the provided data chunk. + /// - Parameter data: A single data chunk. public convenience init(data: Data) { self.init(bytes: ArraySlice(data)) } - /// Accumulates the full body in-memory into a single buffer - /// up to `maxBytes`, converts it to Foundation.Data, and returns it. + /// Accumulates the full body in-memory into a single buffer up to + /// the provided maximum number of bytes, converts it to `Data`, and + /// returns it. /// - Parameters: /// - maxBytes: The maximum number of bytes this method is allowed /// to accumulate in memory before it throws an error. /// - Throws: `TooManyBytesError` if the the body contains more /// than `maxBytes`. + /// - Returns: The accumulated bytes wrapped in `Data`. public func collectAsData(upTo maxBytes: Int) async throws -> Data { - let bytes: DataType = try await collect(upTo: maxBytes) + let bytes: ByteChunk = try await collect(upTo: maxBytes) return Data(bytes) } } @@ -459,15 +543,18 @@ extension HTTPBody { extension HTTPBody { - /// Async iterator of both input async sequences and of the body itself. + /// An async iterator of both input async sequences and of the body itself. public struct Iterator: AsyncIteratorProtocol { - public typealias Element = HTTPBody.DataType + /// The element byte chunk type. + public typealias Element = HTTPBody.ByteChunk + /// The closure that produces the next element. private let produceNext: () async throws -> Element? - @usableFromInline - init( + /// Creates a new type-erased iterator from the provided iterator. + /// - Parameter iterator: The iterator to type-erase. + @usableFromInline init( _ iterator: Iterator ) where Iterator.Element == Element { var iterator = iterator @@ -485,97 +572,92 @@ extension HTTPBody { extension HTTPBody { /// A type-erased async sequence that wraps input sequences. - @usableFromInline - struct BodySequence: AsyncSequence { + @usableFromInline struct BodySequence: AsyncSequence { - @usableFromInline - typealias AsyncIterator = HTTPBody.Iterator + /// The type of the type-erased iterator. + @usableFromInline typealias AsyncIterator = HTTPBody.Iterator - @usableFromInline - typealias Element = DataType + /// The byte chunk element type. + @usableFromInline typealias Element = ByteChunk - @usableFromInline - let produceIterator: () -> AsyncIterator + /// A closure that produces a new iterator. + @usableFromInline let produceIterator: () -> AsyncIterator - @inlinable - init(_ sequence: S) where S.Element == Element { + /// Creates a new sequence. + /// - Parameter sequence: The input sequence to type-erase. + @inlinable init(_ sequence: S) where S.Element == Element { self.produceIterator = { .init(sequence.makeAsyncIterator()) } } - @usableFromInline - func makeAsyncIterator() -> AsyncIterator { + @usableFromInline func makeAsyncIterator() -> AsyncIterator { produceIterator() } } - /// A wrapper for a sync sequence. - @usableFromInline - struct WrappedSyncSequence: AsyncSequence - where S.Element == DataType, S.Iterator.Element == DataType { + /// An async sequence wrapper for a sync sequence. + @usableFromInline struct WrappedSyncSequence: AsyncSequence + where S.Element == ByteChunk, S.Iterator.Element == ByteChunk { - @usableFromInline - typealias AsyncIterator = Iterator + /// The type of the iterator. + @usableFromInline typealias AsyncIterator = Iterator - @usableFromInline - typealias Element = DataType + /// The byte chunk element type. + @usableFromInline typealias Element = ByteChunk - @usableFromInline - struct Iterator: AsyncIteratorProtocol { + /// An iterator type that wraps a sync sequence iterator. + @usableFromInline struct Iterator: AsyncIteratorProtocol { - @usableFromInline - typealias Element = DataType + /// The byte chunk element type. + @usableFromInline typealias Element = ByteChunk + /// The underlying sync sequence iterator. var iterator: any IteratorProtocol - @usableFromInline - mutating func next() async throws -> HTTPBody.DataType? { + @usableFromInline mutating func next() async throws -> HTTPBody.ByteChunk? { iterator.next() } } - @usableFromInline - let sequence: S + /// The underlying sync sequence. + @usableFromInline let sequence: S - @inlinable - init(sequence: S) { + /// Creates a new async sequence with the provided sync sequence. + /// - Parameter sequence: The sync sequence to wrap. + @inlinable init(sequence: S) { self.sequence = sequence } - @usableFromInline - func makeAsyncIterator() -> Iterator { + @usableFromInline func makeAsyncIterator() -> Iterator { Iterator(iterator: sequence.makeIterator()) } } - /// A wrapper for a sync sequence. - @usableFromInline - struct EmptySequence: AsyncSequence { + /// An empty async sequence. + @usableFromInline struct EmptySequence: AsyncSequence { - @usableFromInline - typealias AsyncIterator = EmptyIterator + /// The type of the empty iterator. + @usableFromInline typealias AsyncIterator = EmptyIterator - @usableFromInline - typealias Element = DataType + /// The byte chunk element type. + @usableFromInline typealias Element = ByteChunk - @usableFromInline - struct EmptyIterator: AsyncIteratorProtocol { + /// An async iterator of an empty sequence. + @usableFromInline struct EmptyIterator: AsyncIteratorProtocol { - @usableFromInline - typealias Element = DataType + /// The byte chunk element type. + @usableFromInline typealias Element = ByteChunk - @usableFromInline - mutating func next() async throws -> HTTPBody.DataType? { + @usableFromInline mutating func next() async throws -> HTTPBody.ByteChunk? { nil } } - @inlinable - init() {} + /// Creates a new empty async sequence. + @inlinable init() {} - @usableFromInline - func makeAsyncIterator() -> EmptyIterator { + @usableFromInline func makeAsyncIterator() -> EmptyIterator { EmptyIterator() } } diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift index 44e333b2..b1aa8055 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -56,7 +56,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { name: "search", value: "foo" ) - XCTAssertEqual(request.query, "search=foo") + XCTAssertEqual(request.soar_query, "search=foo") } func test_setQueryItemAsURI_stringConvertible_needsEncoding() throws { @@ -68,7 +68,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { name: "search", value: "h%llo" ) - XCTAssertEqual(request.query, "search=h%25llo") + XCTAssertEqual(request.soar_query, "search=h%25llo") } func test_setQueryItemAsURI_arrayOfStrings() throws { @@ -80,7 +80,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { name: "search", value: ["foo", "bar"] ) - XCTAssertEqual(request.query, "search=foo&search=bar") + XCTAssertEqual(request.soar_query, "search=foo&search=bar") } func test_setQueryItemAsURI_arrayOfStrings_unexploded() throws { @@ -92,7 +92,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { name: "search", value: ["foo", "bar"] ) - XCTAssertEqual(request.query, "search=foo,bar") + XCTAssertEqual(request.soar_query, "search=foo,bar") } func test_setQueryItemAsURI_date() throws { @@ -104,7 +104,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { name: "search", value: testDate ) - XCTAssertEqual(request.query, "search=2023-01-18T10%3A04%3A11Z") + XCTAssertEqual(request.soar_query, "search=2023-01-18T10%3A04%3A11Z") } func test_setQueryItemAsURI_arrayOfDates() throws { @@ -116,7 +116,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { name: "search", value: [testDate, testDate] ) - XCTAssertEqual(request.query, "search=2023-01-18T10%3A04%3A11Z&search=2023-01-18T10%3A04%3A11Z") + XCTAssertEqual(request.soar_query, "search=2023-01-18T10%3A04%3A11Z&search=2023-01-18T10%3A04%3A11Z") } // | client | set | request body | JSON | optional | setOptionalRequestBodyAsJSON | diff --git a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift index 4f500afc..f8a5a9ff 100644 --- a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift +++ b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift @@ -200,7 +200,7 @@ final class Test_Body: Test_Runtime { length: .known(5), iterationBehavior: .single ) - var chunks: [HTTPBody.DataType] = [] + var chunks: [HTTPBody.ByteChunk] = [] for try await chunk in body { chunks.append(chunk) } @@ -211,7 +211,7 @@ final class Test_Body: Test_Runtime { extension Test_Body { func _testConsume( _ body: HTTPBody, - expected: HTTPBody.DataType, + expected: HTTPBody.ByteChunk, file: StaticString = #file, line: UInt = #line ) async throws { diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index d62adc72..98e77c45 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -44,7 +44,7 @@ class Test_Runtime: XCTestCase { } var testRequest: HTTPRequest { - .init(path: "/api", method: .get) + .init(soar_path: "/api", method: .get) } var testDate: Date { From 0ee065fe938d68d64067e640794f76cb27a11335 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 7 Sep 2023 16:10:26 +0200 Subject: [PATCH 21/55] Add docs for HTTPBody --- .../OpenAPIRuntime/Interface/HTTPBody.swift | 95 +++++++++++++++++-- 1 file changed, 86 insertions(+), 9 deletions(-) diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index c6a111b1..14192b5f 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -20,25 +20,102 @@ import struct Foundation.Data // only for convenience initializers /// /// Under the hood, it represents an async sequence of byte chunks. /// -/// ## Consuming a body -/// TODO -/// -/// ## Creating a body +/// ## Creating a body from a buffer /// There are convenience initializers to create a body from common types, such /// as `Data`, `[UInt8]`, `ArraySlice`, and `String`. /// +/// Create an empty body: +/// ```swift +/// let body = HTTPBody() +/// ``` +/// /// Create a body from a byte chunk: /// ```swift /// let bytes: ArraySlice = ... /// let body = HTTPBody(bytes: bytes) /// ``` /// -/// ## Overriding the body length -/// TODO +/// Create a body from `Foundation.Data`: +/// ```swift +/// let data: Foundation.Data = ... +/// let body = HTTPBody(data: data) +/// ``` +/// +/// Create a body from a string: +/// ```swift +/// let body = HTTPBody(string: "Hello, world!") +/// ``` +/// +/// ## Creating a body from an async sequence +/// The body type also supports initialization from an async sequence. +/// +/// ```swift +/// let producingSequence = ... // an AsyncSequence +/// let length: HTTPBody.Length = .known(1024) // or .unknown +/// let body = HTTPBody( +/// sequence: producingSequence, +/// length: length, +/// iterationBehavior: .single // or .multiple +/// ) +/// ``` +/// +/// In addition to the async sequence, also provide the total body length, +/// if known (this can be sent in the `content-length` header), and whether +/// the sequence is safe to be iterated multiple times, or can only be iterated +/// once. +/// +/// Sequences that can be iterated multiple times work better when an HTTP +/// request needs to be retried, or if a redirect is encountered. +/// +/// In addition to providing the async sequence, you can also produce the body +/// using an `AsyncStream` or `AsyncThrowingStream`: +/// +/// ```swift +/// let body = HTTPBody( +/// stream: AsyncStream(ArraySlice.self, { continuation in +/// continuation.yield([72, 69]) +/// continuation.yield([76, 76, 79]) +/// continuation.finish() +/// }), +/// length: .known(5) +/// ) +/// ``` +/// +/// ## Consuming a body as an async sequence +/// The `HTTPBody` type conforms to `AsyncSequence` and uses `ArraySlice` +/// as its element type, so it can be consumed in a streaming fashion, without +/// ever buffering the whole body in your process. +/// +/// For example, to get another sequence that contains only the size of each +/// chunk, and print each size, use: /// -/// ## Specifying the iteration behavior -/// TODO +/// ```swift +/// let chunkSizes = body.map { chunk in chunk.count } +/// for try await chunkSize in chunkSizes { +/// print("Chunk size: \(chunkSize)") +/// } +/// ``` /// +/// ## Consuming a body as a buffer +/// If you need to collect the whole body before processing it, use one of +/// the convenience `collect` methods on `HTTPBody`. +/// +/// To get all the bytes, use: +/// +/// ```swift +/// let buffer = try await body.collect(upTo: 2 * 1024 * 1024) +/// ``` +/// +/// Note that you must provide the maximum number of bytes you can buffer in +/// memory, in the example above we provide 2 MB. If more bytes are available, +/// the method throws the `TooManyBytesError` to stop the process running out +/// of memory. While discouraged, you can provide `collect(upTo: .max)` to +/// read all the available bytes, without a limit. +/// +/// The body type provides more variants of the `collect` method for commonly +/// used buffers, such as: +/// - `collectAsData` provides the buffered data as `Foundation.Data` +/// - `collectAsString` provides the buffered data as a string decoded as UTF-8 public final class HTTPBody: @unchecked Sendable { /// The underlying byte chunk type. @@ -374,7 +451,7 @@ extension HTTPBody { length: .known(string.count) ) } - + /// Creates a new body with the provided strings encoded as UTF-8 bytes. /// - Parameters: /// - stringChunks: A sequence of string chunks. From 99d8252fb14c6fbdd585d510b71dcd23b5f91f47 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 7 Sep 2023 17:12:28 +0200 Subject: [PATCH 22/55] Add doc comments --- .../OpenAPIRuntime/Errors/ServerError.swift | 20 +- .../Interface/ClientTransport.swift | 6 +- .../Interface/ServerTransport.swift | 191 ++++++++++++++++++ 3 files changed, 204 insertions(+), 13 deletions(-) diff --git a/Sources/OpenAPIRuntime/Errors/ServerError.swift b/Sources/OpenAPIRuntime/Errors/ServerError.swift index 250fa8d1..df7ed816 100644 --- a/Sources/OpenAPIRuntime/Errors/ServerError.swift +++ b/Sources/OpenAPIRuntime/Errors/ServerError.swift @@ -21,21 +21,21 @@ public struct ServerError: Error { /// Identifier of the operation that threw the error. public var operationID: String - /// HTTP request provided to the server. + /// The HTTP request provided to the server. public var request: HTTPRequest - /// HTTP request body provided to the server. + /// The HTTP request body provided to the server. public var requestBody: HTTPBody? - /// Request metadata extracted by the server. + /// The request metadata extracted by the server. public var metadata: ServerRequestMetadata - /// Operation-specific Input value. + /// An operation-specific Input value. /// /// Is nil if error was thrown during request -> Input conversion. public var operationInput: (any Sendable)? - /// Operation-specific Output value. + /// An operation-specific Output value. /// /// Is nil if error was thrown before/during Output -> response conversion. public var operationOutput: (any Sendable)? @@ -46,11 +46,11 @@ public struct ServerError: Error { /// Creates a new error. /// - Parameters: /// - operationID: The OpenAPI operation identifier. - /// - request: HTTP request provided to the server. - /// - requestBody: HTTP request body provided to the server. - /// - metadata: Request metadata extracted by the server. - /// - operationInput: Operation-specific Input value. - /// - operationOutput: Operation-specific Output value. + /// - request: The HTTP request provided to the server. + /// - requestBody: The HTTP request body provided to the server. + /// - metadata: The request metadata extracted by the server. + /// - operationInput: An operation-specific Input value. + /// - operationOutput: An operation-specific Output value. /// - underlyingError: The underlying error that caused the operation /// to fail. public init( diff --git a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift index 5fbd06d8..1fb5b3b8 100644 --- a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift +++ b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift @@ -132,7 +132,7 @@ public protocol ClientTransport: Sendable { /// HTTP response. /// - Parameters: /// - request: An HTTP request. - /// - body: HTTP request body. + /// - body: An HTTP request body. /// - baseURL: A server base URL. /// - operationID: The identifier of the OpenAPI operation. /// - Returns: An HTTP response and its body. @@ -236,8 +236,8 @@ public protocol ClientMiddleware: Sendable { /// Intercepts an outgoing HTTP request and an incoming HTTP response. /// - Parameters: /// - request: An HTTP request. - /// - body: HTTP request body. - /// - baseURL: baseURL: A server base URL. + /// - body: An HTTP request body. + /// - baseURL: A server base URL. /// - operationID: The identifier of the OpenAPI operation. /// - next: A closure that calls the next middleware, or the transport. /// - Returns: An HTTP response and its body. diff --git a/Sources/OpenAPIRuntime/Interface/ServerTransport.swift b/Sources/OpenAPIRuntime/Interface/ServerTransport.swift index baeee2f5..f9bd3d38 100644 --- a/Sources/OpenAPIRuntime/Interface/ServerTransport.swift +++ b/Sources/OpenAPIRuntime/Interface/ServerTransport.swift @@ -14,8 +14,105 @@ import HTTPTypes +/// A type that registers and handles HTTP operations. +/// +/// Decouples the HTTP server framework from the generated server code. +/// +/// ### Choose between a transport and a middleware +/// +/// The ``ServerTransport`` and ``ServerMiddleware`` protocols look similar, +/// however each serves a different purpose. +/// +/// A _transport_ abstracts over the underlying HTTP library that actually +/// receives the HTTP requests from the network. An implemented _handler_ +/// (a type implemented by you that conforms to the generated `APIProtocol` +/// protocol) is generally configured with exactly one server transport. +/// +/// A _middleware_ intercepts the HTTP request and response, without being +/// responsible for receiving the HTTP operations itself. That's why +/// middlewares take the extra `next` parameter, to delegate calling the handler +/// to the transport at the top of the middleware stack. +/// +/// ### Use an existing server transport +/// +/// Instantiate the transport using the parameters required by the specific +/// implementation. For example, using the server transport for the +/// `Vapor` web framework, first create the `Application` object provided by +/// Vapor, and provided it to the initializer of `VaporTransport`: +/// +/// let app = Vapor.Application() +/// let transport = VaporTransport(routesBuilder: app) +/// +/// Implement a new type that conforms to the generated `APIProtocol`, which +/// serves as the request handler of your server's business logic. For example, +/// this is what a simple implementation of a server that has a single +/// HTTP operation called `checkHealth` defined in the OpenAPI document, and +/// it always returns the 200 HTTP status code: +/// +/// struct MyAPIImplementation: APIProtocol { +/// func checkHealth( +/// _ input: Operations.checkHealth.Input +/// ) async throws -> Operations.checkHealth.Output { +/// .ok(.init()) +/// } +/// } +/// +/// The generated operation method takes an `Input` type unique to +/// the operation, and returns an `Output` type unique to the operation. +/// +/// > Note: You use the `Input` type to provide parameters such as HTTP request +/// headers, query items, path parameters, and request bodies; and inspect +/// the `Output` type to handle the received HTTP response status code, +/// response header and body. +/// +/// Create an instance of your handler: +/// +/// let handler = MyAPIImplementation() +/// +/// Create the URL where the server will run. The path of the URL is extracted +/// by the transport to create a common prefix (such as `/api/v1`) that might +/// be expected by the clients. If the server URL is defined in the OpenAPI +/// document, find the generated method for it on the `Servers` type, +/// for example: +/// +/// let serverURL = try Servers.server1() +/// +/// Register the generated request handlers by calling the method generated +/// on the `APIProtocol` protocol: +/// +/// try handler.registerHandlers(on: transport, serverURL: serverURL) +/// +/// Start the server by following the documentation of your chosen transport: +/// +/// try app.run() +/// +/// ### Implement a custom server transport +/// +/// If a server transport implementation for your preferred web framework +/// doesn't yet exist, or you need to simulate rare network conditions in +/// your tests, consider implementing a custom server transport. +/// +/// Define a new type that conforms to the `ServerTransport` protocol by +/// registering request handlers with the underlying web framework, to be +/// later called when the web framework receives an HTTP request to one +/// of the HTTP routes. +/// +/// In tests, this might require using the web framework's specific test +/// APIs to allow for simulating incoming HTTP requests. +/// +/// Implementing a test server transport is just one way to help test your +/// code that integrates with your handler. Another is to implement +/// a type conforming to the generated protocol `APIProtocol`, and to implement +/// a custom ``ServerMiddleware``. public protocol ServerTransport { + /// Registers an HTTP operation handler at the provided path and method. + /// - Parameters: + /// - handler: A handler to be invoked when an HTTP request is received. + /// - method: An HTTP request method. + /// - path: A URL template for the path, for example `/pets/{petId}`. + /// - Important: The `path` can have mixed component, such + /// as `/file/{name}.zip`. func register( _ handler: @Sendable @escaping (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> ( HTTPResponse, HTTPBody @@ -25,8 +122,102 @@ public protocol ServerTransport { ) throws } +/// A type that intercepts HTTP requests and responses. +/// +/// It allows you to customize the request after it was provided by +/// the transport, but before it was parsed, validated, and provided to +/// the request handler; and the response after it was provided by the request +/// handler, but before it was handed back to the transport. +/// +/// Appropriate for verifying authentication, performing logging, metrics, +/// tracing, injecting custom headers such as "user-agent", and more. +/// +/// ### Choose between a transport and a middleware +/// +/// The ``ServerTransport`` and ``ServerMiddleware`` protocols look similar, +/// however each serves a different purpose. +/// +/// A _transport_ abstracts over the underlying HTTP library that actually +/// receives the HTTP requests from the network. An implemented _handler_ +/// (a type implemented by you that conforms to the generated `APIProtocol` +/// protocol) is generally configured with exactly one server transport. +/// +/// A _middleware_ intercepts the HTTP request and response, without being +/// responsible for receiving the HTTP operations itself. That's why +/// middlewares take the extra `next` parameter, to delegate calling the handler +/// to the transport at the top of the middleware stack. +/// +/// ### Use an existing server middleware +/// +/// Instantiate the middleware using the parameters required by the specific +/// implementation. For example, using a hypothetical existing middleware +/// that logs every request and response: +/// +/// let loggingMiddleware = LoggingMiddleware() +/// +/// Similarly to the process of using an existing ``ServerTransport``, provide +/// the middleware to the call to register handlers: +/// +/// try handler.registerHandlers( +/// on: transport, +/// serverURL: serverURL, +/// middlewares: [ +/// loggingMiddleware, +/// ] +/// ) +/// +/// Then when an HTTP request is received, the server first invokes +/// the middlewares in the order you provided them, and then passes +/// the parsed request to your handler. When a response is received from +/// the handler, the last middleware handles the response first, and it goes +/// back in the reverse order of the `middlewares` array. At the end, +/// the transport sends the final response back to the client. +/// +/// ### Implement a custom server middleware +/// +/// If a server middleware implementation with your desired behavior doesn't +/// yet exist, or you need to simulate rare requests in your tests, +/// consider implementing a custom server middleware. +/// +/// For example, an implementation a middleware that prints only basic +/// information about the incoming request and outgoing response: +/// +/// /// A middleware that prints request and response metadata. +/// struct PrintingMiddleware: ServerMiddleware { +/// func intercept( +/// _ request: HTTPRequest, +/// body: HTTPBody?, +/// metadata: ServerRequestMetadata, +/// operationID: String, +/// next: (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody) +/// ) async throws -> (HTTPResponse, HTTPBody) { +/// print(">>>: \(request.method.rawValue) \(request.soar_pathOnly)") +/// do { +/// let (response, responseBody) = try await next(request, body, metadata) +/// print("<<<: \(response.status.code)") +/// return (response, responseBody) +/// } catch { +/// print("!!!: \(error.localizedDescription)") +/// throw error +/// } +/// } +/// } +/// +/// Implementing a test server middleware is just one way to help test your +/// code that integrates with your handler. Another is to implement +/// a type conforming to the generated protocol `APIProtocol`, and to implement +/// a custom ``ServerTransport``. public protocol ServerMiddleware: Sendable { + /// Intercepts an incoming HTTP request and an outgoing HTTP response. + /// - Parameters: + /// - request: An HTTP request. + /// - body: An HTTP request body. + /// - metadata: The metadata parsed from the HTTP request, including path + /// parameters. + /// - operationID: The identifier of the OpenAPI operation. + /// - next: A closure that calls the next middleware, or the transport. + /// - Returns: An HTTP response and its body. func intercept( _ request: HTTPRequest, body: HTTPBody?, From d85eb5589a1d00d85e57634e8a1a48dffd259818 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 7 Sep 2023 17:19:43 +0200 Subject: [PATCH 23/55] More docs --- .../Interface/UniversalClient.swift | 35 ++++++++++++++++- .../Interface/UniversalServer.swift | 39 ++++++++++++++++++- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/Sources/OpenAPIRuntime/Interface/UniversalClient.swift b/Sources/OpenAPIRuntime/Interface/UniversalClient.swift index 32644b97..dab7ab64 100644 --- a/Sources/OpenAPIRuntime/Interface/UniversalClient.swift +++ b/Sources/OpenAPIRuntime/Interface/UniversalClient.swift @@ -18,14 +18,27 @@ import Foundation @preconcurrency import struct Foundation.URL #endif -@_spi(Generated) -public struct UniversalClient: Sendable { +/// OpenAPI document-agnostic HTTP client used by OpenAPI document-specific, +/// generated clients to perform request serialization, middleware and transport +/// invocation, and response deserialization. +/// +/// Do not call this directly, only invoked by generated code. +@_spi(Generated) public struct UniversalClient: Sendable { + /// The URL of the server, used as the base URL for requests made by the + /// client. public let serverURL: URL + + /// A converter for encoding/decoding data. public let converter: Converter + + /// A type capable of sending HTTP requests and receiving HTTP responses. public var transport: any ClientTransport + + /// The middlewares to be invoked before the transport. public var middlewares: [any ClientMiddleware] + /// Internal initializer that takes an initialized `Converter`. internal init( serverURL: URL, converter: Converter, @@ -38,6 +51,7 @@ public struct UniversalClient: Sendable { self.middlewares = middlewares } + /// Creates a new client. public init( serverURL: URL = .defaultOpenAPIServerURL, configuration: Configuration = .init(), @@ -52,6 +66,23 @@ public struct UniversalClient: Sendable { ) } + /// Performs the HTTP operation. + /// + /// Should only be called by generated code, not directly. + /// + /// An operation consists of three steps: + /// 1. Convert Input into an HTTP request. + /// 2. Invoke the `ClientTransport` to perform the HTTP call, wrapped by middlewares. + /// 3. Convert the HTTP response into Output. + /// + /// It wraps any thrown errors and attaches appropriate context. + /// + /// - Parameters: + /// - input: Operation-specific input value. + /// - operationID: The OpenAPI operation identifier. + /// - serializer: Creates an HTTP request from the provided Input value. + /// - deserializer: Creates an Output value from the provided HTTP response. + /// - Returns: The Output value produced by `deserializer`. public func send( input: OperationInput, forOperation operationID: String, diff --git a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift index 4ca6d545..c1e38c38 100644 --- a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift +++ b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift @@ -22,17 +22,27 @@ import struct Foundation.URLComponents @preconcurrency import struct Foundation.URLComponents #endif -@_spi(Generated) -public struct UniversalServer: Sendable { +/// OpenAPI document-agnostic HTTP server used by OpenAPI document-specific, +/// generated servers to perform request deserialization, middleware and handler +/// invocation, and response serialization. +/// +/// Do not call this directly, only invoked by generated code. +@_spi(Generated) public struct UniversalServer: Sendable { + /// The URL of the server, used to determine the path prefix for + /// registered request handlers. public var serverURL: URL + /// A converter for encoding/decoding data. public var converter: Converter + /// A type capable of handling HTTP requests and returning HTTP responses. public var handler: APIHandler + /// The middlewares to be invoked before the handler receives the request. public var middlewares: [any ServerMiddleware] + /// Internal initializer that takes an initialized converter. internal init( serverURL: URL, converter: Converter, @@ -45,6 +55,7 @@ public struct UniversalServer: Sendable { self.middlewares = middlewares } + /// Creates a new server with the specified parameters. public init( serverURL: URL = .defaultOpenAPIServerURL, handler: APIHandler, @@ -59,6 +70,27 @@ public struct UniversalServer: Sendable { ) } + /// Performs the operation. + /// + /// Should only be called by generated code, not directly. + /// + /// An operation consists of three steps (middlewares happen before 1 and after 3): + /// 1. Convert HTTP request into Input. + /// 2. Invoke the user handler to perform the user logic. + /// 3. Convert Output into an HTTP response. + /// + /// It wraps any thrown errors and attaching appropriate context. + /// - Parameters: + /// - request: The HTTP request. + /// - requestBody: The HTTP request body. + /// - metadata: The HTTP request metadata. + /// - operationID: The OpenAPI operation identifier. + /// - handlerMethod: The user handler method. + /// - deserializer: A closure that creates an Input value from the + /// provided HTTP request. + /// - serializer: A closure that creates an HTTP response from the + /// provided Output value. + /// - Returns: The HTTP response and its body produced by the serializer. public func handle( request: HTTPRequest, requestBody: HTTPBody?, @@ -136,6 +168,9 @@ public struct UniversalServer: Sendable { return try await next(request, requestBody, metadata) } + /// Returns the path with the server URL's path prefix prepended. + /// - Parameter path: The path suffix. + /// - Returns: The path appended to the server URL's path. public func apiPathComponentsWithServerPrefix( _ path: String ) throws -> String { From 140653762e8e51dac2531cb2cd077656115fcf29 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 7 Sep 2023 17:46:58 +0200 Subject: [PATCH 24/55] Fixes --- .../OpenAPIRuntime/Interface/CurrencyTypes.swift | 15 ++++++++------- .../URICoder/Decoding/URIDecoder.swift | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift index b1bc35ea..070f94d3 100644 --- a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift +++ b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift @@ -38,9 +38,9 @@ extension HTTPRequest { /// - Parameters: /// - path: The URL path of the resource. /// - method: The HTTP method. - @_spi(Generated) - public init(soar_path path: String, method: Method) { - self.init(method: method, scheme: nil, authority: nil, path: path) + /// - headerFields: The HTTP header fields. + public init(soar_path path: String, method: Method, headerFields: HTTPFields = .init()) { + self.init(method: method, scheme: nil, authority: nil, path: path, headerFields: headerFields) } /// The query substring of the request's path. @@ -69,10 +69,11 @@ extension HTTPRequest { extension HTTPResponse { /// Creates a new response. - /// - Parameter statusCode: The status code of the response.AsString - @_spi(Generated) - public init(soar_statusCode statusCode: Int) { - self.init(status: .init(code: statusCode)) + /// - Parameters: + /// - statusCode: The status code of the response.AsString + /// - headerFields: The HTTP header fields. + public init(soar_statusCode statusCode: Int, headerFields: HTTPFields = .init()) { + self.init(status: .init(code: statusCode), headerFields: headerFields) } } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift index 0e2105d3..c6b09dcb 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift @@ -97,7 +97,7 @@ extension URIDecoder { func decodeIfPresent( _ type: T.Type = T.self, forKey key: String = "", - from data: String + from data: Substring ) throws -> T? { try withCachedParser(from: data) { decoder in try decoder.decodeIfPresent(type, forKey: key) From 84361a8029b52e62ea15f8c6fee94879c03cf8b6 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 8 Sep 2023 16:10:12 +0200 Subject: [PATCH 25/55] Make iterator next() method mutating --- Sources/OpenAPIRuntime/Interface/HTTPBody.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index 14192b5f..59e13a31 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -640,7 +640,7 @@ extension HTTPBody { } } - public func next() async throws -> Element? { + public mutating func next() async throws -> Element? { try await produceNext() } } From 9f10325448c6d9f783e51dd78f9fd42900be0a89 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 8 Sep 2023 16:26:34 +0200 Subject: [PATCH 26/55] Represent no responses as a nil HTTPBody in server transport and middleware --- .../Interface/ServerTransport.swift | 6 +-- .../Interface/UniversalServer.swift | 47 ++++++++++--------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/Sources/OpenAPIRuntime/Interface/ServerTransport.swift b/Sources/OpenAPIRuntime/Interface/ServerTransport.swift index f9bd3d38..8ee63d78 100644 --- a/Sources/OpenAPIRuntime/Interface/ServerTransport.swift +++ b/Sources/OpenAPIRuntime/Interface/ServerTransport.swift @@ -115,7 +115,7 @@ public protocol ServerTransport { /// as `/file/{name}.zip`. func register( _ handler: @Sendable @escaping (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> ( - HTTPResponse, HTTPBody + HTTPResponse, HTTPBody? ), method: HTTPRequest.Method, path: String @@ -223,6 +223,6 @@ public protocol ServerMiddleware: Sendable { body: HTTPBody?, metadata: ServerRequestMetadata, operationID: String, - next: @Sendable (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody) - ) async throws -> (HTTPResponse, HTTPBody) + next: @Sendable (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody?) + ) async throws -> (HTTPResponse, HTTPBody?) } diff --git a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift index c1e38c38..c7a380ea 100644 --- a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift +++ b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift @@ -99,8 +99,8 @@ import struct Foundation.URLComponents using handlerMethod: @Sendable @escaping (APIHandler) -> ((OperationInput) async throws -> OperationOutput), deserializer: @Sendable @escaping (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> OperationInput, - serializer: @Sendable @escaping (OperationOutput, HTTPRequest) throws -> (HTTPResponse, HTTPBody) - ) async throws -> (HTTPResponse, HTTPBody) where OperationInput: Sendable, OperationOutput: Sendable { + serializer: @Sendable @escaping (OperationOutput, HTTPRequest) throws -> (HTTPResponse, HTTPBody?) + ) async throws -> (HTTPResponse, HTTPBody?) where OperationInput: Sendable, OperationOutput: Sendable { @Sendable func wrappingErrors( work: () async throws -> R, @@ -128,31 +128,32 @@ import struct Foundation.URLComponents underlyingError: error ) } - var next: @Sendable (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody) = { - _request, - _requestBody, - _metadata in - let input: OperationInput = try await wrappingErrors { - try await deserializer(_request, _requestBody, _metadata) - } mapError: { error in - makeError(error: error) - } - let output: OperationOutput = try await wrappingErrors { - let method = handlerMethod(handler) + var next: @Sendable (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody?) = + { + _request, + _requestBody, + _metadata in + let input: OperationInput = try await wrappingErrors { + try await deserializer(_request, _requestBody, _metadata) + } mapError: { error in + makeError(error: error) + } + let output: OperationOutput = try await wrappingErrors { + let method = handlerMethod(handler) + return try await wrappingErrors { + try await method(input) + } mapError: { error in + RuntimeError.handlerFailed(error) + } + } mapError: { error in + makeError(input: input, error: error) + } return try await wrappingErrors { - try await method(input) + try serializer(output, _request) } mapError: { error in - RuntimeError.handlerFailed(error) + makeError(input: input, output: output, error: error) } - } mapError: { error in - makeError(input: input, error: error) - } - return try await wrappingErrors { - try serializer(output, _request) - } mapError: { error in - makeError(input: input, output: output, error: error) } - } for middleware in middlewares.reversed() { let tmp = next next = { From dbda45024dcb6ae284de6f9802b72a4fbd4a1011 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 12 Sep 2023 16:55:01 +0200 Subject: [PATCH 27/55] Feedback: make the AsyncSequences Sendable, which adds the requirement on the inputs as well. --- .../OpenAPIRuntime/Interface/HTTPBody.swift | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index 59e13a31..b8d50c74 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -248,7 +248,7 @@ extension HTTPBody { byteChunks: S, length: Length, iterationBehavior: IterationBehavior - ) where S.Element == ByteChunk { + ) where S.Element == ByteChunk, S: Sendable { self.init( sequence: .init(WrappedSyncSequence(sequence: byteChunks)), length: length, @@ -263,7 +263,7 @@ extension HTTPBody { @inlinable public convenience init( byteChunks: C, length: Length - ) where C.Element == ByteChunk { + ) where C.Element == ByteChunk, C: Sendable { self.init( sequence: .init(WrappedSyncSequence(sequence: byteChunks)), length: length, @@ -275,7 +275,7 @@ extension HTTPBody { /// - byteChunks: A collection of byte chunks. @inlinable public convenience init( byteChunks: C - ) where C.Element == ByteChunk { + ) where C.Element == ByteChunk, C: Sendable { self.init( sequence: .init(WrappedSyncSequence(sequence: byteChunks)), length: .known(byteChunks.map(\.count).reduce(0, +)), @@ -323,7 +323,7 @@ extension HTTPBody { sequence: S, length: HTTPBody.Length, iterationBehavior: IterationBehavior - ) where S.Element == ByteChunk { + ) where S.Element == ByteChunk, S: Sendable { self.init( sequence: .init(sequence), length: length, @@ -430,10 +430,10 @@ extension HTTPBody { /// - Parameters: /// - string: A string to encode as bytes. /// - length: The total length of the body. - @inlinable public convenience init( - string: some StringProtocol, + @inlinable public convenience init( + string: S, length: Length - ) { + ) where S: Sendable { self.init( bytes: string.asBodyChunk, length: length @@ -443,9 +443,9 @@ extension HTTPBody { /// Creates a new body with the provided string encoded as UTF-8 bytes. /// - Parameters: /// - string: A string to encode as bytes. - @inlinable public convenience init( - string: some StringProtocol - ) { + @inlinable public convenience init( + string: S + ) where S: Sendable { self.init( bytes: string.asBodyChunk, length: .known(string.count) @@ -462,7 +462,7 @@ extension HTTPBody { stringChunks: S, length: Length, iterationBehavior: IterationBehavior - ) where S.Element: StringProtocol { + ) where S.Element: StringProtocol, S: Sendable { self.init( byteChunks: stringChunks.map(\.asBodyChunk), length: length, @@ -477,7 +477,7 @@ extension HTTPBody { @inlinable public convenience init( stringChunks: C, length: Length - ) where C.Element: StringProtocol { + ) where C.Element: StringProtocol, C: Sendable { self.init( byteChunks: stringChunks.map(\.asBodyChunk), length: length @@ -489,7 +489,7 @@ extension HTTPBody { /// - stringChunks: A collection of string chunks. @inlinable public convenience init( stringChunks: C - ) where C.Element: StringProtocol { + ) where C.Element: StringProtocol, C: Sendable { self.init( byteChunks: stringChunks.map(\.asBodyChunk) ) @@ -535,7 +535,7 @@ extension HTTPBody { sequence: S, length: HTTPBody.Length, iterationBehavior: IterationBehavior - ) where S.Element: StringProtocol { + ) where S.Element: StringProtocol, S: Sendable { self.init( sequence: .init(sequence.map(\.asBodyChunk)), length: length, @@ -649,7 +649,7 @@ extension HTTPBody { extension HTTPBody { /// A type-erased async sequence that wraps input sequences. - @usableFromInline struct BodySequence: AsyncSequence { + @usableFromInline struct BodySequence: AsyncSequence, Sendable { /// The type of the type-erased iterator. @usableFromInline typealias AsyncIterator = HTTPBody.Iterator @@ -658,11 +658,11 @@ extension HTTPBody { @usableFromInline typealias Element = ByteChunk /// A closure that produces a new iterator. - @usableFromInline let produceIterator: () -> AsyncIterator + @usableFromInline let produceIterator: @Sendable () -> AsyncIterator /// Creates a new sequence. /// - Parameter sequence: The input sequence to type-erase. - @inlinable init(_ sequence: S) where S.Element == Element { + @inlinable init(_ sequence: S) where S.Element == Element, S: Sendable { self.produceIterator = { .init(sequence.makeAsyncIterator()) } @@ -674,8 +674,8 @@ extension HTTPBody { } /// An async sequence wrapper for a sync sequence. - @usableFromInline struct WrappedSyncSequence: AsyncSequence - where S.Element == ByteChunk, S.Iterator.Element == ByteChunk { + @usableFromInline struct WrappedSyncSequence: AsyncSequence, Sendable + where S.Element == ByteChunk, S.Iterator.Element == ByteChunk, S: Sendable { /// The type of the iterator. @usableFromInline typealias AsyncIterator = Iterator @@ -712,7 +712,7 @@ extension HTTPBody { } /// An empty async sequence. - @usableFromInline struct EmptySequence: AsyncSequence { + @usableFromInline struct EmptySequence: AsyncSequence, Sendable { /// The type of the empty iterator. @usableFromInline typealias AsyncIterator = EmptyIterator From fbe8eea38787c2a6d1ef150be666197352d1fc18 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 12 Sep 2023 22:30:11 +0200 Subject: [PATCH 28/55] Feedback: further cleanup of the HTTPBody API --- .../Conversion/CurrencyExtensions.swift | 2 +- .../OpenAPIRuntime/Interface/HTTPBody.swift | 254 ++++++++++++------ .../Conversion/Test_Converter+Client.swift | 4 +- .../Interface/Test_HTTPBody.swift | 28 +- Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 7 +- 5 files changed, 189 insertions(+), 106 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index 3b2c3118..732f4cd0 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -160,7 +160,7 @@ extension Converter { func convertJSONToBodyCodable( _ body: HTTPBody ) async throws -> T { - let data = try await body.collectAsData(upTo: .max) + let data = try await Data(collecting: body, upTo: .max) return try decoder.decode(T.self, from: data) } diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index b8d50c74..35498046 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -32,18 +32,18 @@ import struct Foundation.Data // only for convenience initializers /// Create a body from a byte chunk: /// ```swift /// let bytes: ArraySlice = ... -/// let body = HTTPBody(bytes: bytes) +/// let body = HTTPBody(bytes) /// ``` /// /// Create a body from `Foundation.Data`: /// ```swift /// let data: Foundation.Data = ... -/// let body = HTTPBody(data: data) +/// let body = HTTPBody(data) /// ``` /// /// Create a body from a string: /// ```swift -/// let body = HTTPBody(string: "Hello, world!") +/// let body = HTTPBody("Hello, world!") /// ``` /// /// ## Creating a body from an async sequence @@ -53,7 +53,7 @@ import struct Foundation.Data // only for convenience initializers /// let producingSequence = ... // an AsyncSequence /// let length: HTTPBody.Length = .known(1024) // or .unknown /// let body = HTTPBody( -/// sequence: producingSequence, +/// producingSequence, /// length: length, /// iterationBehavior: .single // or .multiple /// ) @@ -72,7 +72,7 @@ import struct Foundation.Data // only for convenience initializers /// /// ```swift /// let body = HTTPBody( -/// stream: AsyncStream(ArraySlice.self, { continuation in +/// AsyncStream(ArraySlice.self, { continuation in /// continuation.yield([72, 69]) /// continuation.yield([76, 76, 79]) /// continuation.finish() @@ -98,24 +98,24 @@ import struct Foundation.Data // only for convenience initializers /// /// ## Consuming a body as a buffer /// If you need to collect the whole body before processing it, use one of -/// the convenience `collect` methods on `HTTPBody`. +/// the convenience initializers on the target types that take an `HTTPBody`. /// -/// To get all the bytes, use: +/// To get all the bytes, use the initializer on `ArraySlice` or `[UInt8]`: /// /// ```swift -/// let buffer = try await body.collect(upTo: 2 * 1024 * 1024) +/// let buffer = try await ArraySlice(collecting: body, upTo: 2 * 1024 * 1024) /// ``` /// /// Note that you must provide the maximum number of bytes you can buffer in /// memory, in the example above we provide 2 MB. If more bytes are available, /// the method throws the `TooManyBytesError` to stop the process running out -/// of memory. While discouraged, you can provide `collect(upTo: .max)` to +/// of memory. While discouraged, you can provide `upTo: .max` to /// read all the available bytes, without a limit. /// -/// The body type provides more variants of the `collect` method for commonly +/// The body type provides more variants of the collecting initializer on commonly /// used buffers, such as: -/// - `collectAsData` provides the buffered data as `Foundation.Data` -/// - `collectAsString` provides the buffered data as a string decoded as UTF-8 +/// - `Foundation.Data` +/// - `Swift.String` public final class HTTPBody: @unchecked Sendable { /// The underlying byte chunk type. @@ -175,7 +175,7 @@ public final class HTTPBody: @unchecked Sendable { /// - iterationBehavior: The sequence's iteration behavior, which /// indicates whether the sequence can be iterated multiple times. @usableFromInline init( - sequence: BodySequence, + _ sequence: BodySequence, length: Length, iterationBehavior: IterationBehavior ) { @@ -207,37 +207,73 @@ extension HTTPBody { /// Creates a new empty body. @inlinable public convenience init() { self.init( - sequence: .init(EmptySequence()), + .init(EmptySequence()), length: .known(0), iterationBehavior: .multiple ) } - /// Creates a new body with the provided single byte chunk. + /// Creates a new body with the provided byte chunk. /// - Parameters: /// - bytes: A byte chunk. /// - length: The total length of the body. @inlinable public convenience init( - bytes: ByteChunk, + _ bytes: ByteChunk, length: Length ) { - self.init( - byteChunks: [bytes], - length: length - ) + self.init([bytes], length: length) } - /// Creates a new body with the provided single byte chunk. + /// Creates a new body with the provided byte chunk. /// - Parameter bytes: A byte chunk. @inlinable public convenience init( - bytes: ByteChunk + _ bytes: ByteChunk ) { + self.init([bytes], length: .known(bytes.count)) + } + + /// Creates a new body with the provided byte sequence. + /// - Parameters: + /// - bytes: A byte chunk. + /// - length: The total length of the body. + /// - iterationBehavior: The iteration behavior of the sequence, which + /// indicates whether it can be iterated multiple times. + @inlinable public convenience init( + _ bytes: S, + length: Length, + iterationBehavior: IterationBehavior + ) where S: Sendable, S.Element == UInt8 { + self.init( + [ArraySlice(bytes)], + length: length, + iterationBehavior: iterationBehavior + ) + } + + /// Creates a new body with the provided byte collection. + /// - Parameters: + /// - bytes: A byte chunk. + /// - length: The total length of the body. + @inlinable public convenience init( + _ bytes: C, + length: Length + ) where C: Sendable, C.Element == UInt8 { self.init( - byteChunks: [bytes], - length: .known(bytes.count) + ArraySlice(bytes), + length: length, + iterationBehavior: .multiple ) } + /// Creates a new body with the provided byte collection. + /// - Parameters: + /// - bytes: A byte chunk. + @inlinable public convenience init( + _ bytes: C + ) where C: Sendable, C.Element == UInt8 { + self.init(bytes, length: .known(bytes.count)) + } + /// Creates a new body with the provided sequence of byte chunks. /// - Parameters: /// - byteChunks: A sequence of byte chunks. @@ -245,12 +281,12 @@ extension HTTPBody { /// - iterationBehavior: The iteration behavior of the sequence, which /// indicates whether it can be iterated multiple times. @inlinable public convenience init( - byteChunks: S, + _ byteChunks: S, length: Length, iterationBehavior: IterationBehavior ) where S.Element == ByteChunk, S: Sendable { self.init( - sequence: .init(WrappedSyncSequence(sequence: byteChunks)), + .init(WrappedSyncSequence(sequence: byteChunks)), length: length, iterationBehavior: iterationBehavior ) @@ -261,11 +297,11 @@ extension HTTPBody { /// - byteChunks: A collection of byte chunks. /// - length: The total length of the body. @inlinable public convenience init( - byteChunks: C, + _ byteChunks: C, length: Length ) where C.Element == ByteChunk, C: Sendable { self.init( - sequence: .init(WrappedSyncSequence(sequence: byteChunks)), + .init(WrappedSyncSequence(sequence: byteChunks)), length: length, iterationBehavior: .multiple ) @@ -274,10 +310,10 @@ extension HTTPBody { /// Creates a new body with the provided collection of byte chunks. /// - byteChunks: A collection of byte chunks. @inlinable public convenience init( - byteChunks: C + _ byteChunks: C ) where C.Element == ByteChunk, C: Sendable { self.init( - sequence: .init(WrappedSyncSequence(sequence: byteChunks)), + .init(WrappedSyncSequence(sequence: byteChunks)), length: .known(byteChunks.map(\.count).reduce(0, +)), iterationBehavior: .multiple ) @@ -288,11 +324,11 @@ extension HTTPBody { /// - stream: An async throwing stream that provides the byte chunks. /// - length: The total length of the body. @inlinable public convenience init( - stream: AsyncThrowingStream, + _ stream: AsyncThrowingStream, length: HTTPBody.Length ) { self.init( - sequence: .init(stream), + .init(stream), length: length, iterationBehavior: .single ) @@ -303,11 +339,11 @@ extension HTTPBody { /// - stream: An async stream that provides the byte chunks. /// - length: The total length of the body. @inlinable public convenience init( - stream: AsyncStream, + _ stream: AsyncStream, length: HTTPBody.Length ) { self.init( - sequence: .init(stream), + .init(stream), length: length, iterationBehavior: .single ) @@ -320,12 +356,30 @@ extension HTTPBody { /// - iterationBehavior: The iteration behavior of the sequence, which /// indicates whether it can be iterated multiple times. @inlinable public convenience init( - sequence: S, + _ sequence: S, length: HTTPBody.Length, iterationBehavior: IterationBehavior ) where S.Element == ByteChunk, S: Sendable { self.init( - sequence: .init(sequence), + .init(sequence), + length: length, + iterationBehavior: iterationBehavior + ) + } + + /// Creates a new body with the provided async sequence of byte sequences. + /// - Parameters: + /// - sequence: An async sequence that provides the byte chunks. + /// - length: The total lenght of the body. + /// - iterationBehavior: The iteration behavior of the sequence, which + /// indicates whether it can be iterated multiple times. + @inlinable public convenience init( + _ sequence: S, + length: HTTPBody.Length, + iterationBehavior: IterationBehavior + ) where S.Element == ES, S: Sendable, ES.Element == UInt8 { + self.init( + sequence.map { ArraySlice($0) }, length: length, iterationBehavior: iterationBehavior ) @@ -356,7 +410,7 @@ extension HTTPBody: AsyncSequence { extension HTTPBody { - /// An error thrown by the `collect` function when the body contains more + /// An error thrown by the collecting initializer when the body contains more /// than the maximum allowed number of bytes. private struct TooManyBytesError: Error, CustomStringConvertible, LocalizedError { @@ -372,7 +426,7 @@ extension HTTPBody { } } - /// An error thrown by the `collect` function when another iteration of + /// An error thrown by the collecting initializer when another iteration of /// the body is not allowed. private struct TooManyIterationsError: Error, CustomStringConvertible, LocalizedError { @@ -392,8 +446,8 @@ extension HTTPBody { /// to accumulate in memory before it throws an error. /// - Throws: `TooManyBytesError` if the the sequence contains more /// than `maxBytes`. - /// - Returns: A single byte chunk containing all the accumulated bytes. - public func collect(upTo maxBytes: Int) async throws -> ByteChunk { + /// - Returns: A byte chunk containing all the accumulated bytes. + fileprivate func collect(upTo maxBytes: Int) async throws -> ByteChunk { // As a courtesy, check if another iteration is allowed, and throw // an error instead of fatalError here if the user is trying to @@ -422,6 +476,34 @@ extension HTTPBody { } } +extension HTTPBody.ByteChunk { + /// Creates a byte chunk by accumulating the full body in-memory into a single buffer + /// up to the provided maximum number of bytes and returning it. + /// - Parameters: + /// - body: The HTTP body to collect. + /// - maxBytes: The maximum number of bytes this method is allowed + /// to accumulate in memory before it throws an error. + /// - Throws: `TooManyBytesError` if the the sequence contains more + /// than `maxBytes`. + public init(collecting body: HTTPBody, upTo maxBytes: Int) async throws { + self = try await body.collect(upTo: maxBytes) + } +} + +extension Array where Element == UInt8 { + /// Creates a byte array by accumulating the full body in-memory into a single buffer + /// up to the provided maximum number of bytes and returning it. + /// - Parameters: + /// - body: The HTTP body to collect. + /// - maxBytes: The maximum number of bytes this method is allowed + /// to accumulate in memory before it throws an error. + /// - Throws: `TooManyBytesError` if the the sequence contains more + /// than `maxBytes`. + public init(collecting body: HTTPBody, upTo maxBytes: Int) async throws { + self = try await Array(body.collect(upTo: maxBytes)) + } +} + // MARK: - String-based bodies extension HTTPBody { @@ -431,11 +513,11 @@ extension HTTPBody { /// - string: A string to encode as bytes. /// - length: The total length of the body. @inlinable public convenience init( - string: S, + _ string: S, length: Length ) where S: Sendable { self.init( - bytes: string.asBodyChunk, + ByteChunk.init(string), length: length ) } @@ -444,10 +526,10 @@ extension HTTPBody { /// - Parameters: /// - string: A string to encode as bytes. @inlinable public convenience init( - string: S + _ string: S ) where S: Sendable { self.init( - bytes: string.asBodyChunk, + ByteChunk.init(string), length: .known(string.count) ) } @@ -459,12 +541,12 @@ extension HTTPBody { /// - iterationBehavior: The iteration behavior of the sequence, which /// indicates whether it can be iterated multiple times. @inlinable public convenience init( - stringChunks: S, + _ stringChunks: S, length: Length, iterationBehavior: IterationBehavior ) where S.Element: StringProtocol, S: Sendable { self.init( - byteChunks: stringChunks.map(\.asBodyChunk), + stringChunks.map(ByteChunk.init), length: length, iterationBehavior: iterationBehavior ) @@ -475,24 +557,19 @@ extension HTTPBody { /// - stringChunks: A collection of string chunks. /// - length: The total length of the body. @inlinable public convenience init( - stringChunks: C, + _ stringChunks: C, length: Length ) where C.Element: StringProtocol, C: Sendable { - self.init( - byteChunks: stringChunks.map(\.asBodyChunk), - length: length - ) + self.init(stringChunks.map(ByteChunk.init), length: length) } /// Creates a new body with the provided strings encoded as UTF-8 bytes. /// - Parameters: /// - stringChunks: A collection of string chunks. @inlinable public convenience init( - stringChunks: C + _ stringChunks: C ) where C.Element: StringProtocol, C: Sendable { - self.init( - byteChunks: stringChunks.map(\.asBodyChunk) - ) + self.init(stringChunks.map(ByteChunk.init)) } /// Creates a new body with the provided async throwing stream of strings. @@ -500,11 +577,11 @@ extension HTTPBody { /// - stream: An async throwing stream that provides the string chunks. /// - length: The total length of the body. @inlinable public convenience init( - stream: AsyncThrowingStream, + _ stream: AsyncThrowingStream, length: HTTPBody.Length ) { self.init( - sequence: .init(stream.map(\.asBodyChunk)), + .init(stream.map(ByteChunk.init)), length: length, iterationBehavior: .single ) @@ -515,11 +592,11 @@ extension HTTPBody { /// - stream: An async stream that provides the string chunks. /// - length: The total length of the body. @inlinable public convenience init( - stream: AsyncStream, + _ stream: AsyncStream, length: HTTPBody.Length ) { self.init( - sequence: .init(stream.map(\.asBodyChunk)), + .init(stream.map(ByteChunk.init)), length: length, iterationBehavior: .single ) @@ -532,40 +609,41 @@ extension HTTPBody { /// - iterationBehavior: The iteration behavior of the sequence, which /// indicates whether it can be iterated multiple times. @inlinable public convenience init( - sequence: S, + _ sequence: S, length: HTTPBody.Length, iterationBehavior: IterationBehavior ) where S.Element: StringProtocol, S: Sendable { self.init( - sequence: .init(sequence.map(\.asBodyChunk)), + .init(sequence.map(ByteChunk.init)), length: length, iterationBehavior: iterationBehavior ) } } -extension StringProtocol { +extension HTTPBody.ByteChunk { - /// Returns the string as a byte chunk compatible with the `HTTPBody` type. - @inlinable var asBodyChunk: HTTPBody.ByteChunk { - Array(utf8)[...] + /// Creates a byte chunk compatible with the `HTTPBody` type from the provided string. + /// - Parameter string: The string to encode. + @inlinable init(_ string: S) where S: Sendable { + self = Array(string.utf8)[...] } } -extension HTTPBody { - - /// Accumulates the full body in-memory into a single buffer up to - /// the provided maximum number of bytes, converts it to string from - /// the UTF-8 bytes, and returns it. +extension String { + /// Creates a string by accumulating the full body in-memory into a single buffer up to + /// the provided maximum number of bytes, converting it to string using the provided encoding. /// - Parameters: + /// - body: The HTTP body to collect. /// - maxBytes: The maximum number of bytes this method is allowed /// to accumulate in memory before it throws an error. - /// - Throws: `TooManyBytesError` if the the body contains more + /// - Throws: `TooManyBytesError` if the the sequence contains more /// than `maxBytes`. - /// - Returns: The string decoded from the UTF-8 bytes. - public func collectAsString(upTo maxBytes: Int) async throws -> String { - let bytes: ByteChunk = try await collect(upTo: maxBytes) - return String(decoding: bytes, as: UTF8.self) + public init(collecting body: HTTPBody, upTo maxBytes: Int) async throws { + self = try await String( + decoding: body.collect(upTo: maxBytes), + as: UTF8.self + ) } } @@ -573,7 +651,7 @@ extension HTTPBody { extension HTTPBody: ExpressibleByStringLiteral { public convenience init(stringLiteral value: String) { - self.init(string: value) + self.init(value) } } @@ -581,15 +659,15 @@ extension HTTPBody { /// Creates a new body from the provided array of bytes. /// - Parameter bytes: An array of bytes. - @inlinable public convenience init(bytes: [UInt8]) { - self.init(bytes: bytes[...]) + @inlinable public convenience init(_ bytes: [UInt8]) { + self.init(bytes[...]) } } extension HTTPBody: ExpressibleByArrayLiteral { public typealias ArrayLiteralElement = UInt8 public convenience init(arrayLiteral elements: UInt8...) { - self.init(bytes: elements) + self.init(elements) } } @@ -598,21 +676,21 @@ extension HTTPBody { /// Creates a new body from the provided data chunk. /// - Parameter data: A single data chunk. public convenience init(data: Data) { - self.init(bytes: ArraySlice(data)) + self.init(ArraySlice(data)) } +} - /// Accumulates the full body in-memory into a single buffer up to - /// the provided maximum number of bytes, converts it to `Data`, and - /// returns it. +extension Data { + /// Creates a string by accumulating the full body in-memory into a single buffer up to + /// the provided maximum number of bytes and converting it to `Data`. /// - Parameters: + /// - body: The HTTP body to collect. /// - maxBytes: The maximum number of bytes this method is allowed /// to accumulate in memory before it throws an error. - /// - Throws: `TooManyBytesError` if the the body contains more + /// - Throws: `TooManyBytesError` if the the sequence contains more /// than `maxBytes`. - /// - Returns: The accumulated bytes wrapped in `Data`. - public func collectAsData(upTo maxBytes: Int) async throws -> Data { - let bytes: ByteChunk = try await collect(upTo: maxBytes) - return Data(bytes) + public init(collecting body: HTTPBody, upTo maxBytes: Int) async throws { + self = try await Data(body.collect(upTo: maxBytes)) } } diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift index b1aa8055..e384182e 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -190,7 +190,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { func test_setRequiredRequestBodyAsBinary_data() async throws { var headerFields: HTTPFields = [:] let body = try converter.setRequiredRequestBodyAsBinary( - .init(string: testString), + .init(testString), headerFields: &headerFields, contentType: "application/octet-stream" ) @@ -217,7 +217,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { func test_getResponseBodyAsBinary_data() async throws { let value: HTTPBody = try converter.getResponseBodyAsBinary( HTTPBody.self, - from: .init(string: testString), + from: .init(testString), transforming: { $0 } ) try await XCTAssertEqualStringifiedData(value, testString) diff --git a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift index f8a5a9ff..fd36ae2e 100644 --- a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift +++ b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift @@ -21,7 +21,7 @@ final class Test_Body: Test_Runtime { // A single string. do { - let body: HTTPBody = HTTPBody(string: "hello") + let body: HTTPBody = HTTPBody("hello") try await _testConsume( body, expected: "hello" @@ -39,7 +39,7 @@ final class Test_Body: Test_Runtime { // A sequence of strings. do { - let body: HTTPBody = HTTPBody(stringChunks: ["hel", "lo"]) + let body: HTTPBody = HTTPBody(["hel", "lo"]) try await _testConsume( body, expected: "hello" @@ -48,7 +48,7 @@ final class Test_Body: Test_Runtime { // A single substring. do { - let body: HTTPBody = HTTPBody(string: "hello") + let body: HTTPBody = HTTPBody("hello") try await _testConsume( body, expected: "hello" @@ -57,7 +57,7 @@ final class Test_Body: Test_Runtime { // A sequence of substrings. do { - let body: HTTPBody = HTTPBody(stringChunks: [ + let body: HTTPBody = HTTPBody([ "hel", "lo", ]) @@ -69,7 +69,7 @@ final class Test_Body: Test_Runtime { // A single array of bytes. do { - let body: HTTPBody = HTTPBody(bytes: [0]) + let body: HTTPBody = HTTPBody([0]) try await _testConsume( body, expected: [0] @@ -96,7 +96,7 @@ final class Test_Body: Test_Runtime { // A sequence of arrays of bytes. do { - let body: HTTPBody = HTTPBody(byteChunks: [[0], [1]]) + let body: HTTPBody = HTTPBody([[0], [1]]) try await _testConsume( body, expected: [0, 1] @@ -105,7 +105,7 @@ final class Test_Body: Test_Runtime { // A single slice of an array of bytes. do { - let body: HTTPBody = HTTPBody(bytes: [0][...]) + let body: HTTPBody = HTTPBody([0][...]) try await _testConsume( body, expected: [0][...] @@ -114,7 +114,7 @@ final class Test_Body: Test_Runtime { // A sequence of slices of an array of bytes. do { - let body: HTTPBody = HTTPBody(byteChunks: [ + let body: HTTPBody = HTTPBody([ [0][...], [1][...], ]) @@ -127,7 +127,7 @@ final class Test_Body: Test_Runtime { // An async throwing stream. do { let body: HTTPBody = HTTPBody( - stream: AsyncThrowingStream( + AsyncThrowingStream( String.self, { continuation in continuation.yield("hel") @@ -146,7 +146,7 @@ final class Test_Body: Test_Runtime { // An async stream. do { let body: HTTPBody = HTTPBody( - stream: AsyncStream( + AsyncStream( String.self, { continuation in continuation.yield("hel") @@ -174,7 +174,7 @@ final class Test_Body: Test_Runtime { ) .map { $0 } let body: HTTPBody = HTTPBody( - sequence: sequence, + sequence, length: .known(5), iterationBehavior: .single ) @@ -196,7 +196,7 @@ final class Test_Body: Test_Runtime { ) .map { $0 } let body: HTTPBody = HTTPBody( - sequence: sequence, + sequence, length: .known(5), iterationBehavior: .single ) @@ -215,7 +215,7 @@ extension Test_Body { file: StaticString = #file, line: UInt = #line ) async throws { - let output = try await body.collect(upTo: .max) + let output = try await ArraySlice(collecting: body, upTo: .max) XCTAssertEqual(output, expected, file: file, line: line) } @@ -225,7 +225,7 @@ extension Test_Body { file: StaticString = #file, line: UInt = #line ) async throws { - let output = try await body.collectAsString(upTo: .max) + let output = try await String(collecting: body, upTo: .max) XCTAssertEqual(output, expected.description, file: file, line: line) } } diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index 98e77c45..22479219 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -213,6 +213,11 @@ public func XCTAssertEqualStringifiedData( file: StaticString = #filePath, line: UInt = #line ) async throws { - let data = try await expression1()?.collectAsData(upTo: .max) + let data: Data + if let body = try expression1() { + data = try await Data(collecting: body, upTo: .max) + } else { + data = .init() + } XCTAssertEqualStringifiedData(data, try expression2(), message(), file: file, line: line) } From 8b432b2937cd543d13042ccd2496d9e6fb3bac3e Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 12 Sep 2023 22:44:53 +0200 Subject: [PATCH 29/55] Remove unnecessary initializers --- .../OpenAPIRuntime/Interface/HTTPBody.swift | 117 ++++-------------- .../Interface/Test_HTTPBody.swift | 42 ------- 2 files changed, 26 insertions(+), 133 deletions(-) diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index 35498046..e99877f0 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -106,16 +106,16 @@ import struct Foundation.Data // only for convenience initializers /// let buffer = try await ArraySlice(collecting: body, upTo: 2 * 1024 * 1024) /// ``` /// -/// Note that you must provide the maximum number of bytes you can buffer in -/// memory, in the example above we provide 2 MB. If more bytes are available, -/// the method throws the `TooManyBytesError` to stop the process running out -/// of memory. While discouraged, you can provide `upTo: .max` to -/// read all the available bytes, without a limit. -/// /// The body type provides more variants of the collecting initializer on commonly /// used buffers, such as: /// - `Foundation.Data` /// - `Swift.String` +/// +/// > Important: You must provide the maximum number of bytes you can buffer in +/// memory, in the example above we provide 2 MB. If more bytes are available, +/// the method throws the `TooManyBytesError` to stop the process running out +/// of memory. While discouraged, you can provide `upTo: .max` to +/// read all the available bytes, without a limit. public final class HTTPBody: @unchecked Sendable { /// The underlying byte chunk type. @@ -183,6 +183,24 @@ public final class HTTPBody: @unchecked Sendable { self.length = length self.iterationBehavior = iterationBehavior } + + /// Creates a new body with the provided sequence of byte chunks. + /// - Parameters: + /// - byteChunks: A sequence of byte chunks. + /// - length: The total length of the body. + /// - iterationBehavior: The iteration behavior of the sequence, which + /// indicates whether it can be iterated multiple times. + @usableFromInline convenience init( + _ byteChunks: S, + length: Length, + iterationBehavior: IterationBehavior + ) where S.Element == ByteChunk, S: Sendable { + self.init( + .init(WrappedSyncSequence(sequence: byteChunks)), + length: length, + iterationBehavior: iterationBehavior + ) + } } extension HTTPBody: Equatable { @@ -221,7 +239,7 @@ extension HTTPBody { _ bytes: ByteChunk, length: Length ) { - self.init([bytes], length: length) + self.init([bytes], length: length, iterationBehavior: .multiple) } /// Creates a new body with the provided byte chunk. @@ -229,7 +247,7 @@ extension HTTPBody { @inlinable public convenience init( _ bytes: ByteChunk ) { - self.init([bytes], length: .known(bytes.count)) + self.init([bytes], length: .known(bytes.count), iterationBehavior: .multiple) } /// Creates a new body with the provided byte sequence. @@ -274,51 +292,6 @@ extension HTTPBody { self.init(bytes, length: .known(bytes.count)) } - /// Creates a new body with the provided sequence of byte chunks. - /// - Parameters: - /// - byteChunks: A sequence of byte chunks. - /// - length: The total length of the body. - /// - iterationBehavior: The iteration behavior of the sequence, which - /// indicates whether it can be iterated multiple times. - @inlinable public convenience init( - _ byteChunks: S, - length: Length, - iterationBehavior: IterationBehavior - ) where S.Element == ByteChunk, S: Sendable { - self.init( - .init(WrappedSyncSequence(sequence: byteChunks)), - length: length, - iterationBehavior: iterationBehavior - ) - } - - /// Creates a new body with the provided collection of byte chunks. - /// - Parameters: - /// - byteChunks: A collection of byte chunks. - /// - length: The total length of the body. - @inlinable public convenience init( - _ byteChunks: C, - length: Length - ) where C.Element == ByteChunk, C: Sendable { - self.init( - .init(WrappedSyncSequence(sequence: byteChunks)), - length: length, - iterationBehavior: .multiple - ) - } - - /// Creates a new body with the provided collection of byte chunks. - /// - byteChunks: A collection of byte chunks. - @inlinable public convenience init( - _ byteChunks: C - ) where C.Element == ByteChunk, C: Sendable { - self.init( - .init(WrappedSyncSequence(sequence: byteChunks)), - length: .known(byteChunks.map(\.count).reduce(0, +)), - iterationBehavior: .multiple - ) - } - /// Creates a new body with the provided async throwing stream. /// - Parameters: /// - stream: An async throwing stream that provides the byte chunks. @@ -534,44 +507,6 @@ extension HTTPBody { ) } - /// Creates a new body with the provided strings encoded as UTF-8 bytes. - /// - Parameters: - /// - stringChunks: A sequence of string chunks. - /// - length: The total length of the body. - /// - iterationBehavior: The iteration behavior of the sequence, which - /// indicates whether it can be iterated multiple times. - @inlinable public convenience init( - _ stringChunks: S, - length: Length, - iterationBehavior: IterationBehavior - ) where S.Element: StringProtocol, S: Sendable { - self.init( - stringChunks.map(ByteChunk.init), - length: length, - iterationBehavior: iterationBehavior - ) - } - - /// Creates a new body with the provided strings encoded as UTF-8 bytes. - /// - Parameters: - /// - stringChunks: A collection of string chunks. - /// - length: The total length of the body. - @inlinable public convenience init( - _ stringChunks: C, - length: Length - ) where C.Element: StringProtocol, C: Sendable { - self.init(stringChunks.map(ByteChunk.init), length: length) - } - - /// Creates a new body with the provided strings encoded as UTF-8 bytes. - /// - Parameters: - /// - stringChunks: A collection of string chunks. - @inlinable public convenience init( - _ stringChunks: C - ) where C.Element: StringProtocol, C: Sendable { - self.init(stringChunks.map(ByteChunk.init)) - } - /// Creates a new body with the provided async throwing stream of strings. /// - Parameters: /// - stream: An async throwing stream that provides the string chunks. diff --git a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift index fd36ae2e..653771ec 100644 --- a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift +++ b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift @@ -37,15 +37,6 @@ final class Test_Body: Test_Runtime { ) } - // A sequence of strings. - do { - let body: HTTPBody = HTTPBody(["hel", "lo"]) - try await _testConsume( - body, - expected: "hello" - ) - } - // A single substring. do { let body: HTTPBody = HTTPBody("hello") @@ -55,18 +46,6 @@ final class Test_Body: Test_Runtime { ) } - // A sequence of substrings. - do { - let body: HTTPBody = HTTPBody([ - "hel", - "lo", - ]) - try await _testConsume( - body, - expected: "hello"[...] - ) - } - // A single array of bytes. do { let body: HTTPBody = HTTPBody([0]) @@ -94,15 +73,6 @@ final class Test_Body: Test_Runtime { ) } - // A sequence of arrays of bytes. - do { - let body: HTTPBody = HTTPBody([[0], [1]]) - try await _testConsume( - body, - expected: [0, 1] - ) - } - // A single slice of an array of bytes. do { let body: HTTPBody = HTTPBody([0][...]) @@ -112,18 +82,6 @@ final class Test_Body: Test_Runtime { ) } - // A sequence of slices of an array of bytes. - do { - let body: HTTPBody = HTTPBody([ - [0][...], - [1][...], - ]) - try await _testConsume( - body, - expected: [0, 1][...] - ) - } - // An async throwing stream. do { let body: HTTPBody = HTTPBody( From 3f59f4488c8f8aeba11cf3e6eb707e4b97093db3 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 13 Sep 2023 16:34:54 +0200 Subject: [PATCH 30/55] Review feedback: --- .../OpenAPIRuntime/Interface/HTTPBody.swift | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index e99877f0..89f4c099 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -190,11 +190,11 @@ public final class HTTPBody: @unchecked Sendable { /// - length: The total length of the body. /// - iterationBehavior: The iteration behavior of the sequence, which /// indicates whether it can be iterated multiple times. - @usableFromInline convenience init( - _ byteChunks: S, + @usableFromInline convenience init( + _ byteChunks: some Sequence & Sendable, length: Length, iterationBehavior: IterationBehavior - ) where S.Element == ByteChunk, S: Sendable { + ) { self.init( .init(WrappedSyncSequence(sequence: byteChunks)), length: length, @@ -256,11 +256,11 @@ extension HTTPBody { /// - length: The total length of the body. /// - iterationBehavior: The iteration behavior of the sequence, which /// indicates whether it can be iterated multiple times. - @inlinable public convenience init( - _ bytes: S, + @inlinable public convenience init( + _ bytes: some Sequence & Sendable, length: Length, iterationBehavior: IterationBehavior - ) where S: Sendable, S.Element == UInt8 { + ) { self.init( [ArraySlice(bytes)], length: length, @@ -272,10 +272,10 @@ extension HTTPBody { /// - Parameters: /// - bytes: A byte chunk. /// - length: The total length of the body. - @inlinable public convenience init( - _ bytes: C, + @inlinable public convenience init( + _ bytes: some Collection & Sendable, length: Length - ) where C: Sendable, C.Element == UInt8 { + ) { self.init( ArraySlice(bytes), length: length, @@ -286,9 +286,9 @@ extension HTTPBody { /// Creates a new body with the provided byte collection. /// - Parameters: /// - bytes: A byte chunk. - @inlinable public convenience init( - _ bytes: C - ) where C: Sendable, C.Element == UInt8 { + @inlinable public convenience init( + _ bytes: some Collection & Sendable + ) { self.init(bytes, length: .known(bytes.count)) } @@ -328,11 +328,11 @@ extension HTTPBody { /// - length: The total lenght of the body. /// - iterationBehavior: The iteration behavior of the sequence, which /// indicates whether it can be iterated multiple times. - @inlinable public convenience init( - _ sequence: S, + @inlinable public convenience init( + _ sequence: Bytes, length: HTTPBody.Length, iterationBehavior: IterationBehavior - ) where S.Element == ByteChunk, S: Sendable { + ) where Bytes.Element == ByteChunk, Bytes: Sendable { self.init( .init(sequence), length: length, @@ -346,11 +346,11 @@ extension HTTPBody { /// - length: The total lenght of the body. /// - iterationBehavior: The iteration behavior of the sequence, which /// indicates whether it can be iterated multiple times. - @inlinable public convenience init( - _ sequence: S, + @inlinable public convenience init( + _ sequence: Bytes, length: HTTPBody.Length, iterationBehavior: IterationBehavior - ) where S.Element == ES, S: Sendable, ES.Element == UInt8 { + ) where Bytes: Sendable, Bytes.Element: Sequence, Bytes.Element.Element == UInt8 { self.init( sequence.map { ArraySlice($0) }, length: length, @@ -485,10 +485,10 @@ extension HTTPBody { /// - Parameters: /// - string: A string to encode as bytes. /// - length: The total length of the body. - @inlinable public convenience init( - _ string: S, + @inlinable public convenience init( + _ string: some StringProtocol & Sendable, length: Length - ) where S: Sendable { + ) { self.init( ByteChunk.init(string), length: length @@ -498,9 +498,9 @@ extension HTTPBody { /// Creates a new body with the provided string encoded as UTF-8 bytes. /// - Parameters: /// - string: A string to encode as bytes. - @inlinable public convenience init( - _ string: S - ) where S: Sendable { + @inlinable public convenience init( + _ string: some StringProtocol & Sendable + ) { self.init( ByteChunk.init(string), length: .known(string.count) @@ -512,7 +512,7 @@ extension HTTPBody { /// - stream: An async throwing stream that provides the string chunks. /// - length: The total length of the body. @inlinable public convenience init( - _ stream: AsyncThrowingStream, + _ stream: AsyncThrowingStream, length: HTTPBody.Length ) { self.init( @@ -527,7 +527,7 @@ extension HTTPBody { /// - stream: An async stream that provides the string chunks. /// - length: The total length of the body. @inlinable public convenience init( - _ stream: AsyncStream, + _ stream: AsyncStream, length: HTTPBody.Length ) { self.init( @@ -543,11 +543,11 @@ extension HTTPBody { /// - length: The total lenght of the body. /// - iterationBehavior: The iteration behavior of the sequence, which /// indicates whether it can be iterated multiple times. - @inlinable public convenience init( - _ sequence: S, + @inlinable public convenience init( + _ sequence: Strings, length: HTTPBody.Length, iterationBehavior: IterationBehavior - ) where S.Element: StringProtocol, S: Sendable { + ) where Strings.Element: StringProtocol & Sendable, Strings: Sendable { self.init( .init(sequence.map(ByteChunk.init)), length: length, @@ -560,7 +560,7 @@ extension HTTPBody.ByteChunk { /// Creates a byte chunk compatible with the `HTTPBody` type from the provided string. /// - Parameter string: The string to encode. - @inlinable init(_ string: S) where S: Sendable { + @inlinable init(_ string: some StringProtocol & Sendable) { self = Array(string.utf8)[...] } } From 16917cea56d8353c4e3d9a166e044d9c21b1d345 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 13 Sep 2023 17:01:19 +0200 Subject: [PATCH 31/55] Review feedback: make response body optional --- .../Conversion/Converter+Client.swift | 14 ++++++++++---- Sources/OpenAPIRuntime/Errors/RuntimeError.swift | 3 +++ .../OpenAPIRuntime/Interface/ClientTransport.swift | 14 +++++++------- .../OpenAPIRuntime/Interface/ServerTransport.swift | 4 ++-- .../OpenAPIRuntime/Interface/UniversalClient.swift | 6 +++--- Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 8 ++++---- 6 files changed, 29 insertions(+), 20 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index b0f5d9a1..2b30de70 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -139,10 +139,13 @@ extension Converter { // | client | get | response body | JSON | required | getResponseBodyAsJSON | public func getResponseBodyAsJSON( _ type: T.Type, - from data: HTTPBody, + from data: HTTPBody?, transforming transform: (T) -> C ) async throws -> C { - try await getBufferingResponseBody( + guard let data else { + throw RuntimeError.missingRequiredResponseBody + } + return try await getBufferingResponseBody( type, from: data, transforming: transform, @@ -153,10 +156,13 @@ extension Converter { // | client | get | response body | binary | required | getResponseBodyAsBinary | public func getResponseBodyAsBinary( _ type: HTTPBody.Type, - from data: HTTPBody, + from data: HTTPBody?, transforming transform: (HTTPBody) -> C ) throws -> C { - try getResponseBody( + guard let data else { + throw RuntimeError.missingRequiredResponseBody + } + return try getResponseBody( type, from: data, transforming: transform, diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index 3a2c0939..4304b4cd 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -49,6 +49,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret // Body case missingRequiredRequestBody + case missingRequiredResponseBody // Transport/Handler case transportFailed(any Error) @@ -89,6 +90,8 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret return "Missing required query parameter named: \(name)" case .missingRequiredRequestBody: return "Missing required request body" + case .missingRequiredResponseBody: + return "Missing required response body" case .transportFailed(let underlyingError): return "Transport failed with error: \(underlyingError.localizedDescription)" case .handlerFailed(let underlyingError): diff --git a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift index 1fb5b3b8..9ac1bf27 100644 --- a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift +++ b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift @@ -100,10 +100,10 @@ import struct Foundation.URL /// body: HTTPBody?, /// baseURL: URL, /// operationID: String -/// ) async throws -> (HTTPResponse, HTTPBody) { +/// ) async throws -> (HTTPResponse, HTTPBody?) { /// ( /// HTTPResponse(status: isHealthy ? .ok : .internalServerError), -/// HTTPBody() +/// nil /// ) /// } /// } @@ -141,7 +141,7 @@ public protocol ClientTransport: Sendable { body: HTTPBody?, baseURL: URL, operationID: String - ) async throws -> (HTTPResponse, HTTPBody) + ) async throws -> (HTTPResponse, HTTPBody?) } /// A type that intercepts HTTP requests and responses. @@ -216,8 +216,8 @@ public protocol ClientTransport: Sendable { /// body: HTTPBody?, /// baseURL: URL, /// operationID: String, -/// next: (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody) -/// ) async throws -> (HTTPResponse, HTTPBody) { +/// next: (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) +/// ) async throws -> (HTTPResponse, HTTPBody?) { /// var request = request /// request.headerFields[.authorization] = "Bearer \(bearerToken)" /// return try await next(request, body, baseURL) @@ -246,6 +246,6 @@ public protocol ClientMiddleware: Sendable { body: HTTPBody?, baseURL: URL, operationID: String, - next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody) - ) async throws -> (HTTPResponse, HTTPBody) + next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) + ) async throws -> (HTTPResponse, HTTPBody?) } diff --git a/Sources/OpenAPIRuntime/Interface/ServerTransport.swift b/Sources/OpenAPIRuntime/Interface/ServerTransport.swift index 8ee63d78..44a93c1c 100644 --- a/Sources/OpenAPIRuntime/Interface/ServerTransport.swift +++ b/Sources/OpenAPIRuntime/Interface/ServerTransport.swift @@ -189,8 +189,8 @@ public protocol ServerTransport { /// body: HTTPBody?, /// metadata: ServerRequestMetadata, /// operationID: String, -/// next: (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody) -/// ) async throws -> (HTTPResponse, HTTPBody) { +/// next: (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody?) +/// ) async throws -> (HTTPResponse, HTTPBody?) { /// print(">>>: \(request.method.rawValue) \(request.soar_pathOnly)") /// do { /// let (response, responseBody) = try await next(request, body, metadata) diff --git a/Sources/OpenAPIRuntime/Interface/UniversalClient.swift b/Sources/OpenAPIRuntime/Interface/UniversalClient.swift index dab7ab64..019ca728 100644 --- a/Sources/OpenAPIRuntime/Interface/UniversalClient.swift +++ b/Sources/OpenAPIRuntime/Interface/UniversalClient.swift @@ -87,7 +87,7 @@ import Foundation input: OperationInput, forOperation operationID: String, serializer: @Sendable (OperationInput) throws -> (HTTPRequest, HTTPBody?), - deserializer: @Sendable (HTTPResponse, HTTPBody) async throws -> OperationOutput + deserializer: @Sendable (HTTPResponse, HTTPBody?) async throws -> OperationOutput ) async throws -> OperationOutput where OperationInput: Sendable, OperationOutput: Sendable { @Sendable func wrappingErrors( @@ -125,8 +125,8 @@ import Foundation } mapError: { error in makeError(error: error) } - let (response, responseBody): (HTTPResponse, HTTPBody) = try await wrappingErrors { - var next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody) = { + let (response, responseBody): (HTTPResponse, HTTPBody?) = try await wrappingErrors { + var next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) = { (_request, _body, _url) in try await wrappingErrors { try await transport.send( diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index 22479219..313df8e7 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -158,8 +158,8 @@ struct AuthenticationMiddleware: ClientMiddleware { body: HTTPBody?, baseURL: URL, operationID: String, - next: (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody) - ) async throws -> (HTTPResponse, HTTPBody) { + next: (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) + ) async throws -> (HTTPResponse, HTTPBody?) { var request = request request.headerFields[.authorization] = "Bearer \(token)" return try await next(request, body, baseURL) @@ -173,8 +173,8 @@ struct PrintingMiddleware: ClientMiddleware { body: HTTPBody?, baseURL: URL, operationID: String, - next: (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody) - ) async throws -> (HTTPResponse, HTTPBody) { + next: (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) + ) async throws -> (HTTPResponse, HTTPBody?) { print("Sending \(request.method) \(request.path ?? "")") do { let (response, responseBody) = try await next(request, body, baseURL) From 483082137547c3f709b373d4956381e5dd24dd77 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 13 Sep 2023 17:12:04 +0200 Subject: [PATCH 32/55] A few more changes --- Sources/OpenAPIRuntime/Interface/HTTPBody.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index 89f4c099..c3253c94 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -675,7 +675,7 @@ extension HTTPBody { /// Creates a new sequence. /// - Parameter sequence: The input sequence to type-erase. - @inlinable init(_ sequence: S) where S.Element == Element, S: Sendable { + @inlinable init(_ sequence: Bytes) where Bytes.Element == Element, Bytes: Sendable { self.produceIterator = { .init(sequence.makeAsyncIterator()) } @@ -687,8 +687,8 @@ extension HTTPBody { } /// An async sequence wrapper for a sync sequence. - @usableFromInline struct WrappedSyncSequence: AsyncSequence, Sendable - where S.Element == ByteChunk, S.Iterator.Element == ByteChunk, S: Sendable { + @usableFromInline struct WrappedSyncSequence: AsyncSequence, Sendable + where Bytes.Element == ByteChunk, Bytes.Iterator.Element == ByteChunk, Bytes: Sendable { /// The type of the iterator. @usableFromInline typealias AsyncIterator = Iterator From 213addfe02549c0eeaeacbc6348e4da3cc761cc3 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 14 Sep 2023 08:49:48 +0200 Subject: [PATCH 33/55] Wording and a refactor fix --- Sources/OpenAPIRuntime/Interface/HTTPBody.swift | 14 +++++++------- .../Decoding/URIValueFromNodeDecoder+Unkeyed.swift | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index c3253c94..4887f8ee 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -417,7 +417,7 @@ extension HTTPBody { /// - Parameters: /// - maxBytes: The maximum number of bytes this method is allowed /// to accumulate in memory before it throws an error. - /// - Throws: `TooManyBytesError` if the the sequence contains more + /// - Throws: `TooManyBytesError` if the body contains more /// than `maxBytes`. /// - Returns: A byte chunk containing all the accumulated bytes. fileprivate func collect(upTo maxBytes: Int) async throws -> ByteChunk { @@ -456,7 +456,7 @@ extension HTTPBody.ByteChunk { /// - body: The HTTP body to collect. /// - maxBytes: The maximum number of bytes this method is allowed /// to accumulate in memory before it throws an error. - /// - Throws: `TooManyBytesError` if the the sequence contains more + /// - Throws: `TooManyBytesError` if the body contains more /// than `maxBytes`. public init(collecting body: HTTPBody, upTo maxBytes: Int) async throws { self = try await body.collect(upTo: maxBytes) @@ -470,7 +470,7 @@ extension Array where Element == UInt8 { /// - body: The HTTP body to collect. /// - maxBytes: The maximum number of bytes this method is allowed /// to accumulate in memory before it throws an error. - /// - Throws: `TooManyBytesError` if the the sequence contains more + /// - Throws: `TooManyBytesError` if the body contains more /// than `maxBytes`. public init(collecting body: HTTPBody, upTo maxBytes: Int) async throws { self = try await Array(body.collect(upTo: maxBytes)) @@ -572,7 +572,7 @@ extension String { /// - body: The HTTP body to collect. /// - maxBytes: The maximum number of bytes this method is allowed /// to accumulate in memory before it throws an error. - /// - Throws: `TooManyBytesError` if the the sequence contains more + /// - Throws: `TooManyBytesError` if the body contains more /// than `maxBytes`. public init(collecting body: HTTPBody, upTo maxBytes: Int) async throws { self = try await String( @@ -622,7 +622,7 @@ extension Data { /// - body: The HTTP body to collect. /// - maxBytes: The maximum number of bytes this method is allowed /// to accumulate in memory before it throws an error. - /// - Throws: `TooManyBytesError` if the the sequence contains more + /// - Throws: `TooManyBytesError` if the body contains more /// than `maxBytes`. public init(collecting body: HTTPBody, upTo maxBytes: Int) async throws { self = try await Data(body.collect(upTo: maxBytes)) @@ -711,11 +711,11 @@ extension HTTPBody { } /// The underlying sync sequence. - @usableFromInline let sequence: S + @usableFromInline let sequence: Bytes /// Creates a new async sequence with the provided sync sequence. /// - Parameter sequence: The sync sequence to wrap. - @inlinable init(sequence: S) { + @inlinable init(sequence: Bytes) { self.sequence = sequence } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift index abb55d7a..5f0d78be 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift @@ -54,7 +54,7 @@ extension URIUnkeyedDecodingContainer { return try work() } - /// Returns the the current item in the underlying array and increments + /// Returns the current item in the underlying array and increments /// the index. /// - Returns: The next value found. /// - Throws: An error if the container ran out of items. From c3149142b6059106ef2e871aa40411eff2070fb2 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 18 Sep 2023 15:42:33 +0200 Subject: [PATCH 34/55] Fix a few more missing sendable annotations --- Sources/OpenAPIRuntime/Interface/HTTPBody.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index 4887f8ee..3cb20679 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -350,7 +350,7 @@ extension HTTPBody { _ sequence: Bytes, length: HTTPBody.Length, iterationBehavior: IterationBehavior - ) where Bytes: Sendable, Bytes.Element: Sequence, Bytes.Element.Element == UInt8 { + ) where Bytes: Sendable, Bytes.Element: Sequence & Sendable, Bytes.Element.Element == UInt8 { self.init( sequence.map { ArraySlice($0) }, length: length, @@ -516,7 +516,7 @@ extension HTTPBody { length: HTTPBody.Length ) { self.init( - .init(stream.map(ByteChunk.init)), + .init(stream.map { ByteChunk.init($0) }), length: length, iterationBehavior: .single ) @@ -531,7 +531,7 @@ extension HTTPBody { length: HTTPBody.Length ) { self.init( - .init(stream.map(ByteChunk.init)), + .init(stream.map { ByteChunk.init($0) }), length: length, iterationBehavior: .single ) @@ -549,7 +549,7 @@ extension HTTPBody { iterationBehavior: IterationBehavior ) where Strings.Element: StringProtocol & Sendable, Strings: Sendable { self.init( - .init(sequence.map(ByteChunk.init)), + .init(sequence.map { ByteChunk.init($0) }), length: length, iterationBehavior: iterationBehavior ) From 0cf76f2a458511b32e5a9b2eeea12af5133ce43c Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 18 Sep 2023 15:50:46 +0200 Subject: [PATCH 35/55] Update docs --- .../OpenAPIRuntime/Documentation.docc/Documentation.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md b/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md index 10119b46..0cff1d72 100644 --- a/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md +++ b/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md @@ -10,6 +10,8 @@ It contains: - Common types used in the code generated by the `swift-openapi-generator` package plugin. - Protocol definitions for pluggable layers, including ``ClientTransport``, ``ServerTransport``, and middleware. +Many of the HTTP currency types used are defined in the [Swift HTTP Types](https://github.com/apple/swift-http-types) library. + ### Usage Add the package dependency in your `Package.swift`: @@ -73,12 +75,8 @@ Please report any issues related to this library in the [swift-openapi-generator - ``UndocumentedPayload`` ### HTTP Currency Types -- ``Request`` -- ``Response`` -- ``HTTPMethod`` -- ``HeaderField`` +- ``HTTPBody`` - ``ServerRequestMetadata`` -- ``RouterPathComponent`` ### Dynamic Payloads - ``OpenAPIValueContainer`` From af10f7ddea6587837e162ecd0e1f7ca90fb65a65 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 18 Sep 2023 19:09:40 +0200 Subject: [PATCH 36/55] Update HTTPBody.swift Co-authored-by: Si Beaumont --- Sources/OpenAPIRuntime/Interface/HTTPBody.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index 3cb20679..36270808 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -343,7 +343,7 @@ extension HTTPBody { /// Creates a new body with the provided async sequence of byte sequences. /// - Parameters: /// - sequence: An async sequence that provides the byte chunks. - /// - length: The total lenght of the body. + /// - length: The total length of the body. /// - iterationBehavior: The iteration behavior of the sequence, which /// indicates whether it can be iterated multiple times. @inlinable public convenience init( From b2fb79e40006236535eee5e5d059388fb1ed2c34 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 18 Sep 2023 19:09:48 +0200 Subject: [PATCH 37/55] Update HTTPBody.swift Co-authored-by: Si Beaumont --- Sources/OpenAPIRuntime/Interface/HTTPBody.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index 36270808..74b98646 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -540,7 +540,7 @@ extension HTTPBody { /// Creates a new body with the provided async sequence of string chunks. /// - Parameters: /// - sequence: An async sequence that provides the string chunks. - /// - length: The total lenght of the body. + /// - length: The total length of the body. /// - iterationBehavior: The iteration behavior of the sequence, which /// indicates whether it can be iterated multiple times. @inlinable public convenience init( From 1778209ec9e3182f81541096c130980dbcb465b0 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 18 Sep 2023 19:09:58 +0200 Subject: [PATCH 38/55] Update HTTPBody.swift Co-authored-by: Si Beaumont --- Sources/OpenAPIRuntime/Interface/HTTPBody.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index 74b98646..cee51a21 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -325,7 +325,7 @@ extension HTTPBody { /// Creates a new body with the provided async sequence. /// - Parameters: /// - sequence: An async sequence that provides the byte chunks. - /// - length: The total lenght of the body. + /// - length: The total length of the body. /// - iterationBehavior: The iteration behavior of the sequence, which /// indicates whether it can be iterated multiple times. @inlinable public convenience init( From b58a3b62f738ff0e24dc391e710eacc1a3b0968e Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 18 Sep 2023 19:11:16 +0200 Subject: [PATCH 39/55] Update HTTPBody.swift Co-authored-by: Si Beaumont --- Sources/OpenAPIRuntime/Interface/HTTPBody.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index cee51a21..c50c8bdb 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -438,7 +438,7 @@ extension HTTPBody { }() } - var buffer = ByteChunk.init() + var buffer = ByteChunk() for try await chunk in self { guard buffer.count + chunk.count <= maxBytes else { throw TooManyBytesError(maxBytes: maxBytes) From b2d4ec31ca7b161b79711a233cb723c66fecf30b Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 18 Sep 2023 19:11:25 +0200 Subject: [PATCH 40/55] Update HTTPBody.swift Co-authored-by: Si Beaumont --- Sources/OpenAPIRuntime/Interface/HTTPBody.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index c50c8bdb..7f6c6994 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -490,7 +490,7 @@ extension HTTPBody { length: Length ) { self.init( - ByteChunk.init(string), + ByteChunk(string), length: length ) } From 66b602fb06ca1dde40148ffdd8665e396f7535f9 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 18 Sep 2023 19:11:33 +0200 Subject: [PATCH 41/55] Update HTTPBody.swift Co-authored-by: Si Beaumont --- Sources/OpenAPIRuntime/Interface/HTTPBody.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index 7f6c6994..7ed15491 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -502,7 +502,7 @@ extension HTTPBody { _ string: some StringProtocol & Sendable ) { self.init( - ByteChunk.init(string), + ByteChunk(string), length: .known(string.count) ) } From b0ccaae913cdf68b77057c296656372c5513ed9e Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 18 Sep 2023 19:15:07 +0200 Subject: [PATCH 42/55] Update ServerTransport.swift Co-authored-by: Si Beaumont --- Sources/OpenAPIRuntime/Interface/ServerTransport.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Interface/ServerTransport.swift b/Sources/OpenAPIRuntime/Interface/ServerTransport.swift index 44a93c1c..7944c3d9 100644 --- a/Sources/OpenAPIRuntime/Interface/ServerTransport.swift +++ b/Sources/OpenAPIRuntime/Interface/ServerTransport.swift @@ -111,7 +111,7 @@ public protocol ServerTransport { /// - handler: A handler to be invoked when an HTTP request is received. /// - method: An HTTP request method. /// - path: A URL template for the path, for example `/pets/{petId}`. - /// - Important: The `path` can have mixed component, such + /// - Important: The `path` can have mixed components, such /// as `/file/{name}.zip`. func register( _ handler: @Sendable @escaping (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> ( From a129b6b139f4db0a55fae2f3291f30a881b98a2f Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 25 Sep 2023 13:57:35 +0200 Subject: [PATCH 43/55] Fixing up the merge from main --- .../Conversion/Converter+Client.swift | 23 ++----- .../Conversion/Converter+Server.swift | 34 ++-------- .../Conversion/CurrencyExtensions.swift | 66 ++++++++++--------- .../OpenAPIRuntime/Interface/HTTPBody.swift | 2 +- .../Conversion/Test_Converter+Client.swift | 20 +++--- .../Conversion/Test_Converter+Server.swift | 24 +++---- .../Interface/Test_HTTPBody.swift | 2 +- 7 files changed, 70 insertions(+), 101 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index 613e65df..40fd3e86 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -133,16 +133,15 @@ extension Converter { headerFields: &headerFields, contentType: contentType, convert: { $0 } - convert: convertDataToBinary ) } // | client | set | request body | urlEncodedForm | codable | optional | setOptionalRequestBodyAsURLEncodedForm | public func setOptionalRequestBodyAsURLEncodedForm( _ value: T, - headerFields: inout [HeaderField], + headerFields: inout HTTPFields, contentType: String - ) throws -> Data? { + ) throws -> HTTPBody? { try setOptionalRequestBody( value, headerFields: &headerFields, @@ -154,9 +153,9 @@ extension Converter { // | client | set | request body | urlEncodedForm | codable | required | setRequiredRequestBodyAsURLEncodedForm | public func setRequiredRequestBodyAsURLEncodedForm( _ value: T, - headerFields: inout [HeaderField], + headerFields: inout HTTPFields, contentType: String - ) throws -> Data { + ) throws -> HTTPBody { try setRequiredRequestBody( value, headerFields: &headerFields, @@ -165,20 +164,6 @@ extension Converter { ) } - // | client | get | response body | string | required | getResponseBodyAsString | - public func getResponseBodyAsString( - _ type: T.Type, - from data: Data, - transforming transform: (T) -> C - ) throws -> C { - try getResponseBody( - type, - from: data, - transforming: transform, - convert: convertFromStringData - ) - } - // | client | get | response body | JSON | required | getResponseBodyAsJSON | public func getResponseBodyAsJSON( _ type: T.Type, diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index 6f182bb6..e815149d 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -233,17 +233,16 @@ extension Converter { from: data, transforming: transform, convert: { $0 } - convert: convertBinaryToData ) } // | server | get | request body | URLEncodedForm | codable | optional | getOptionalRequestBodyAsURLEncodedForm | func getOptionalRequestBodyAsURLEncodedForm( _ type: T.Type, - from data: Data?, + from data: HTTPBody?, transforming transform: (T) -> C - ) throws -> C? { - try getOptionalRequestBody( + ) async throws -> C? { + try await getOptionalBufferingRequestBody( type, from: data, transforming: transform, @@ -254,10 +253,10 @@ extension Converter { // | server | get | request body | URLEncodedForm | codable | required | getRequiredRequestBodyAsURLEncodedForm | public func getRequiredRequestBodyAsURLEncodedForm( _ type: T.Type, - from data: Data?, + from data: HTTPBody?, transforming transform: (T) -> C - ) throws -> C { - try getRequiredRequestBody( + ) async throws -> C { + try await getRequiredBufferingRequestBody( type, from: data, transforming: transform, @@ -265,27 +264,6 @@ extension Converter { ) } - // | server | set | response body | string | required | setResponseBodyAsString | - public func setResponseBodyAsString( - _ value: T, - headerFields: inout [HeaderField], - contentType: String - ) throws -> Data { - try setResponseBody( - value, - headerFields: &headerFields, - contentType: contentType, - convert: { value in - let encoder = StringEncoder( - dateTranscoder: configuration.dateTranscoder - ) - let encodedString = try encoder.encode(value) - let encodedData = Data(encodedString.utf8) - return encodedData - } - ) - } - // | server | set | response body | JSON | required | setResponseBodyAsJSON | public func setResponseBodyAsJSON( _ value: T, diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index 9d2b19e1..a1b4bc6d 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -171,39 +171,45 @@ extension Converter { _ value: T ) throws -> HTTPBody { let data = try encoder.encode(value) - return HTTPBody(data: data) + return HTTPBody(data) } -func convertURLEncodedFormToCodable( -_ data: Data -) throws -> T { -let decoder = URIDecoder( -configuration: .init( -style: .form, -explode: true, -spaceEscapingCharacter: .plus, -dateTranscoder: configuration.dateTranscoder -) -) -let uriString = String(decoding: data, as: UTF8.self) -return try decoder.decode(T.self, from: uriString) -} + /// Returns a value decoded from a URL-encoded form body. + /// - Parameter body: The body containing the raw URL-encoded form bytes. + /// - Returns: A decoded value. + func convertURLEncodedFormToCodable( + _ body: HTTPBody + ) async throws -> T { + let decoder = URIDecoder( + configuration: .init( + style: .form, + explode: true, + spaceEscapingCharacter: .plus, + dateTranscoder: configuration.dateTranscoder + ) + ) + let data = try await Data(collecting: body, upTo: .max) + let uriString = Substring(decoding: data, as: UTF8.self) + return try decoder.decode(T.self, from: uriString) + } -func convertBodyCodableToURLFormData( -_ value: T -) throws -> Data { -let encoder = URIEncoder( -configuration: .init( -style: .form, -explode: true, -spaceEscapingCharacter: .plus, -dateTranscoder: configuration.dateTranscoder -) -) -let encodedString = try encoder.encode(value, forKey: "") -let data = Data(encodedString.utf8) -return data -} + /// Returns a URL-encoded form string for the provided encodable value. + /// - Parameter value: The value to encode. + /// - Returns: The raw URL-encoded form body. + func convertBodyCodableToURLFormData( + _ value: T + ) throws -> HTTPBody { + let encoder = URIEncoder( + configuration: .init( + style: .form, + explode: true, + spaceEscapingCharacter: .plus, + dateTranscoder: configuration.dateTranscoder + ) + ) + let encodedString = try encoder.encode(value, forKey: "") + return HTTPBody(encodedString) + } /// Returns a JSON string for the provided encodable value. /// - Parameter value: The value to encode. diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index 3cb20679..b57b150f 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -610,7 +610,7 @@ extension HTTPBody { /// Creates a new body from the provided data chunk. /// - Parameter data: A single data chunk. - public convenience init(data: Data) { + public convenience init(_ data: Data) { self.init(ArraySlice(data)) } } diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift index 0a23c112..bd02ae66 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -170,8 +170,8 @@ final class Test_ClientConverterExtensions: Test_Runtime { } // | client | set | request body | urlEncodedForm | codable | optional | setRequiredRequestBodyAsURLEncodedForm | - func test_setOptionalRequestBodyAsURLEncodedForm_codable() throws { - var headerFields: [HeaderField] = [] + func test_setOptionalRequestBodyAsURLEncodedForm_codable() async throws { + var headerFields: HTTPFields = [:] let body = try converter.setOptionalRequestBodyAsURLEncodedForm( testStructDetailed, headerFields: &headerFields, @@ -183,28 +183,28 @@ final class Test_ClientConverterExtensions: Test_Runtime { return } - XCTAssertEqualStringifiedData(body, testStructURLFormString) + try await XCTAssertEqualStringifiedData(body, testStructURLFormString) XCTAssertEqual( headerFields, [ - .init(name: "content-type", value: "application/x-www-form-urlencoded") + .contentType: "application/x-www-form-urlencoded" ] ) } // | client | set | request body | urlEncodedForm | codable | required | setRequiredRequestBodyAsURLEncodedForm | - func test_setRequiredRequestBodyAsURLEncodedForm_codable() throws { - var headerFields: [HeaderField] = [] + func test_setRequiredRequestBodyAsURLEncodedForm_codable() async throws { + var headerFields: HTTPFields = [:] let body = try converter.setRequiredRequestBodyAsURLEncodedForm( testStructDetailed, headerFields: &headerFields, contentType: "application/x-www-form-urlencoded" ) - XCTAssertEqualStringifiedData(body, testStructURLFormString) + try await XCTAssertEqualStringifiedData(body, testStructURLFormString) XCTAssertEqual( headerFields, [ - .init(name: "content-type", value: "application/x-www-form-urlencoded") + .contentType: "application/x-www-form-urlencoded" ] ) } @@ -213,7 +213,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { func test_setOptionalRequestBodyAsBinary_data() async throws { var headerFields: HTTPFields = [:] let body = try converter.setOptionalRequestBodyAsBinary( - .init(data: testStringData), + .init(testStringData), headerFields: &headerFields, contentType: "application/octet-stream" ) @@ -247,7 +247,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { func test_getResponseBodyAsJSON_codable() async throws { let value: TestPet = try await converter.getResponseBodyAsJSON( TestPet.self, - from: .init(data: testStructData), + from: .init(testStructData), transforming: { $0 } ) XCTAssertEqual(value, testStruct) diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift index e7256638..9aad104b 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift @@ -279,7 +279,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { func test_getOptionalRequestBodyAsJSON_codable() async throws { let body: TestPet? = try await converter.getOptionalRequestBodyAsJSON( TestPet.self, - from: .init(data: testStructData), + from: .init(testStructData), transforming: { $0 } ) XCTAssertEqual(body, testStruct) @@ -288,7 +288,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { func test_getOptionalRequestBodyAsJSON_codable_string() async throws { let body: String? = try await converter.getOptionalRequestBodyAsJSON( String.self, - from: .init(data: testQuotedStringData), + from: .init(testQuotedStringData), transforming: { $0 } ) XCTAssertEqual(body, testString) @@ -298,27 +298,27 @@ final class Test_ServerConverterExtensions: Test_Runtime { func test_getRequiredRequestBodyAsJSON_codable() async throws { let body: TestPet = try await converter.getRequiredRequestBodyAsJSON( TestPet.self, - from: .init(data: testStructData), + from: .init(testStructData), transforming: { $0 } ) XCTAssertEqual(body, testStruct) } // | server | get | request body | urlEncodedForm | optional | getOptionalRequestBodyAsURLEncodedForm | - func test_getOptionalRequestBodyAsURLEncodedForm_codable() throws { - let body: TestPetDetailed? = try converter.getOptionalRequestBodyAsURLEncodedForm( + func test_getOptionalRequestBodyAsURLEncodedForm_codable() async throws { + let body: TestPetDetailed? = try await converter.getOptionalRequestBodyAsURLEncodedForm( TestPetDetailed.self, - from: testStructURLFormData, + from: .init(testStructURLFormData), transforming: { $0 } ) XCTAssertEqual(body, testStructDetailed) } // | server | get | request body | urlEncodedForm | required | getRequiredRequestBodyAsURLEncodedForm | - func test_getRequiredRequestBodyAsURLEncodedForm_codable() throws { - let body: TestPetDetailed = try converter.getRequiredRequestBodyAsURLEncodedForm( + func test_getRequiredRequestBodyAsURLEncodedForm_codable() async throws { + let body: TestPetDetailed = try await converter.getRequiredRequestBodyAsURLEncodedForm( TestPetDetailed.self, - from: testStructURLFormData, + from: .init(testStructURLFormData), transforming: { $0 } ) XCTAssertEqual(body, testStructDetailed) @@ -328,7 +328,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { func test_getOptionalRequestBodyAsBinary_data() async throws { let body: HTTPBody? = try converter.getOptionalRequestBodyAsBinary( HTTPBody.self, - from: .init(data: testStringData), + from: .init(testStringData), transforming: { $0 } ) try await XCTAssertEqualStringifiedData(body, testString) @@ -338,7 +338,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { func test_getRequiredRequestBodyAsBinary_data() async throws { let body: HTTPBody = try converter.getRequiredRequestBodyAsBinary( HTTPBody.self, - from: .init(data: testStringData), + from: .init(testStringData), transforming: { $0 } ) try await XCTAssertEqualStringifiedData(body, testString) @@ -365,7 +365,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { func test_setResponseBodyAsBinary_data() async throws { var headers: HTTPFields = [:] let data = try converter.setResponseBodyAsBinary( - .init(data: testStringData), + .init(testStringData), headerFields: &headers, contentType: "application/octet-stream" ) diff --git a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift index 653771ec..bd271b64 100644 --- a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift +++ b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift @@ -66,7 +66,7 @@ final class Test_Body: Test_Runtime { // A single data. do { - let body: HTTPBody = HTTPBody(data: Data([0])) + let body: HTTPBody = HTTPBody(Data([0])) try await _testConsume( body, expected: [0] From a89bcedbb1d1e9c219d7ef570b57a0ce81c4a420 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 25 Sep 2023 17:28:43 +0200 Subject: [PATCH 44/55] Use swift-http-types 1.0 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 1981f283..960e3311 100644 --- a/Package.swift +++ b/Package.swift @@ -37,7 +37,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/apple/swift-http-types", branch: "main"), + .package(url: "https://github.com/apple/swift-http-types", from: "1.0.0"), .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), ], targets: [ From b19a78793e9ba0d2f1c8b8deb280d15b63656673 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 25 Sep 2023 17:33:54 +0200 Subject: [PATCH 45/55] Provide a closure for accessing the locked value HTTPBody.iteratorCreated --- .../OpenAPIRuntime/Interface/HTTPBody.swift | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index 251e3b56..6a1189c0 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -167,6 +167,19 @@ public final class HTTPBody: @unchecked Sendable { /// A flag indicating whether an iterator has already been created. private var locked_iteratorCreated: Bool = false + /// Runs the provided closure in a scope where the `iteratorCreated` value + /// is locked and available both for reading and writing. + /// - Parameter work: A closure that is provided with a read-write reference + /// to the `iteratorCreated` value. + /// - Returns: Whatever value the closure returned. + private func withIteratorCreated(_ work: (inout Bool) throws -> R) rethrows -> R { + lock.lock() + defer { + lock.unlock() + } + return try work(&locked_iteratorCreated) + } + /// Creates a new body. /// - Parameters: /// - sequence: The input sequence providing the byte chunks. @@ -366,16 +379,14 @@ extension HTTPBody: AsyncSequence { public typealias AsyncIterator = Iterator public func makeAsyncIterator() -> AsyncIterator { if iterationBehavior == .single { - lock.lock() - defer { - lock.unlock() - } - guard !locked_iteratorCreated else { - fatalError( - "OpenAPIRuntime.HTTPBody attempted to create a second iterator, but the underlying sequence is only safe to be iterated once." - ) + withIteratorCreated { iteratorCreated in + guard !iteratorCreated else { + fatalError( + "OpenAPIRuntime.HTTPBody attempted to create a second iterator, but the underlying sequence is only safe to be iterated once." + ) + } + iteratorCreated = true } - locked_iteratorCreated = true } return sequence.makeAsyncIterator() } @@ -427,15 +438,11 @@ extension HTTPBody { // iterate a sequence for the second time, if it's only safe to be // iterated once. if iterationBehavior == .single { - try { - lock.lock() - defer { - lock.unlock() - } - guard !locked_iteratorCreated else { + try withIteratorCreated { iteratorCreated in + guard !iteratorCreated else { throw TooManyIterationsError() } - }() + } } var buffer = ByteChunk() From 481e6bbab4b6b7894bd68842aa3d78ca7666a522 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 25 Sep 2023 17:40:24 +0200 Subject: [PATCH 46/55] Fix up encoding comment --- Sources/OpenAPIRuntime/Interface/HTTPBody.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index 6a1189c0..bc53f083 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -574,7 +574,7 @@ extension HTTPBody.ByteChunk { extension String { /// Creates a string by accumulating the full body in-memory into a single buffer up to - /// the provided maximum number of bytes, converting it to string using the provided encoding. + /// the provided maximum number of bytes, converting it to string using UTF-8 encoding. /// - Parameters: /// - body: The HTTP body to collect. /// - maxBytes: The maximum number of bytes this method is allowed From dc209d38b9bb70f2fabb220e70991ae8abe3afd4 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 25 Sep 2023 17:41:45 +0200 Subject: [PATCH 47/55] Update Sources/OpenAPIRuntime/Interface/HTTPBody.swift Co-authored-by: Si Beaumont --- Sources/OpenAPIRuntime/Interface/HTTPBody.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index bc53f083..4da26322 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -623,7 +623,7 @@ extension HTTPBody { } extension Data { - /// Creates a string by accumulating the full body in-memory into a single buffer up to + /// Creates a Data by accumulating the full body in-memory into a single buffer up to /// the provided maximum number of bytes and converting it to `Data`. /// - Parameters: /// - body: The HTTP body to collect. From 05082f979dbcb128eeb6b398bdda9ec974a7ed5f Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 25 Sep 2023 17:46:07 +0200 Subject: [PATCH 48/55] Update Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift Co-authored-by: Si Beaumont --- Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index a1b4bc6d..b6c92d1e 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -49,7 +49,7 @@ extension HTTPField.Name { /// - Parameter name: A field name. /// - Throws: If the name isn't a valid field name. init(validated name: String) throws { - guard let fieldName = Self.init(name) else { + guard let fieldName = Self(name) else { throw RuntimeError.invalidHeaderFieldName(name) } self = fieldName From 03a9f2dbe178943a71f7eb890fd063975df1425c Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 26 Sep 2023 09:30:23 +0200 Subject: [PATCH 49/55] Update Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift Co-authored-by: Si Beaumont --- .../OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift index fd897d8e..cb5bd055 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift @@ -17,7 +17,7 @@ import HTTPTypes extension HTTPField.Name { static var foo: Self { - .init("foo")! + Self("foo")! } } From ed46fa511c355387869231c87624d7e2722c7f5e Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 26 Sep 2023 09:31:46 +0200 Subject: [PATCH 50/55] Explicitly test substring support --- Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift index bd271b64..3f609a82 100644 --- a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift +++ b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift @@ -39,7 +39,8 @@ final class Test_Body: Test_Runtime { // A single substring. do { - let body: HTTPBody = HTTPBody("hello") + let substring: Substring = "hello" + let body: HTTPBody = HTTPBody(substring) try await _testConsume( body, expected: "hello" From 669a7f36aab0aad14519e551c1d96a65a0454512 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 26 Sep 2023 09:39:11 +0200 Subject: [PATCH 51/55] Add a test case with an unknown length --- .../Interface/Test_HTTPBody.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift index 3f609a82..7fae2ee9 100644 --- a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift +++ b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift @@ -102,6 +102,25 @@ final class Test_Body: Test_Runtime { ) } + // An async throwing stream, unknown length. + do { + let body: HTTPBody = HTTPBody( + AsyncThrowingStream( + String.self, + { continuation in + continuation.yield("hel") + continuation.yield("lo") + continuation.finish() + } + ), + length: .unknown + ) + try await _testConsume( + body, + expected: "hello" + ) + } + // An async stream. do { let body: HTTPBody = HTTPBody( From 72d241d0f22a9ac11482213b3febc0f8d24837b9 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 26 Sep 2023 11:52:21 +0200 Subject: [PATCH 52/55] PR feedback: improve the iteration behavior checking in HTTPBody, rename ServerError.metadata -> ServerError.requestMetadata. --- .../OpenAPIRuntime/Errors/ServerError.swift | 10 +-- .../OpenAPIRuntime/Interface/HTTPBody.swift | 73 ++++++++++++------- .../Interface/UniversalServer.swift | 2 +- .../Interface/Test_HTTPBody.swift | 58 +++++++++++++++ 4 files changed, 111 insertions(+), 32 deletions(-) diff --git a/Sources/OpenAPIRuntime/Errors/ServerError.swift b/Sources/OpenAPIRuntime/Errors/ServerError.swift index df7ed816..b93ac72b 100644 --- a/Sources/OpenAPIRuntime/Errors/ServerError.swift +++ b/Sources/OpenAPIRuntime/Errors/ServerError.swift @@ -28,7 +28,7 @@ public struct ServerError: Error { public var requestBody: HTTPBody? /// The request metadata extracted by the server. - public var metadata: ServerRequestMetadata + public var requestMetadata: ServerRequestMetadata /// An operation-specific Input value. /// @@ -48,7 +48,7 @@ public struct ServerError: Error { /// - operationID: The OpenAPI operation identifier. /// - request: The HTTP request provided to the server. /// - requestBody: The HTTP request body provided to the server. - /// - metadata: The request metadata extracted by the server. + /// - requestMetadata: The request metadata extracted by the server. /// - operationInput: An operation-specific Input value. /// - operationOutput: An operation-specific Output value. /// - underlyingError: The underlying error that caused the operation @@ -57,7 +57,7 @@ public struct ServerError: Error { operationID: String, request: HTTPRequest, requestBody: HTTPBody?, - metadata: ServerRequestMetadata, + requestMetadata: ServerRequestMetadata, operationInput: (any Sendable)? = nil, operationOutput: (any Sendable)? = nil, underlyingError: (any Error) @@ -65,7 +65,7 @@ public struct ServerError: Error { self.operationID = operationID self.request = request self.requestBody = requestBody - self.metadata = metadata + self.requestMetadata = requestMetadata self.operationInput = operationInput self.operationOutput = operationOutput self.underlyingError = underlyingError @@ -83,7 +83,7 @@ public struct ServerError: Error { extension ServerError: CustomStringConvertible { public var description: String { - "Server error - operationID: \(operationID), request: \(request.prettyDescription), requestBody: \(requestBody?.prettyDescription ?? ""), metadata: \(metadata.description), operationInput: \(operationInput.map { String(describing: $0) } ?? ""), operationOutput: \(operationOutput.map { String(describing: $0) } ?? ""), underlying error: \(underlyingErrorDescription)" + "Server error - operationID: \(operationID), request: \(request.prettyDescription), requestBody: \(requestBody?.prettyDescription ?? ""), metadata: \(requestMetadata.description), operationInput: \(operationInput.map { String(describing: $0) } ?? ""), operationOutput: \(operationOutput.map { String(describing: $0) } ?? ""), underlying error: \(underlyingErrorDescription)" } } diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index 4da26322..50b8b92d 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -167,17 +167,47 @@ public final class HTTPBody: @unchecked Sendable { /// A flag indicating whether an iterator has already been created. private var locked_iteratorCreated: Bool = false - /// Runs the provided closure in a scope where the `iteratorCreated` value - /// is locked and available both for reading and writing. - /// - Parameter work: A closure that is provided with a read-write reference - /// to the `iteratorCreated` value. - /// - Returns: Whatever value the closure returned. - private func withIteratorCreated(_ work: (inout Bool) throws -> R) rethrows -> R { + /// A flag indicating whether an iterator has already been created, only + /// used for testing. + internal var testing_iteratorCreated: Bool { lock.lock() defer { lock.unlock() } - return try work(&locked_iteratorCreated) + return locked_iteratorCreated + } + + /// Verifying that creating another iterator is allowed based on + /// the values of `iterationBehavior` and `locked_iteratorCreated`. + /// - Throws: If another iterator is not allowed to be created. + private func checkIfCanCreateIterator() throws { + lock.lock() + defer { + lock.unlock() + } + guard iterationBehavior == .single else { + return + } + if locked_iteratorCreated { + throw TooManyIterationsError() + } + } + + /// Tries to mark an iterator as created, verifying that it is allowed + /// based on the values of `iterationBehavior` and `locked_iteratorCreated`. + /// - Throws: If another iterator is not allowed to be created. + private func tryToMarkIteratorCreated() throws { + lock.lock() + defer { + locked_iteratorCreated = true + lock.unlock() + } + guard iterationBehavior == .single else { + return + } + if locked_iteratorCreated { + throw TooManyIterationsError() + } } /// Creates a new body. @@ -378,16 +408,8 @@ extension HTTPBody: AsyncSequence { public typealias Element = ByteChunk public typealias AsyncIterator = Iterator public func makeAsyncIterator() -> AsyncIterator { - if iterationBehavior == .single { - withIteratorCreated { iteratorCreated in - guard !iteratorCreated else { - fatalError( - "OpenAPIRuntime.HTTPBody attempted to create a second iterator, but the underlying sequence is only safe to be iterated once." - ) - } - iteratorCreated = true - } - } + // The crash on error is intentional here. + try! tryToMarkIteratorCreated() return sequence.makeAsyncIterator() } } @@ -433,18 +455,17 @@ extension HTTPBody { /// - Returns: A byte chunk containing all the accumulated bytes. fileprivate func collect(upTo maxBytes: Int) async throws -> ByteChunk { - // As a courtesy, check if another iteration is allowed, and throw - // an error instead of fatalError here if the user is trying to - // iterate a sequence for the second time, if it's only safe to be - // iterated once. - if iterationBehavior == .single { - try withIteratorCreated { iteratorCreated in - guard !iteratorCreated else { - throw TooManyIterationsError() - } + // Check that we're allowed to iterate again. + try checkIfCanCreateIterator() + + // If the length is known, verify it's within the limit. + if case .known(let knownBytes) = length { + guard knownBytes <= maxBytes else { + throw TooManyBytesError(maxBytes: maxBytes) } } + // Accumulate the byte chunks. var buffer = ByteChunk() for try await chunk in self { guard buffer.count + chunk.count <= maxBytes else { diff --git a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift index c7a380ea..53cd613a 100644 --- a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift +++ b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift @@ -122,7 +122,7 @@ import struct Foundation.URLComponents operationID: operationID, request: request, requestBody: requestBody, - metadata: metadata, + requestMetadata: metadata, operationInput: input, operationOutput: output, underlyingError: error diff --git a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift index 7fae2ee9..50ffc307 100644 --- a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift +++ b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift @@ -184,6 +184,64 @@ final class Test_Body: Test_Runtime { } XCTAssertEqual(chunks, ["hel", "lo"].map { Array($0.utf8)[...] }) } + + func testIterationBehavior_single() async throws { + let sequence = AsyncStream( + String.self, + { continuation in + continuation.yield("hel") + continuation.yield("lo") + continuation.finish() + } + ) + .map { $0 } + let body: HTTPBody = HTTPBody( + sequence, + length: .unknown, + iterationBehavior: .single + ) + + XCTAssertFalse(body.testing_iteratorCreated) + + var chunkCount = 0 + for try await _ in body { + chunkCount += 1 + } + XCTAssertEqual(chunkCount, 2) + + XCTAssertTrue(body.testing_iteratorCreated) + + do { + _ = try await String(collecting: body, upTo: .max) + XCTFail("Expected an error to be thrown") + } catch {} + } + + func testIterationBehavior_multiple() async throws { + let body: HTTPBody = HTTPBody([104, 105]) + + XCTAssertFalse(body.testing_iteratorCreated) + + do { + var chunkCount = 0 + for try await _ in body { + chunkCount += 1 + } + XCTAssertEqual(chunkCount, 1) + } + + XCTAssertTrue(body.testing_iteratorCreated) + + do { + var chunkCount = 0 + for try await _ in body { + chunkCount += 1 + } + XCTAssertEqual(chunkCount, 1) + } + + XCTAssertTrue(body.testing_iteratorCreated) + } } extension Test_Body { From 07046738cf0929dd96339a0706e8f944507f2b62 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 26 Sep 2023 12:22:31 +0200 Subject: [PATCH 53/55] Added a test for the collect method based on a known length --- .../Interface/Test_HTTPBody.swift | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift index 50ffc307..83fbe182 100644 --- a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift +++ b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift @@ -242,6 +242,31 @@ final class Test_Body: Test_Runtime { XCTAssertTrue(body.testing_iteratorCreated) } + + func testIterationBehavior_multiple_byteLimit() async throws { + let body: HTTPBody = HTTPBody([104, 105]) + + do { + _ = try await String(collecting: body, upTo: 0) + XCTFail("Expected an error to be thrown") + } catch {} + + do { + _ = try await String(collecting: body, upTo: 1) + XCTFail("Expected an error to be thrown") + } catch {} + + do { + let string = try await String(collecting: body, upTo: 2) + XCTAssertEqual(string, "hi") + } + + do { + let string = try await String(collecting: body, upTo: .max) + XCTAssertEqual(string, "hi") + } + } + } extension Test_Body { From 492cf0c18d93ab777c0597976f9dff3af23268e1 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 26 Sep 2023 17:29:45 +0200 Subject: [PATCH 54/55] Move extensions of http types into the generated SPI --- Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift index 070f94d3..328c5e56 100644 --- a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift +++ b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift @@ -39,11 +39,13 @@ extension HTTPRequest { /// - path: The URL path of the resource. /// - method: The HTTP method. /// - headerFields: The HTTP header fields. + @_spi(Generated) public init(soar_path path: String, method: Method, headerFields: HTTPFields = .init()) { self.init(method: method, scheme: nil, authority: nil, path: path, headerFields: headerFields) } /// The query substring of the request's path. + @_spi(Generated) public var soar_query: Substring? { guard let path else { return nil @@ -57,6 +59,7 @@ extension HTTPRequest { } /// The request path, without any query or fragment portions. + @_spi(Generated) public var soar_pathOnly: Substring { guard let path else { return ""[...] @@ -72,8 +75,9 @@ extension HTTPResponse { /// - Parameters: /// - statusCode: The status code of the response.AsString /// - headerFields: The HTTP header fields. - public init(soar_statusCode statusCode: Int, headerFields: HTTPFields = .init()) { - self.init(status: .init(code: statusCode), headerFields: headerFields) + @_spi(Generated) + public init(soar_statusCode statusCode: Int) { + self.init(status: .init(code: statusCode)) } } From 24f887022e1b92c38e3e2351f8d58711553ed32a Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 27 Sep 2023 08:59:27 +0200 Subject: [PATCH 55/55] Update version in docs to 0.3.0 --- README.md | 2 +- Sources/OpenAPIRuntime/Documentation.docc/Documentation.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7565c433..d54d870b 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Add the package dependency in your `Package.swift`: ```swift .package( url: "https://github.com/apple/swift-openapi-runtime", - .upToNextMinor(from: "0.2.0") + .upToNextMinor(from: "0.3.0") ), ``` diff --git a/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md b/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md index 0cff1d72..f124e7d8 100644 --- a/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md +++ b/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md @@ -19,7 +19,7 @@ Add the package dependency in your `Package.swift`: ```swift .package( url: "https://github.com/apple/swift-openapi-runtime", - .upToNextMinor(from: "0.2.0") + .upToNextMinor(from: "0.3.0") ), ```