From fd47bd2597056bf77f012365f4f0101a8e342148 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 1 Sep 2023 17:00:27 +0200 Subject: [PATCH 1/2] [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 2/2] 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