diff --git a/Sources/OpenAPIRuntime/Base/CopyOnWriteBox.swift b/Sources/OpenAPIRuntime/Base/CopyOnWriteBox.swift index 58de90e0..f876666e 100644 --- a/Sources/OpenAPIRuntime/Base/CopyOnWriteBox.swift +++ b/Sources/OpenAPIRuntime/Base/CopyOnWriteBox.swift @@ -26,7 +26,7 @@ /// Creates a new storage with the provided initial value. /// - Parameter value: The initial value to store in the box. - @inlinable init(value: Wrapped) { self.value = value } + @usableFromInline init(value: Wrapped) { self.value = value } } /// The internal storage of the box. @@ -34,7 +34,7 @@ /// Creates a new box. /// - Parameter value: The value to store in the box. - @inlinable public init(value: Wrapped) { self.storage = .init(value: value) } + public init(value: Wrapped) { self.storage = .init(value: value) } /// The stored value whose accessors enforce copy-on-write semantics. @inlinable public var value: Wrapped { diff --git a/Sources/OpenAPIRuntime/Conversion/Configuration.swift b/Sources/OpenAPIRuntime/Conversion/Configuration.swift index 93b00f32..6cff9130 100644 --- a/Sources/OpenAPIRuntime/Conversion/Configuration.swift +++ b/Sources/OpenAPIRuntime/Conversion/Configuration.swift @@ -74,9 +74,20 @@ public struct Configuration: Sendable { /// The transcoder used when converting between date and string values. public var dateTranscoder: any DateTranscoder + /// The generator to use when creating mutlipart bodies. + public var multipartBoundaryGenerator: any MultipartBoundaryGenerator + /// Creates a new configuration with the specified values. /// - /// - Parameter dateTranscoder: The transcoder to use when converting between date + /// - Parameters: + /// - dateTranscoder: The transcoder to use when converting between date /// and string values. - public init(dateTranscoder: any DateTranscoder = .iso8601) { self.dateTranscoder = dateTranscoder } + /// - multipartBoundaryGenerator: The generator to use when creating mutlipart bodies. + public init( + dateTranscoder: any DateTranscoder = .iso8601, + multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random + ) { + self.dateTranscoder = dateTranscoder + self.multipartBoundaryGenerator = multipartBoundaryGenerator + } } diff --git a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift index 323da60f..5dfee0b0 100644 --- a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift +++ b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift @@ -195,3 +195,24 @@ extension DecodingError { } } } + +extension Configuration { + /// Creates a new configuration with the specified values. + /// + /// - Parameter dateTranscoder: The transcoder to use when converting between date + /// and string values. + @available(*, deprecated, renamed: "init(dateTranscoder:multipartBoundaryGenerator:)") @_disfavoredOverload + public init(dateTranscoder: any DateTranscoder) { + self.init(dateTranscoder: dateTranscoder, multipartBoundaryGenerator: .random) + } +} + +extension HTTPBody { + /// Describes how many times the provided sequence can be iterated. + @available( + *, + deprecated, + renamed: "IterationBehavior", + message: "Use the top level IterationBehavior directly instead of HTTPBody.IterationBehavior." + ) public typealias IterationBehavior = OpenAPIRuntime.IterationBehavior +} diff --git a/Sources/OpenAPIRuntime/Interface/AsyncSequenceCommon.swift b/Sources/OpenAPIRuntime/Interface/AsyncSequenceCommon.swift new file mode 100644 index 00000000..392eead8 --- /dev/null +++ b/Sources/OpenAPIRuntime/Interface/AsyncSequenceCommon.swift @@ -0,0 +1,120 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// Describes 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 +} + +// MARK: - Internal + +/// A type-erasing closure-based iterator. +@usableFromInline struct AnyIterator: AsyncIteratorProtocol { + + /// The closure that produces the next element. + private let produceNext: () async throws -> Element? + + /// 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 + self.produceNext = { try await iterator.next() } + } + + /// Advances the iterator to the next element and returns it asynchronously. + /// + /// - Returns: The next element in the sequence, or `nil` if there are no more elements. + /// - Throws: An error if there is an issue advancing the iterator or retrieving the next element. + public mutating func next() async throws -> Element? { try await produceNext() } +} + +/// A type-erased async sequence that wraps input sequences. +@usableFromInline struct AnySequence: AsyncSequence, Sendable { + + /// The type of the type-erased iterator. + @usableFromInline typealias AsyncIterator = AnyIterator + + /// A closure that produces a new iterator. + @usableFromInline let produceIterator: @Sendable () -> AsyncIterator + + /// Creates a new sequence. + /// - Parameter sequence: The input sequence to type-erase. + @usableFromInline init(_ sequence: Upstream) + where Upstream.Element == Element, Upstream: Sendable { + self.produceIterator = { .init(sequence.makeAsyncIterator()) } + } + + @usableFromInline func makeAsyncIterator() -> AsyncIterator { produceIterator() } +} + +/// An async sequence wrapper for a sync sequence. +@usableFromInline struct WrappedSyncSequence: AsyncSequence, Sendable +where Upstream.Element: Sendable { + + /// The type of the iterator. + @usableFromInline typealias AsyncIterator = Iterator + + /// The element type. + @usableFromInline typealias Element = Upstream.Element + + /// An iterator type that wraps a sync sequence iterator. + @usableFromInline struct Iterator: AsyncIteratorProtocol { + + /// The element type. + @usableFromInline typealias Element = IteratorElement + + /// The underlying sync sequence iterator. + var iterator: any IteratorProtocol + + @usableFromInline mutating func next() async throws -> IteratorElement? { iterator.next() } + } + + /// The underlying sync sequence. + @usableFromInline let sequence: Upstream + + /// Creates a new async sequence with the provided sync sequence. + /// - Parameter sequence: The sync sequence to wrap. + @usableFromInline init(sequence: Upstream) { self.sequence = sequence } + + @usableFromInline func makeAsyncIterator() -> AsyncIterator { Iterator(iterator: sequence.makeIterator()) } +} + +/// An empty async sequence. +@usableFromInline struct EmptySequence: AsyncSequence, Sendable { + + /// The type of the empty iterator. + @usableFromInline typealias AsyncIterator = EmptyIterator + + /// An async iterator of an empty sequence. + @usableFromInline struct EmptyIterator: AsyncIteratorProtocol { + + @usableFromInline mutating func next() async throws -> IteratorElement? { nil } + } + + /// Creates a new empty async sequence. + @usableFromInline init() {} + + @usableFromInline func makeAsyncIterator() -> AsyncIterator { EmptyIterator() } +} diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index b97906ba..eb163459 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -121,25 +121,9 @@ public final class HTTPBody: @unchecked Sendable { /// The underlying byte chunk type. public typealias ByteChunk = ArraySlice - /// Describes 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 - } - - /// The body's iteration behavior, which controls how many times + /// The iteration behavior, which controls how many times /// the input sequence can be iterated. - public let iterationBehavior: IterationBehavior + public let iterationBehavior: OpenAPIRuntime.IterationBehavior /// Describes the total length of the body, if known. public enum Length: Sendable, Equatable { @@ -155,7 +139,7 @@ public final class HTTPBody: @unchecked Sendable { public let length: Length /// The underlying type-erased async sequence. - private let sequence: BodySequence + private let sequence: AnySequence /// A lock for shared mutable state. private let lock: NSLock = { @@ -205,7 +189,11 @@ public final class HTTPBody: @unchecked Sendable { /// 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) { + @usableFromInline init( + _ sequence: AnySequence, + length: Length, + iterationBehavior: OpenAPIRuntime.IterationBehavior + ) { self.sequence = sequence self.length = length self.iterationBehavior = iterationBehavior @@ -220,7 +208,7 @@ public final class HTTPBody: @unchecked Sendable { @usableFromInline convenience init( _ byteChunks: some Sequence & Sendable, length: Length, - iterationBehavior: IterationBehavior + iterationBehavior: OpenAPIRuntime.IterationBehavior ) { self.init( .init(WrappedSyncSequence(sequence: byteChunks)), @@ -281,7 +269,7 @@ extension HTTPBody { @inlinable public convenience init( _ bytes: some Sequence & Sendable, length: Length, - iterationBehavior: IterationBehavior + iterationBehavior: OpenAPIRuntime.IterationBehavior ) { self.init([ArraySlice(bytes)], length: length, iterationBehavior: iterationBehavior) } /// Creates a new body with the provided byte collection. @@ -323,7 +311,7 @@ extension HTTPBody { @inlinable public convenience init( _ sequence: Bytes, length: HTTPBody.Length, - iterationBehavior: IterationBehavior + iterationBehavior: OpenAPIRuntime.IterationBehavior ) where Bytes.Element == ByteChunk, Bytes: Sendable { self.init(.init(sequence), length: length, iterationBehavior: iterationBehavior) } @@ -337,7 +325,7 @@ extension HTTPBody { @inlinable public convenience init( _ sequence: Bytes, length: HTTPBody.Length, - iterationBehavior: IterationBehavior + iterationBehavior: OpenAPIRuntime.IterationBehavior ) where Bytes: Sendable, Bytes.Element: Sequence & Sendable, Bytes.Element.Element == UInt8 { self.init(sequence.map { ArraySlice($0) }, length: length, iterationBehavior: iterationBehavior) } @@ -356,7 +344,7 @@ extension HTTPBody: AsyncSequence { public func makeAsyncIterator() -> AsyncIterator { // The crash on error is intentional here. try! tryToMarkIteratorCreated() - return sequence.makeAsyncIterator() + return .init(sequence.makeAsyncIterator()) } } @@ -482,7 +470,7 @@ extension HTTPBody { @inlinable public convenience init( _ sequence: Strings, length: HTTPBody.Length, - iterationBehavior: IterationBehavior + iterationBehavior: OpenAPIRuntime.IterationBehavior ) where Strings.Element: StringProtocol & Sendable, Strings: Sendable { self.init(.init(sequence.map { ByteChunk.init($0) }), length: length, iterationBehavior: iterationBehavior) } @@ -583,83 +571,3 @@ extension HTTPBody { public mutating func next() async throws -> Element? { try await produceNext() } } } - -extension HTTPBody { - - /// A type-erased async sequence that wraps input sequences. - @usableFromInline struct BodySequence: AsyncSequence, Sendable { - - /// The type of the type-erased iterator. - @usableFromInline typealias AsyncIterator = HTTPBody.Iterator - - /// The byte chunk element type. - @usableFromInline typealias Element = ByteChunk - - /// A closure that produces a new iterator. - @usableFromInline let produceIterator: @Sendable () -> AsyncIterator - - /// Creates a new sequence. - /// - Parameter sequence: The input sequence to type-erase. - @inlinable init(_ sequence: Bytes) where Bytes.Element == Element, Bytes: Sendable { - self.produceIterator = { .init(sequence.makeAsyncIterator()) } - } - - @usableFromInline func makeAsyncIterator() -> AsyncIterator { produceIterator() } - } - - /// An async sequence wrapper for a sync sequence. - @usableFromInline struct WrappedSyncSequence: AsyncSequence, Sendable - where Bytes.Element == ByteChunk, Bytes.Iterator.Element == ByteChunk, Bytes: Sendable { - - /// The type of the iterator. - @usableFromInline typealias AsyncIterator = Iterator - - /// The byte chunk element type. - @usableFromInline typealias Element = ByteChunk - - /// An iterator type that wraps a sync sequence iterator. - @usableFromInline struct Iterator: AsyncIteratorProtocol { - - /// 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.ByteChunk? { iterator.next() } - } - - /// The underlying sync sequence. - @usableFromInline let sequence: Bytes - - /// Creates a new async sequence with the provided sync sequence. - /// - Parameter sequence: The sync sequence to wrap. - @inlinable init(sequence: Bytes) { self.sequence = sequence } - - @usableFromInline func makeAsyncIterator() -> Iterator { Iterator(iterator: sequence.makeIterator()) } - } - - /// An empty async sequence. - @usableFromInline struct EmptySequence: AsyncSequence, Sendable { - - /// The type of the empty iterator. - @usableFromInline typealias AsyncIterator = EmptyIterator - - /// The byte chunk element type. - @usableFromInline typealias Element = ByteChunk - - /// An async iterator of an empty sequence. - @usableFromInline struct EmptyIterator: AsyncIteratorProtocol { - - /// The byte chunk element type. - @usableFromInline typealias Element = ByteChunk - - @usableFromInline mutating func next() async throws -> HTTPBody.ByteChunk? { nil } - } - - /// Creates a new empty async sequence. - @inlinable init() {} - - @usableFromInline func makeAsyncIterator() -> EmptyIterator { EmptyIterator() } - } -} diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartBoundaryGenerator.swift b/Sources/OpenAPIRuntime/Multipart/MultipartBoundaryGenerator.swift new file mode 100644 index 00000000..39bc9d21 --- /dev/null +++ b/Sources/OpenAPIRuntime/Multipart/MultipartBoundaryGenerator.swift @@ -0,0 +1,75 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// A generator of a new boundary string used by multipart messages to separate parts. +public protocol MultipartBoundaryGenerator: Sendable { + + /// Generates a boundary string for a multipart message. + /// - Returns: A boundary string. + func makeBoundary() -> String +} + +extension MultipartBoundaryGenerator where Self == ConstantMultipartBoundaryGenerator { + + /// A generator that always returns the same boundary string. + public static var constant: Self { ConstantMultipartBoundaryGenerator() } +} + +extension MultipartBoundaryGenerator where Self == RandomMultipartBoundaryGenerator { + + /// A generator that produces a random boundary every time. + public static var random: Self { RandomMultipartBoundaryGenerator() } +} + +/// A generator that always returns the same constant boundary string. +public struct ConstantMultipartBoundaryGenerator: MultipartBoundaryGenerator { + + /// The boundary string to return. + public let boundary: String + /// Creates a new generator. + /// - Parameter boundary: The boundary string to return every time. + public init(boundary: String = "__X_SWIFT_OPENAPI_GENERATOR_BOUNDARY__") { self.boundary = boundary } + + /// Generates a boundary string for a multipart message. + /// - Returns: A boundary string. + public func makeBoundary() -> String { boundary } +} + +/// A generator that returns a boundary containg a constant prefix and a random suffix. +public struct RandomMultipartBoundaryGenerator: MultipartBoundaryGenerator { + + /// The constant prefix of each boundary. + public let boundaryPrefix: String + /// The length, in bytes, of the random boundary suffix. + public let randomNumberSuffixLength: Int + + /// The options for the random bytes suffix. + private let values: [UInt8] = Array("0123456789".utf8) + + /// Create a new generator. + /// - Parameters: + /// - boundaryPrefix: The constant prefix of each boundary. + /// - randomNumberSuffixLength: The length, in bytes, of the random boundary suffix. + public init(boundaryPrefix: String = "__X_SWIFT_OPENAPI_", randomNumberSuffixLength: Int = 20) { + self.boundaryPrefix = boundaryPrefix + self.randomNumberSuffixLength = randomNumberSuffixLength + } + /// Generates a boundary string for a multipart message. + /// - Returns: A boundary string. + public func makeBoundary() -> String { + var randomSuffix = [UInt8](repeating: 0, count: randomNumberSuffixLength) + for i in randomSuffix.startIndex..: Sendable, Hashable { + + /// The underlying typed part payload, which has a statically known part name. + public var payload: Payload + + /// A file name parameter provided in the `content-disposition` part header field. + public var filename: String? + + /// Creates a new wrapper. + /// - Parameters: + /// - payload: The underlying typed part payload, which has a statically known part name. + /// - filename: A file name parameter provided in the `content-disposition` part header field. + public init(payload: Payload, filename: String? = nil) { + self.payload = payload + self.filename = filename + } +} + +/// A wrapper of a typed part without a statically known name that adds +/// dynamic `content-disposition` parameter values, such as `name` and `filename`. +public struct MultipartDynamicallyNamedPart: Sendable, Hashable { + + /// The underlying typed part payload, which has a statically known part name. + public var payload: Payload + + /// A file name parameter provided in the `content-disposition` part header field. + public var filename: String? + + /// A name parameter provided in the `content-disposition` part header field. + public var name: String? + + /// Creates a new wrapper. + /// - Parameters: + /// - payload: The underlying typed part payload, which has a statically known part name. + /// - filename: A file name parameter provided in the `content-disposition` part header field. + /// - name: A name parameter provided in the `content-disposition` part header field. + public init(payload: Payload, filename: String? = nil, name: String? = nil) { + self.payload = payload + self.filename = filename + self.name = name + } +} + +/// The body of multipart requests and responses. +/// +/// `MultipartBody` represents an async sequence of multipart parts of a specific type. +/// +/// The `Part` generic type parameter is usually a generated enum representing +/// the different values documented for this multipart body. +/// +/// ## Creating a body from buffered parts +/// +/// Create a body from an array of values of type `Part`: +/// +/// ```swift +/// let body: MultipartBody = [ +/// .myCaseA(...), +/// .myCaseB(...), +/// ] +/// ``` +/// +/// ## Creating a body from an async sequence of parts +/// +/// The body type also supports initialization from an async sequence. +/// +/// ```swift +/// let producingSequence = ... // an AsyncSequence of MyPartType +/// let body = MultipartBody( +/// producingSequence, +/// iterationBehavior: .single // or .multiple +/// ) +/// ``` +/// +/// In addition to the async sequence, also specify 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 (stream, continuation) = AsyncStream.makeStream(of: MyPartType.self) +/// // Pass the continuation to another task that produces the parts asynchronously. +/// Task { +/// continuation.yield(.myCaseA(...)) +/// // ... later +/// continuation.yield(.myCaseB(...)) +/// continuation.finish() +/// } +/// let body = MultipartBody(stream) +/// ``` +/// +/// ## Consuming a body as an async sequence +/// +/// The `MultipartBody` type conforms to `AsyncSequence` and uses a generic element type, +/// so it can be consumed in a streaming fashion, without ever buffering the whole body +/// in your process. +/// +/// ```swift +/// let multipartBody: MultipartBody = ... +/// for try await part in multipartBody { +/// switch part { +/// case .myCaseA(let myCaseAValue): +/// // Handle myCaseAValue. +/// case .myCaseB(let myCaseBValue): +/// // Handle myCaseBValue, which is a raw type with a streaming part body. +/// // +/// // Option 1: Process the part body bytes in chunks. +/// for try await bodyChunk in myCaseBValue.body { +/// // Handle bodyChunk. +/// } +/// // Option 2: Accumulate the body into a byte array. +/// // (For other convenience initializers, check out ``HTTPBody``. +/// let fullPartBody = try await [UInt8](collecting: myCaseBValue.body, upTo: 1024) +/// // ... +/// } +/// } +/// ``` +/// +/// Multipart parts of different names can arrive in any order, and the order is not significant. +/// +/// Consuming the multipart body should be resilient to parts of different names being reordered. +/// +/// However, multiple parts of the same name, if allowed by the OpenAPI document by defining it as an array, +/// should be treated as an ordered array of values, and those cannot be reordered without changing +/// the message's meaning. +/// +/// > Important: Parts that contain a raw streaming body (of type ``HTTPBody``) must +/// have their bodies fully consumed before the multipart body sequence is asked for +/// the next part. The multipart body sequence does not buffer internally, and since +/// the parts and their bodies arrive in a single stream of bytes, you cannot move on +/// to the next part until the current one is consumed. +public final class MultipartBody: @unchecked Sendable { + + /// The iteration behavior, which controls how many times the input sequence can be iterated. + public let iterationBehavior: IterationBehavior + + /// The underlying type-erased async sequence. + private let sequence: AnySequence + + /// A lock for shared mutable state. + private let lock: NSLock = { + let lock = NSLock() + lock.name = "com.apple.swift-openapi-generator.runtime.multipart-body" + return lock + }() + + /// A flag indicating whether an iterator has already been created. + private var locked_iteratorCreated: Bool = false + + /// 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 locked_iteratorCreated + } + + /// An error thrown by the collecting initializer when another iteration of + /// the body is not allowed. + private struct TooManyIterationsError: Error, CustomStringConvertible, LocalizedError { + + /// A textual representation of this instance. + var description: String { + "OpenAPIRuntime.MultipartBody attempted to create a second iterator, but the underlying sequence is only safe to be iterated once." + } + + /// A localized message describing what error occurred. + var errorDescription: String? { description } + } + + /// 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. + internal 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 sequence. + /// - Parameters: + /// - sequence: The input sequence providing the parts. + /// - iterationBehavior: The sequence's iteration behavior, which indicates whether the sequence + /// can be iterated multiple times. + @usableFromInline init(_ sequence: AnySequence, iterationBehavior: IterationBehavior) { + self.sequence = sequence + self.iterationBehavior = iterationBehavior + } +} + +extension MultipartBody: Equatable { + + /// Compares two OpenAPISequence instances for equality by comparing their object identifiers. + /// + /// - Parameters: + /// - lhs: The left-hand side OpenAPISequence. + /// - rhs: The right-hand side OpenAPISequence. + /// + /// - Returns: `true` if the object identifiers of the two OpenAPISequence instances are equal, + /// indicating that they are the same object in memory; otherwise, returns `false`. + public static func == (lhs: MultipartBody, rhs: MultipartBody) -> Bool { + ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } +} + +extension MultipartBody: Hashable { + + /// Hashes the OpenAPISequence instance by combining its object identifier into the provided hasher. + /// + /// - Parameter hasher: The hasher used to combine the hash value. + public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } +} + +// MARK: - Creating the MultipartBody. + +extension MultipartBody { + + /// Creates a new sequence with the provided async sequence of parts. + /// - Parameters: + /// - sequence: An async sequence that provides the parts. + /// - iterationBehavior: The iteration behavior of the sequence, which indicates whether it + /// can be iterated multiple times. + @inlinable public convenience init( + _ sequence: Input, + iterationBehavior: IterationBehavior + ) where Input.Element == Element { self.init(.init(sequence), iterationBehavior: iterationBehavior) } + + /// Creates a new sequence with the provided sequence parts. + /// - Parameters: + /// - elements: A sequence of parts. + /// - iterationBehavior: The iteration behavior of the sequence, which indicates whether it + /// can be iterated multiple times. + @usableFromInline convenience init( + _ elements: some Sequence & Sendable, + iterationBehavior: IterationBehavior + ) { self.init(.init(WrappedSyncSequence(sequence: elements)), iterationBehavior: iterationBehavior) } + + /// Creates a new sequence with the provided collection of parts. + /// - Parameter elements: A collection of parts. + @inlinable public convenience init(_ elements: some Collection & Sendable) { + self.init(elements, iterationBehavior: .multiple) + } + + /// Creates a new sequence with the provided async throwing stream. + /// - Parameter stream: An async throwing stream that provides the parts. + @inlinable public convenience init(_ stream: AsyncThrowingStream) { + self.init(.init(stream), iterationBehavior: .single) + } + + /// Creates a new sequence with the provided async stream. + /// - Parameter stream: An async stream that provides the parts. + @inlinable public convenience init(_ stream: AsyncStream) { + self.init(.init(stream), iterationBehavior: .single) + } +} + +// MARK: - Conversion from literals +extension MultipartBody: ExpressibleByArrayLiteral { + + /// The type of the elements of an array literal. + public typealias ArrayLiteralElement = Element + + /// Creates an instance initialized with the given elements. + public convenience init(arrayLiteral elements: Element...) { self.init(elements) } +} + +// MARK: - Consuming the sequence +extension MultipartBody: AsyncSequence { + + /// The type of the element. + public typealias Element = Part + + /// Represents an asynchronous iterator over a sequence of elements. + public typealias AsyncIterator = Iterator + + /// Creates and returns an asynchronous iterator + /// + /// - Returns: An asynchronous iterator for parts. + public func makeAsyncIterator() -> AsyncIterator { + // The crash on error is intentional here. + try! tryToMarkIteratorCreated() + return .init(sequence.makeAsyncIterator()) + } +} + +// MARK: - Underlying async sequences +extension MultipartBody { + + /// An async iterator of both input async sequences and of the sequence itself. + public struct Iterator: AsyncIteratorProtocol { + + /// The closure that produces the next element. + private let produceNext: () async throws -> Element? + + /// 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 + self.produceNext = { try await iterator.next() } + } + + /// Advances the iterator to the next element and returns it asynchronously. + /// + /// - Returns: The next element in the sequence, or `nil` if there are no more elements. + /// - Throws: An error if there is an issue advancing the iterator or retrieving the next element. + public mutating func next() async throws -> Element? { try await produceNext() } + } +} diff --git a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBoundaryGenerator.swift b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBoundaryGenerator.swift new file mode 100644 index 00000000..edb8e033 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBoundaryGenerator.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// 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_MultipartBoundaryGenerator: Test_Runtime { + + func testConstant() throws { + let generator = ConstantMultipartBoundaryGenerator(boundary: "__abcd__") + let firstBoundary = generator.makeBoundary() + let secondBoundary = generator.makeBoundary() + XCTAssertEqual(firstBoundary, "__abcd__") + XCTAssertEqual(secondBoundary, "__abcd__") + } + + func testRandom() throws { + let generator = RandomMultipartBoundaryGenerator(boundaryPrefix: "__abcd__", randomNumberSuffixLength: 8) + let firstBoundary = generator.makeBoundary() + let secondBoundary = generator.makeBoundary() + XCTAssertNotEqual(firstBoundary, secondBoundary) + XCTAssertTrue(firstBoundary.hasPrefix("__abcd__")) + XCTAssertTrue(secondBoundary.hasPrefix("__abcd__")) + } +}