From f6085a3b33dc5267becb13b7217f23f1c891091d Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 31 Oct 2023 15:55:52 +0100 Subject: [PATCH 01/20] Apply the same .swift-format as the generator repo (#68) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the same .swift-format as the generator repo ### Motivation Bring the runtime library formatting in line with the generator repo. Also fixes https://github.com/apple/swift-openapi-generator/issues/250. ### Modifications Copied the `.swift-format` file, reran swift-format. Only formatting changes. ### Result Consistent style. ### Test Plan CI. Reviewed by: gjcairo Builds: ✔︎ pull request validation (5.10) - Build finished. ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (api breakage) - Build finished. ✔︎ pull request validation (docc test) - Build finished. ✔︎ pull request validation (integration test) - Build finished. ✔︎ pull request validation (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. https://github.com/apple/swift-openapi-runtime/pull/68 --- .swift-format | 9 +- Sources/OpenAPIRuntime/Base/Acceptable.swift | 38 +-- .../Base/Base64EncodedData.swift | 4 +- .../OpenAPIRuntime/Base/CopyOnWriteBox.swift | 62 +--- .../OpenAPIRuntime/Base/OpenAPIMIMEType.swift | 127 ++------ .../OpenAPIRuntime/Base/OpenAPIValue.swift | 165 +++------- .../Base/WarningSuppressingAnnotations.swift | 8 +- .../Conversion/CodableExtensions.swift | 115 ++----- .../Conversion/Configuration.swift | 23 +- .../Conversion/Converter+Client.swift | 68 +--- .../Conversion/Converter+Common.swift | 118 ++----- .../Conversion/Converter+Server.swift | 109 ++----- .../OpenAPIRuntime/Conversion/Converter.swift | 7 +- .../Conversion/CurrencyExtensions.swift | 160 ++-------- .../Conversion/ErrorExtensions.swift | 39 +-- .../Conversion/FoundationExtensions.swift | 4 +- .../Conversion/ParameterStyles.swift | 13 +- .../Conversion/ServerVariable.swift | 13 +- .../Conversion/URLExtensions.swift | 8 +- .../Deprecated/Deprecated.swift | 47 +-- .../OpenAPIRuntime/Errors/ClientError.swift | 4 +- .../OpenAPIRuntime/Errors/CodingErrors.swift | 21 +- .../OpenAPIRuntime/Errors/RuntimeError.swift | 74 ++--- .../OpenAPIRuntime/Errors/ServerError.swift | 4 +- .../Interface/ClientTransport.swift | 9 +- .../Interface/CurrencyTypes.swift | 53 +--- .../OpenAPIRuntime/Interface/HTTPBody.swift | 240 +++----------- .../Interface/UniversalClient.swift | 28 +- .../Interface/UniversalServer.swift | 58 +--- .../URICoder/Common/URIEncodedNode.swift | 33 +- .../URICoder/Decoding/URIDecoder.swift | 41 +-- .../URIValueFromNodeDecoder+Keyed.swift | 179 +++-------- .../URIValueFromNodeDecoder+Single.swift | 145 +++------ .../URIValueFromNodeDecoder+Unkeyed.swift | 179 +++-------- .../Decoding/URIValueFromNodeDecoder.swift | 116 ++----- .../URICoder/Encoding/URIEncoder.swift | 22 +- .../URIValueToNodeEncoder+Keyed.swift | 138 +++----- .../URIValueToNodeEncoder+Single.swift | 112 ++----- .../URIValueToNodeEncoder+Unkeyed.swift | 141 +++------ .../Encoding/URIValueToNodeEncoder.swift | 32 +- .../URICoder/Parsing/URIParser.swift | 78 ++--- .../Serialization/URISerializer.swift | 122 ++----- .../Base/Test_Acceptable.swift | 48 +-- .../Base/Test_CopyOnWriteBox.swift | 53 +--- .../Base/Test_OpenAPIMIMEType.swift | 82 ++--- .../Base/Test_OpenAPIValue.swift | 44 +-- .../Conversion/Test_CodableExtensions.swift | 59 +--- .../Conversion/Test_Converter+Client.swift | 98 +----- .../Conversion/Test_Converter+Common.swift | 298 +++--------------- .../Conversion/Test_Converter+Server.swift | 95 ++---- .../Conversion/Test_ServerVariable.swift | 21 +- .../Interface/Test_HTTPBody.swift | 107 ++----- .../Interface/Test_UniversalClient.swift | 97 ++---- .../Interface/Test_UniversalServer.swift | 57 +--- Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 124 ++------ .../URICoder/Decoder/Test_URIDecoder.swift | 52 +-- .../Test_URIValueFromNodeDecoder.swift | 87 +---- .../URICoder/Encoding/Test_URIEncoder.swift | 9 +- .../Encoding/Test_URIValueToNodeEncoder.swift | 174 ++-------- .../URICoder/Parsing/Test_URIParser.swift | 95 ++---- .../Serialization/Test_URISerializer.swift | 61 +--- .../URICoder/Test_URICodingRoundtrip.swift | 137 ++------ 62 files changed, 1107 insertions(+), 3657 deletions(-) diff --git a/.swift-format b/.swift-format index 7efc7847..3213ba65 100644 --- a/.swift-format +++ b/.swift-format @@ -14,10 +14,11 @@ "lineLength" : 120, "maximumBlankLines" : 1, "prioritizeKeepingFunctionOutputTogether" : false, - "respectsExistingLineBreaks" : true, + "respectsExistingLineBreaks" : false, "rules" : { "AllPublicDeclarationsHaveDocumentation" : true, "AlwaysUseLowerCamelCase" : false, + "AlwaysUseLiteralForEmptyCollectionInit" : true, "AmbiguousTrailingClosureOverload" : true, "BeginDocumentationCommentWithOneLineSummary" : false, "DoNotUseSemicolons" : true, @@ -38,16 +39,18 @@ "NoLeadingUnderscores" : false, "NoParensAroundConditions" : true, "NoVoidReturnOnFunctionSignature" : true, + "OmitExplicitReturns" : true, "OneCasePerLine" : true, "OneVariableDeclarationPerLine" : true, "OnlyOneTrailingClosureArgument" : true, "OrderedImports" : false, + "ReplaceForEachWithForLoop" : true, "ReturnVoidInsteadOfEmptyTuple" : true, - "UseEarlyExits" : true, + "UseEarlyExits" : false, "UseLetInEveryBoundCaseVariable" : false, "UseShorthandTypeNames" : true, "UseSingleLinePropertyGetter" : false, - "UseSynthesizedInitializer" : false, + "UseSynthesizedInitializer" : true, "UseTripleSlashForDocumentationComments" : true, "UseWhereClausesInForLoops" : false, "ValidateDocumentationComments" : true diff --git a/Sources/OpenAPIRuntime/Base/Acceptable.swift b/Sources/OpenAPIRuntime/Base/Acceptable.swift index 217d9f7f..fb19799f 100644 --- a/Sources/OpenAPIRuntime/Base/Acceptable.swift +++ b/Sources/OpenAPIRuntime/Base/Acceptable.swift @@ -29,9 +29,7 @@ public struct QualityValue: Sendable, Hashable { /// Returns a Boolean value indicating whether the quality value is /// at its default value 1.0. - public var isDefault: Bool { - thousands == 1000 - } + public var isDefault: Bool { thousands == 1000 } /// Creates a new quality value from the provided floating-point number. /// @@ -46,9 +44,7 @@ public struct QualityValue: Sendable, Hashable { } /// The value represented as a floating-point number between 0.0 and 1.0, inclusive. - public var doubleValue: Double { - Double(thousands) / 1000 - } + public var doubleValue: Double { Double(thousands) / 1000 } } extension QualityValue: RawRepresentable { @@ -56,16 +52,12 @@ extension QualityValue: RawRepresentable { /// /// - Parameter rawValue: A string representing the quality value. public init?(rawValue: String) { - guard let doubleValue = Double(rawValue) else { - return nil - } + guard let doubleValue = Double(rawValue) else { return nil } self.init(doubleValue: doubleValue) } /// The raw string representation of the `QualityValue`. - public var rawValue: String { - String(format: "%0.3f", doubleValue) - } + public var rawValue: String { String(format: "%0.3f", doubleValue) } } extension QualityValue: ExpressibleByIntegerLiteral { @@ -86,18 +78,14 @@ extension QualityValue: ExpressibleByFloatLiteral { /// Creates a new `QualityValue` instance from a floating-point literal value. /// /// - Parameter value: A floating-point literal value representing the quality value. - public init(floatLiteral value: Double) { - self.init(doubleValue: value) - } + public init(floatLiteral value: Double) { self.init(doubleValue: value) } } extension Array { /// Returns the default values for the acceptable type. public static func defaultValues() -> [AcceptHeaderContentType] - where Element == AcceptHeaderContentType { - T.allCases.map { .init(contentType: $0) } - } + where Element == AcceptHeaderContentType { T.allCases.map { .init(contentType: $0) } } } /// A wrapper of an individual content type in the accept header. @@ -129,9 +117,7 @@ public struct AcceptHeaderContentType: Sendable /// Returns the default set of acceptable content types for this type, in /// the order specified in the OpenAPI document. - public static var defaultValues: [Self] { - ContentType.allCases.map { .init(contentType: $0) } - } + public static var defaultValues: [Self] { ContentType.allCases.map { .init(contentType: $0) } } } extension AcceptHeaderContentType: RawRepresentable { @@ -161,18 +147,12 @@ extension AcceptHeaderContentType: RawRepresentable { } /// The raw representation of the content negotiation as a MIME type string. - public var rawValue: String { - contentType.rawValue + (quality.isDefault ? "" : "; q=\(quality.rawValue)") - } + public var rawValue: String { contentType.rawValue + (quality.isDefault ? "" : "; q=\(quality.rawValue)") } } extension Array { /// Returns the array sorted by the quality value, highest quality first. public func sortedByQuality() -> [AcceptHeaderContentType] - where Element == AcceptHeaderContentType { - sorted { a, b in - a.quality.doubleValue > b.quality.doubleValue - } - } + where Element == AcceptHeaderContentType { sorted { a, b in a.quality.doubleValue > b.quality.doubleValue } } } diff --git a/Sources/OpenAPIRuntime/Base/Base64EncodedData.swift b/Sources/OpenAPIRuntime/Base/Base64EncodedData.swift index 3dab18e6..f5116c38 100644 --- a/Sources/OpenAPIRuntime/Base/Base64EncodedData.swift +++ b/Sources/OpenAPIRuntime/Base/Base64EncodedData.swift @@ -56,9 +56,7 @@ public struct Base64EncodedData: Sendable, Hashable { /// Initializes an instance of ``Base64EncodedData`` wrapping the provided slice of bytes. /// - Parameter data: The underlying bytes to wrap. - public init(data: ArraySlice) { - self.data = data - } + public init(data: ArraySlice) { self.data = data } } extension Base64EncodedData: Codable { diff --git a/Sources/OpenAPIRuntime/Base/CopyOnWriteBox.swift b/Sources/OpenAPIRuntime/Base/CopyOnWriteBox.swift index cbfc8f01..58de90e0 100644 --- a/Sources/OpenAPIRuntime/Base/CopyOnWriteBox.swift +++ b/Sources/OpenAPIRuntime/Base/CopyOnWriteBox.swift @@ -16,46 +16,31 @@ /// /// It also enables recursive types by introducing a "box" into the cycle, which /// allows the owning type to have a finite size. -@_spi(Generated) -public struct CopyOnWriteBox { +@_spi(Generated) public struct CopyOnWriteBox { /// The reference type storage for the box. - @usableFromInline - internal final class Storage { + @usableFromInline internal final class Storage { /// The stored value. - @usableFromInline - var value: Wrapped + @usableFromInline var value: Wrapped /// 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 - } + @inlinable init(value: Wrapped) { self.value = value } } /// The internal storage of the box. - @usableFromInline - internal var storage: Storage + @usableFromInline internal var storage: Storage /// Creates a new box. /// - Parameter value: The value to store in the box. - @inlinable - public init(value: Wrapped) { - self.storage = .init(value: value) - } + @inlinable public init(value: Wrapped) { self.storage = .init(value: value) } /// The stored value whose accessors enforce copy-on-write semantics. - @inlinable - public var value: Wrapped { - get { - storage.value - } + @inlinable public var value: Wrapped { + get { storage.value } _modify { - if !isKnownUniquelyReferenced(&storage) { - storage = Storage(value: storage.value) - } + if !isKnownUniquelyReferenced(&storage) { storage = Storage(value: storage.value) } yield &storage.value } } @@ -73,10 +58,7 @@ extension CopyOnWriteBox: Encodable where Wrapped: Encodable { /// /// - Parameter encoder: The encoder to write data to. /// - Throws: On an encoding error. - @inlinable - public func encode(to encoder: any Encoder) throws { - try value.encode(to: encoder) - } + @inlinable public func encode(to encoder: any Encoder) throws { try value.encode(to: encoder) } } extension CopyOnWriteBox: Decodable where Wrapped: Decodable { @@ -88,8 +70,7 @@ extension CopyOnWriteBox: Decodable where Wrapped: Decodable { /// /// - Parameter decoder: The decoder to read data from. /// - Throws: On a decoding error. - @inlinable - public init(from decoder: any Decoder) throws { + @inlinable public init(from decoder: any Decoder) throws { let value = try Wrapped(from: decoder) self.init(value: value) } @@ -106,11 +87,7 @@ extension CopyOnWriteBox: Equatable where Wrapped: Equatable { /// - lhs: A value to compare. /// - rhs: Another value to compare. /// - Returns: A Boolean value indicating whether the values are equal. - @inlinable - public static func == ( - lhs: CopyOnWriteBox, - rhs: CopyOnWriteBox - ) -> Bool { + @inlinable public static func == (lhs: CopyOnWriteBox, rhs: CopyOnWriteBox) -> Bool { lhs.value == rhs.value } } @@ -132,10 +109,7 @@ extension CopyOnWriteBox: Hashable where Wrapped: Hashable { /// /// - Parameter hasher: The hasher to use when combining the components /// of this instance. - @inlinable - public func hash(into hasher: inout Hasher) { - hasher.combine(value) - } + @inlinable public func hash(into hasher: inout Hasher) { hasher.combine(value) } } extension CopyOnWriteBox: CustomStringConvertible where Wrapped: CustomStringConvertible { @@ -163,10 +137,7 @@ extension CopyOnWriteBox: CustomStringConvertible where Wrapped: CustomStringCon /// /// The conversion of `p` to a string in the assignment to `s` uses the /// `Point` type's `description` property. - @inlinable - public var description: String { - value.description - } + @inlinable public var description: String { value.description } } extension CopyOnWriteBox: CustomDebugStringConvertible where Wrapped: CustomDebugStringConvertible { @@ -194,10 +165,7 @@ extension CopyOnWriteBox: CustomDebugStringConvertible where Wrapped: CustomDebu /// /// The conversion of `p` to a string in the assignment to `s` uses the /// `Point` type's `debugDescription` property. - @inlinable - public var debugDescription: String { - value.debugDescription - } + @inlinable public var debugDescription: String { value.debugDescription } } extension CopyOnWriteBox: @unchecked Sendable where Wrapped: Sendable {} diff --git a/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift b/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift index 95390a7f..6dc2a730 100644 --- a/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift +++ b/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift @@ -14,8 +14,7 @@ import Foundation /// A container for a parsed, valid MIME type. -@_spi(Generated) -public struct OpenAPIMIMEType: Equatable { +@_spi(Generated) public struct OpenAPIMIMEType: Equatable { /// The kind of the MIME type. public enum Kind: Equatable { @@ -38,15 +37,12 @@ public struct OpenAPIMIMEType: Equatable { /// - Returns: `true` if the MIME type kinds are equal, otherwise `false`. public static func == (lhs: Kind, rhs: Kind) -> Bool { switch (lhs, rhs) { - case (.any, .any): - return true - case let (.anySubtype(lhsType), .anySubtype(rhsType)): - return lhsType.lowercased() == rhsType.lowercased() + case (.any, .any): return true + case let (.anySubtype(lhsType), .anySubtype(rhsType)): return lhsType.lowercased() == rhsType.lowercased() case let (.concrete(lhsType, lhsSubtype), .concrete(rhsType, rhsSubtype)): return lhsType.lowercased() == rhsType.lowercased() && lhsSubtype.lowercased() == rhsSubtype.lowercased() - default: - return false + default: return false } } } @@ -74,26 +70,14 @@ public struct OpenAPIMIMEType: Equatable { /// /// - Returns: `true` if the MIME types are equal, otherwise `false`. public static func == (lhs: OpenAPIMIMEType, rhs: OpenAPIMIMEType) -> Bool { - guard lhs.kind == rhs.kind else { - return false - } + guard lhs.kind == rhs.kind else { return false } // Parameter names are case-insensitive, parameter values are // case-sensitive. - guard lhs.parameters.count == rhs.parameters.count else { - return false - } - if lhs.parameters.isEmpty { - return true - } - func normalizeKeyValue(key: String, value: String) -> (String, String) { - (key.lowercased(), value) - } - let normalizedLeftParams = Dictionary( - uniqueKeysWithValues: lhs.parameters.map(normalizeKeyValue) - ) - let normalizedRightParams = Dictionary( - uniqueKeysWithValues: rhs.parameters.map(normalizeKeyValue) - ) + guard lhs.parameters.count == rhs.parameters.count else { return false } + if lhs.parameters.isEmpty { return true } + func normalizeKeyValue(key: String, value: String) -> (String, String) { (key.lowercased(), value) } + let normalizedLeftParams = Dictionary(uniqueKeysWithValues: lhs.parameters.map(normalizeKeyValue)) + let normalizedRightParams = Dictionary(uniqueKeysWithValues: rhs.parameters.map(normalizeKeyValue)) return normalizedLeftParams == normalizedRightParams } } @@ -103,35 +87,23 @@ extension OpenAPIMIMEType.Kind: LosslessStringConvertible { /// /// - Parameter description: A string description of the MIME type kind. public init?(_ description: String) { - let typeAndSubtype = - description - .split(separator: "/") - .map(String.init) - guard typeAndSubtype.count == 2 else { - return nil - } + let typeAndSubtype = description.split(separator: "/").map(String.init) + guard typeAndSubtype.count == 2 else { return nil } switch (typeAndSubtype[0], typeAndSubtype[1]) { case ("*", let subtype): - guard subtype == "*" else { - return nil - } + guard subtype == "*" else { return nil } self = .any - case (let type, "*"): - self = .anySubtype(type: type) - case (let type, let subtype): - self = .concrete(type: type, subtype: subtype) + case (let type, "*"): self = .anySubtype(type: type) + case (let type, let subtype): self = .concrete(type: type, subtype: subtype) } } /// A textual representation of the MIME type kind. public var description: String { switch self { - case .any: - return "*/*" - case .anySubtype(let type): - return "\(type)/*" - case .concrete(let type, let subtype): - return "\(type)/\(subtype)" + case .any: return "*/*" + case .anySubtype(let type): return "\(type)/*" + case .concrete(let type, let subtype): return "\(type)/\(subtype)" } } } @@ -142,32 +114,18 @@ extension OpenAPIMIMEType: LosslessStringConvertible { /// - Parameter description: A string description of the MIME. public init?(_ description: String) { var components = - description - // Split by semicolon - .split(separator: ";") - .map(String.init) - // Trim leading/trailing spaces + description // Split by semicolon + .split(separator: ";").map(String.init) // Trim leading/trailing spaces .map { $0.trimmingLeadingAndTrailingSpaces } - guard !components.isEmpty else { - return nil - } + guard !components.isEmpty else { return nil } let firstComponent = components.removeFirst() - guard let kind = OpenAPIMIMEType.Kind(firstComponent) else { - return nil - } + guard let kind = OpenAPIMIMEType.Kind(firstComponent) else { return nil } func parseParameter(_ string: String) -> (String, String)? { - let components = - string - .split(separator: "=") - .map(String.init) - guard components.count == 2 else { - return nil - } + let components = string.split(separator: "=").map(String.init) + guard components.count == 2 else { return nil } return (components[0], components[1]) } - let parameters = - components - .compactMap(parseParameter) + let parameters = components.compactMap(parseParameter) self.init( kind: kind, parameters: Dictionary( @@ -180,10 +138,7 @@ extension OpenAPIMIMEType: LosslessStringConvertible { /// A string description of the MIME type. public var description: String { - ([kind.description] - + parameters - .sorted(by: { a, b in a.key < b.key }) - .map { "\($0)=\($1)" }) + ([kind.description] + parameters.sorted(by: { a, b in a.key < b.key }).map { "\($0)=\($1)" }) .joined(separator: "; ") } } @@ -225,14 +180,10 @@ extension OpenAPIMIMEType { /// the closer the types are. var score: Int { switch self { - case .incompatible: - return 0 - case .wildcard: - return 1 - case .subtypeWildcard: - return 2 - case .typeAndSubtype(let matchedParameterCount): - return 3 + matchedParameterCount + case .incompatible: return 0 + case .wildcard: return 1 + case .subtypeWildcard: return 2 + case .typeAndSubtype(let matchedParameterCount): return 3 + matchedParameterCount } } } @@ -251,20 +202,15 @@ extension OpenAPIMIMEType { against option: OpenAPIMIMEType ) -> Match { switch option.kind { - case .any: - return .wildcard + case .any: return .wildcard case .anySubtype(let expectedType): - guard receivedType.lowercased() == expectedType.lowercased() else { - return .incompatible(.type) - } + guard receivedType.lowercased() == expectedType.lowercased() else { return .incompatible(.type) } return .subtypeWildcard case .concrete(let expectedType, let expectedSubtype): guard receivedType.lowercased() == expectedType.lowercased() && receivedSubtype.lowercased() == expectedSubtype.lowercased() - else { - return .incompatible(.subtype) - } + else { return .incompatible(.subtype) } // A full concrete match, so also check parameters. // The rule is: @@ -287,12 +233,9 @@ extension OpenAPIMIMEType { var matchedParameterCount = 0 for optionParameter in option.parameters { let normalizedParameterName = optionParameter.key.lowercased() - guard - let receivedValue = receivedNormalizedParameters[normalizedParameterName], + guard let receivedValue = receivedNormalizedParameters[normalizedParameterName], receivedValue == optionParameter.value - else { - return .incompatible(.parameter(name: normalizedParameterName)) - } + else { return .incompatible(.parameter(name: normalizedParameterName)) } matchedParameterCount += 1 } return .typeAndSubtype(matchedParameterCount: matchedParameterCount) diff --git a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift index d7b395b8..0d39a6c4 100644 --- a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift +++ b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift @@ -41,9 +41,7 @@ public struct OpenAPIValueContainer: Codable, Hashable, Sendable { /// Creates a new container with the given validated value. /// - Parameter value: A value of a JSON-compatible type, such as `String`, /// `[Any]`, and `[String: Any]`. - init(validatedValue value: (any Sendable)?) { - self.value = value - } + init(validatedValue value: (any Sendable)?) { self.value = value } /// Creates a new container with the given unvalidated value. /// @@ -63,24 +61,13 @@ public struct OpenAPIValueContainer: Codable, Hashable, Sendable { /// - Returns: A cast value if supported. /// - Throws: When the value is not supported. static func tryCast(_ value: (any Sendable)?) throws -> (any Sendable)? { - guard let value = value else { - return nil - } - if let array = value as? [(any Sendable)?] { - return try array.map(tryCast(_:)) - } - if let dictionary = value as? [String: (any Sendable)?] { - return try dictionary.mapValues(tryCast(_:)) - } - if let value = tryCastPrimitiveType(value) { - return value - } + guard let value = value else { return nil } + if let array = value as? [(any Sendable)?] { return try array.map(tryCast(_:)) } + if let dictionary = value as? [String: (any Sendable)?] { return try dictionary.mapValues(tryCast(_:)) } + if let value = tryCastPrimitiveType(value) { return value } throw EncodingError.invalidValue( value, - .init( - codingPath: [], - debugDescription: "Type '\(type(of: value))' is not a supported OpenAPI value." - ) + .init(codingPath: [], debugDescription: "Type '\(type(of: value))' is not a supported OpenAPI value.") ) } @@ -89,10 +76,8 @@ public struct OpenAPIValueContainer: Codable, Hashable, Sendable { /// - Returns: A cast value if supported, nil otherwise. static func tryCastPrimitiveType(_ value: any Sendable) -> (any Sendable)? { switch value { - case is String, is Int, is Bool, is Double: - return value - default: - return nil + case is String, is Int, is Bool, is Double: return value + default: return nil } } @@ -139,14 +124,10 @@ public struct OpenAPIValueContainer: Codable, Hashable, Sendable { return } switch value { - case let value as Bool: - try container.encode(value) - case let value as Int: - try container.encode(value) - case let value as Double: - try container.encode(value) - case let value as String: - try container.encode(value) + case let value as Bool: try container.encode(value) + case let value as Int: try container.encode(value) + case let value as Double: try container.encode(value) + case let value as String: try container.encode(value) case let value as [(any Sendable)?]: try container.encode(value.map(OpenAPIValueContainer.init(validatedValue:))) case let value as [String: (any Sendable)?]: @@ -169,47 +150,30 @@ public struct OpenAPIValueContainer: Codable, Hashable, Sendable { /// - Returns: `true` if the two instances are equal, `false` otherwise. public static func == (lhs: OpenAPIValueContainer, rhs: OpenAPIValueContainer) -> Bool { switch (lhs.value, rhs.value) { - case (nil, nil), is (Void, Void): - return true - case let (lhs as Bool, rhs as Bool): - return lhs == rhs - case let (lhs as Int, rhs as Int): - return lhs == rhs - case let (lhs as Int64, rhs as Int64): - return lhs == rhs - case let (lhs as Int32, rhs as Int32): - return lhs == rhs - case let (lhs as Float, rhs as Float): - return lhs == rhs - case let (lhs as Double, rhs as Double): - return lhs == rhs - case let (lhs as String, rhs as String): - return lhs == rhs + case (nil, nil), is (Void, Void): return true + case let (lhs as Bool, rhs as Bool): return lhs == rhs + case let (lhs as Int, rhs as Int): return lhs == rhs + case let (lhs as Int64, rhs as Int64): return lhs == rhs + case let (lhs as Int32, rhs as Int32): return lhs == rhs + case let (lhs as Float, rhs as Float): return lhs == rhs + case let (lhs as Double, rhs as Double): return lhs == rhs + case let (lhs as String, rhs as String): return lhs == rhs case let (lhs as [(any Sendable)?], rhs as [(any Sendable)?]): - guard lhs.count == rhs.count else { - return false - } + guard lhs.count == rhs.count else { return false } return zip(lhs, rhs) .allSatisfy { lhs, rhs in OpenAPIValueContainer(validatedValue: lhs) == OpenAPIValueContainer(validatedValue: rhs) } case let (lhs as [String: (any Sendable)?], rhs as [String: (any Sendable)?]): - guard lhs.count == rhs.count else { - return false - } - guard Set(lhs.keys) == Set(rhs.keys) else { - return false - } + guard lhs.count == rhs.count else { return false } + guard Set(lhs.keys) == Set(rhs.keys) else { return false } for key in lhs.keys { guard OpenAPIValueContainer(validatedValue: lhs[key]!) == OpenAPIValueContainer(validatedValue: rhs[key]!) - else { - return false - } + else { return false } } return true - default: - return false + default: return false } } @@ -220,25 +184,18 @@ public struct OpenAPIValueContainer: Codable, Hashable, Sendable { /// - Parameter hasher: The hasher used to compute the hash value. public func hash(into hasher: inout Hasher) { switch value { - case let value as Bool: - hasher.combine(value) - case let value as Int: - hasher.combine(value) - case let value as Double: - hasher.combine(value) - case let value as String: - hasher.combine(value) + case let value as Bool: hasher.combine(value) + case let value as Int: hasher.combine(value) + case let value as Double: hasher.combine(value) + case let value as String: hasher.combine(value) case let value as [(any Sendable)?]: - for item in value { - hasher.combine(OpenAPIValueContainer(validatedValue: item)) - } + for item in value { hasher.combine(OpenAPIValueContainer(validatedValue: item)) } case let value as [String: (any Sendable)?]: for (key, itemValue) in value { hasher.combine(key) hasher.combine(OpenAPIValueContainer(validatedValue: itemValue)) } - default: - break + default: break } } } @@ -247,45 +204,35 @@ extension OpenAPIValueContainer: ExpressibleByBooleanLiteral { /// Creates an `OpenAPIValueContainer` with the provided boolean value. /// /// - Parameter value: The boolean value to store in the container. - public init(booleanLiteral value: BooleanLiteralType) { - self.init(validatedValue: value) - } + public init(booleanLiteral value: BooleanLiteralType) { self.init(validatedValue: value) } } extension OpenAPIValueContainer: ExpressibleByStringLiteral { /// Creates an `OpenAPIValueContainer` with the provided string value. /// /// - Parameter value: The string value to store in the container. - public init(stringLiteral value: String) { - self.init(validatedValue: value) - } + public init(stringLiteral value: String) { self.init(validatedValue: value) } } extension OpenAPIValueContainer: ExpressibleByNilLiteral { /// Creates an `OpenAPIValueContainer` with a `nil` value. /// /// - Parameter nilLiteral: The `nil` literal. - public init(nilLiteral: ()) { - self.init(validatedValue: nil) - } + public init(nilLiteral: ()) { self.init(validatedValue: nil) } } extension OpenAPIValueContainer: ExpressibleByIntegerLiteral { /// Creates an `OpenAPIValueContainer` with the provided integer value. /// /// - Parameter value: The integer value to store in the container. - public init(integerLiteral value: Int) { - self.init(validatedValue: value) - } + public init(integerLiteral value: Int) { self.init(validatedValue: value) } } extension OpenAPIValueContainer: ExpressibleByFloatLiteral { /// Creates an `OpenAPIValueContainer` with the provided floating-point value. /// /// - Parameter value: The floating-point value to store in the container. - public init(floatLiteral value: Double) { - self.init(validatedValue: value) - } + public init(floatLiteral value: Double) { self.init(validatedValue: value) } } /// A container for a dictionary with values represented by JSON Schema. @@ -317,14 +264,10 @@ public struct OpenAPIObjectContainer: Codable, Hashable, Sendable { /// Creates a new container with the given validated dictionary. /// - Parameter value: A dictionary value. - init(validatedValue value: [String: (any Sendable)?]) { - self.value = value - } + init(validatedValue value: [String: (any Sendable)?]) { self.value = value } /// Creates a new empty container. - public init() { - self.init(validatedValue: [:]) - } + public init() { self.init(validatedValue: [:]) } /// Creates a new container with the given unvalidated value. /// @@ -344,7 +287,7 @@ public struct OpenAPIObjectContainer: Codable, Hashable, Sendable { /// - Returns: A cast dictionary if values are supported. /// - Throws: If an unsupported value is found. static func tryCast(_ value: [String: (any Sendable)?]) throws -> [String: (any Sendable)?] { - return try value.mapValues(OpenAPIValueContainer.tryCast(_:)) + try value.mapValues(OpenAPIValueContainer.tryCast(_:)) } // MARK: Decodable @@ -382,17 +325,11 @@ public struct OpenAPIObjectContainer: Codable, Hashable, Sendable { public static func == (lhs: OpenAPIObjectContainer, rhs: OpenAPIObjectContainer) -> Bool { let lv = lhs.value let rv = rhs.value - guard lv.count == rv.count else { - return false - } - guard Set(lv.keys) == Set(rv.keys) else { - return false - } + guard lv.count == rv.count else { return false } + guard Set(lv.keys) == Set(rv.keys) else { return false } for key in lv.keys { guard OpenAPIValueContainer(validatedValue: lv[key]!) == OpenAPIValueContainer(validatedValue: rv[key]!) - else { - return false - } + else { return false } } return true } @@ -439,14 +376,10 @@ public struct OpenAPIArrayContainer: Codable, Hashable, Sendable { /// Creates a new container with the given validated array. /// - Parameter value: An array value. - init(validatedValue value: [(any Sendable)?]) { - self.value = value - } + init(validatedValue value: [(any Sendable)?]) { self.value = value } /// Creates a new empty container. - public init() { - self.init(validatedValue: []) - } + public init() { self.init(validatedValue: []) } /// Creates a new container with the given unvalidated value. /// @@ -466,7 +399,7 @@ public struct OpenAPIArrayContainer: Codable, Hashable, Sendable { /// - Returns: A cast value if values are supported, nil otherwise. /// - Throws: An error if casting to supported values fails for any element. static func tryCast(_ value: [(any Sendable)?]) throws -> [(any Sendable)?] { - return try value.map(OpenAPIValueContainer.tryCast(_:)) + try value.map(OpenAPIValueContainer.tryCast(_:)) } // MARK: Decodable @@ -503,9 +436,7 @@ public struct OpenAPIArrayContainer: Codable, Hashable, Sendable { public static func == (lhs: OpenAPIArrayContainer, rhs: OpenAPIArrayContainer) -> Bool { let lv = lhs.value let rv = rhs.value - guard lv.count == rv.count else { - return false - } + guard lv.count == rv.count else { return false } return zip(lv, rv) .allSatisfy { lhs, rhs in OpenAPIValueContainer(validatedValue: lhs) == OpenAPIValueContainer(validatedValue: rhs) @@ -518,8 +449,6 @@ public struct OpenAPIArrayContainer: Codable, Hashable, Sendable { /// /// - Parameter hasher: The hasher used to compute the hash value. public func hash(into hasher: inout Hasher) { - for item in value { - hasher.combine(OpenAPIValueContainer(validatedValue: item)) - } + for item in value { hasher.combine(OpenAPIValueContainer(validatedValue: item)) } } } diff --git a/Sources/OpenAPIRuntime/Base/WarningSuppressingAnnotations.swift b/Sources/OpenAPIRuntime/Base/WarningSuppressingAnnotations.swift index d40d07d2..f3eeb6f1 100644 --- a/Sources/OpenAPIRuntime/Base/WarningSuppressingAnnotations.swift +++ b/Sources/OpenAPIRuntime/Base/WarningSuppressingAnnotations.swift @@ -24,9 +24,7 @@ /// There should be no runtime impact in release builds, as the function is inlined and /// has no executable code. /// - Parameter value: The value for which you want to suppress "variable was never mutated, change to let" warnings. -@_spi(Generated) -@inline(__always) -public func suppressMutabilityWarning(_ value: inout T) {} +@_spi(Generated) @inline(__always) public func suppressMutabilityWarning(_ value: inout T) {} /// Suppress "variable unused" warnings. /// @@ -40,6 +38,4 @@ public func suppressMutabilityWarning(_ value: inout T) {} /// There should be no runtime impact in release builds, as the function is inlined and /// has no executable code. /// - Parameter value: The value for which you want to suppress "variable unused" warnings. -@_spi(Generated) -@inline(__always) -public func suppressUnusedWarning(_ value: T) {} +@_spi(Generated) @inline(__always) public func suppressUnusedWarning(_ value: T) {} diff --git a/Sources/OpenAPIRuntime/Conversion/CodableExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CodableExtensions.swift index ef35e83a..5aa893bf 100644 --- a/Sources/OpenAPIRuntime/Conversion/CodableExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CodableExtensions.swift @@ -12,8 +12,7 @@ // //===----------------------------------------------------------------------===// -@_spi(Generated) -extension Decoder { +@_spi(Generated) extension Decoder { // MARK: - Coding SPI @@ -22,9 +21,7 @@ extension Decoder { /// - Throws: When at least one undocumented key is found. /// - Parameter knownKeys: A set of known and already decoded keys. public func ensureNoAdditionalProperties(knownKeys: Set) throws { - let (unknownKeys, container) = try unknownKeysAndContainer( - knownKeys: knownKeys - ) + let (unknownKeys, container) = try unknownKeysAndContainer(knownKeys: knownKeys) guard unknownKeys.isEmpty else { let key = unknownKeys.sorted().first! throw DecodingError.dataCorruptedError( @@ -43,28 +40,13 @@ extension Decoder { /// - Parameter knownKeys: Known and already decoded keys. /// - Returns: A container with the decoded undocumented properties. /// - Throws: An error if decoding additional properties fails. - public func decodeAdditionalProperties( - knownKeys: Set - ) throws -> OpenAPIObjectContainer { - let (unknownKeys, container) = try unknownKeysAndContainer( - knownKeys: knownKeys - ) - guard !unknownKeys.isEmpty else { - return .init() - } + public func decodeAdditionalProperties(knownKeys: Set) throws -> OpenAPIObjectContainer { + let (unknownKeys, container) = try unknownKeysAndContainer(knownKeys: knownKeys) + guard !unknownKeys.isEmpty else { return .init() } let keyValuePairs: [(String, (any Sendable)?)] = try unknownKeys.map { key in - ( - key.stringValue, - try container.decode( - OpenAPIValueContainer.self, - forKey: key - ) - .value - ) + (key.stringValue, try container.decode(OpenAPIValueContainer.self, forKey: key).value) } - return .init( - validatedValue: Dictionary(uniqueKeysWithValues: keyValuePairs) - ) + return .init(validatedValue: Dictionary(uniqueKeysWithValues: keyValuePairs)) } /// Returns decoded additional properties. @@ -74,17 +56,11 @@ extension Decoder { /// - Parameter knownKeys: Known and already decoded keys. /// - Returns: A container with the decoded undocumented properties. /// - Throws: An error if there are issues with decoding the additional properties. - public func decodeAdditionalProperties( - knownKeys: Set - ) throws -> [String: T] { - let (unknownKeys, container) = try unknownKeysAndContainer( - knownKeys: knownKeys - ) - guard !unknownKeys.isEmpty else { - return .init() - } + public func decodeAdditionalProperties(knownKeys: Set) throws -> [String: T] { + let (unknownKeys, container) = try unknownKeysAndContainer(knownKeys: knownKeys) + guard !unknownKeys.isEmpty else { return .init() } let keyValuePairs: [(String, T)] = try unknownKeys.compactMap { key in - return (key.stringValue, try container.decode(T.self, forKey: key)) + (key.stringValue, try container.decode(T.self, forKey: key)) } return .init(uniqueKeysWithValues: keyValuePairs) } @@ -93,9 +69,7 @@ extension Decoder { /// - Parameter type: The type to decode. /// - Returns: The decoded value. /// - Throws: An error if there are issues with decoding the value from the single value container. - public func decodeFromSingleValueContainer( - _ type: T.Type = T.self - ) throws -> T { + public func decodeFromSingleValueContainer(_ type: T.Type = T.self) throws -> T { let container = try singleValueContainer() return try container.decode(T.self) } @@ -111,36 +85,27 @@ extension Decoder { /// for further decoding of the unknown properties. /// - Throws: An error if there are issues with creating the decoding container or identifying /// the unknown keys. - private func unknownKeysAndContainer( - knownKeys: Set - ) throws -> (Set, KeyedDecodingContainer) { + private func unknownKeysAndContainer(knownKeys: Set) throws -> ( + Set, KeyedDecodingContainer + ) { let container = try container(keyedBy: StringKey.self) - let unknownKeys = Set(container.allKeys) - .subtracting(knownKeys.map(StringKey.init(_:))) + let unknownKeys = Set(container.allKeys).subtracting(knownKeys.map(StringKey.init(_:))) return (unknownKeys, container) } } -@_spi(Generated) -extension Encoder { +@_spi(Generated) extension Encoder { /// Encodes additional properties into the encoder. /// /// The properties are encoded directly into the encoder, rather that /// into a nested container. /// - Parameter additionalProperties: A container of additional properties. /// - Throws: An error if there are issues with encoding the additional properties. - public func encodeAdditionalProperties( - _ additionalProperties: OpenAPIObjectContainer - ) throws { - guard !additionalProperties.value.isEmpty else { - return - } + public func encodeAdditionalProperties(_ additionalProperties: OpenAPIObjectContainer) throws { + guard !additionalProperties.value.isEmpty else { return } var container = container(keyedBy: StringKey.self) for (key, value) in additionalProperties.value { - try container.encode( - OpenAPIValueContainer(unvalidatedValue: value), - forKey: .init(key) - ) + try container.encode(OpenAPIValueContainer(unvalidatedValue: value), forKey: .init(key)) } } @@ -150,24 +115,16 @@ extension Encoder { /// into a nested container. /// - Parameter additionalProperties: A container of additional properties. /// - Throws: An error if there are issues with encoding the additional properties. - public func encodeAdditionalProperties( - _ additionalProperties: [String: T] - ) throws { - guard !additionalProperties.isEmpty else { - return - } + public func encodeAdditionalProperties(_ additionalProperties: [String: T]) throws { + guard !additionalProperties.isEmpty else { return } var container = container(keyedBy: StringKey.self) - for (key, value) in additionalProperties { - try container.encode(value, forKey: .init(key)) - } + for (key, value) in additionalProperties { try container.encode(value, forKey: .init(key)) } } /// Encodes the value into the encoder using a single value container. /// - Parameter value: The value to encode. /// - Throws: An error if there are issues with encoding the value. - public func encodeToSingleValueContainer( - _ value: T - ) throws { + public func encodeToSingleValueContainer(_ value: T) throws { var container = singleValueContainer() try container.encode(value) } @@ -176,9 +133,7 @@ extension Encoder { /// the encoder using a single value container. /// - Parameter values: An array of optional values. /// - Throws: An error if there are issues with encoding the value. - public func encodeFirstNonNilValueToSingleValueContainer( - _ values: [(any Encodable)?] - ) throws { + public func encodeFirstNonNilValueToSingleValueContainer(_ values: [(any Encodable)?]) throws { for value in values { if let value { try encodeToSingleValueContainer(value) @@ -192,23 +147,13 @@ extension Encoder { private struct StringKey: CodingKey, Hashable, Comparable { var stringValue: String - var intValue: Int? { - Int(stringValue) - } + var intValue: Int? { Int(stringValue) } - init(_ string: String) { - self.stringValue = string - } + init(_ string: String) { self.stringValue = string } - init?(stringValue: String) { - self.stringValue = stringValue - } + init?(stringValue: String) { self.stringValue = stringValue } - init?(intValue: Int) { - self.stringValue = String(intValue) - } + init?(intValue: Int) { self.stringValue = String(intValue) } - static func < (lhs: StringKey, rhs: StringKey) -> Bool { - lhs.stringValue < rhs.stringValue - } + static func < (lhs: StringKey, rhs: StringKey) -> Bool { lhs.stringValue < rhs.stringValue } } diff --git a/Sources/OpenAPIRuntime/Conversion/Configuration.swift b/Sources/OpenAPIRuntime/Conversion/Configuration.swift index 439b424b..93b00f32 100644 --- a/Sources/OpenAPIRuntime/Conversion/Configuration.swift +++ b/Sources/OpenAPIRuntime/Conversion/Configuration.swift @@ -28,18 +28,13 @@ public protocol DateTranscoder: Sendable { public struct ISO8601DateTranscoder: DateTranscoder { /// Creates and returns an ISO 8601 formatted string representation of the specified date. - public func encode(_ date: Date) throws -> String { - ISO8601DateFormatter().string(from: date) - } + public func encode(_ date: Date) throws -> String { ISO8601DateFormatter().string(from: date) } /// Creates and returns a date object from the specified ISO 8601 formatted string representation. public func decode(_ dateString: String) throws -> Date { guard let date = ISO8601DateFormatter().date(from: dateString) else { throw DecodingError.dataCorrupted( - .init( - codingPath: [], - debugDescription: "Expected date string to be ISO8601-formatted." - ) + .init(codingPath: [], debugDescription: "Expected date string to be ISO8601-formatted.") ) } return date @@ -48,15 +43,13 @@ public struct ISO8601DateTranscoder: DateTranscoder { extension DateTranscoder where Self == ISO8601DateTranscoder { /// A transcoder that transcodes dates as ISO-8601–formatted string (in RFC 3339 format). - public static var iso8601: Self { - ISO8601DateTranscoder() - } + public static var iso8601: Self { ISO8601DateTranscoder() } } extension JSONEncoder.DateEncodingStrategy { /// Encode the `Date` as a custom value encoded using the given ``DateTranscoder``. static func from(dateTranscoder: any DateTranscoder) -> Self { - return .custom { date, encoder in + .custom { date, encoder in let dateAsString = try dateTranscoder.encode(date) var container = encoder.singleValueContainer() try container.encode(dateAsString) @@ -67,7 +60,7 @@ extension JSONEncoder.DateEncodingStrategy { extension JSONDecoder.DateDecodingStrategy { /// Decode the `Date` as a custom value decoded by the given ``DateTranscoder``. static func from(dateTranscoder: any DateTranscoder) -> Self { - return .custom { decoder in + .custom { decoder in let container = try decoder.singleValueContainer() let dateString = try container.decode(String.self) return try dateTranscoder.decode(dateString) @@ -85,9 +78,5 @@ public struct Configuration: Sendable { /// /// - Parameter dateTranscoder: The transcoder to use when converting between date /// and string values. - public init( - dateTranscoder: any DateTranscoder = .iso8601 - ) { - self.dateTranscoder = dateTranscoder - } + public init(dateTranscoder: any DateTranscoder = .iso8601) { self.dateTranscoder = dateTranscoder } } diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index 4c6950b2..4b723cac 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -23,9 +23,7 @@ extension Converter { public func setAcceptHeader( in headerFields: inout HTTPFields, contentTypes: [AcceptHeaderContentType] - ) { - headerFields[.accept] = contentTypes.map(\.rawValue).joined(separator: ", ") - } + ) { headerFields[.accept] = contentTypes.map(\.rawValue).joined(separator: ", ") } /// Renders the path template with the specified parameters to construct a URI. /// @@ -36,10 +34,7 @@ extension Converter { /// - Returns: A URI path string with placeholders replaced by the provided parameters. /// /// - Throws: An error if rendering the path fails. - public func renderedPath( - template: String, - parameters: [any Encodable] - ) throws -> String { + public func renderedPath(template: String, parameters: [any Encodable]) throws -> String { var renderedString = template let encoder = URIEncoder( configuration: .init( @@ -52,11 +47,7 @@ extension Converter { for parameter in parameters { let value = try encoder.encode(parameter, forKey: "") if let range = renderedString.range(of: "{}") { - renderedString = renderedString.replacingOccurrences( - of: "{}", - with: value, - range: range - ) + renderedString = renderedString.replacingOccurrences(of: "{}", with: value, range: range) } } return renderedString @@ -86,13 +77,7 @@ extension Converter { name: name, value: value, convert: { value, style, explode in - try convertToURI( - style: style, - explode: explode, - inBody: false, - key: name, - value: value - ) + try convertToURI(style: style, explode: explode, inBody: false, key: name, value: value) } ) } @@ -153,18 +138,9 @@ extension Converter { /// - Returns: An `HTTPBody` representing the binary request body, or `nil` if the `value` is `nil`. /// /// - Throws: An error if setting the request body as binary fails. - public func setOptionalRequestBodyAsBinary( - _ value: HTTPBody?, - headerFields: inout HTTPFields, - contentType: String - ) throws -> HTTPBody? { - try setOptionalRequestBody( - value, - headerFields: &headerFields, - contentType: contentType, - convert: { $0 } - ) - } + public func setOptionalRequestBodyAsBinary(_ value: HTTPBody?, headerFields: inout HTTPFields, contentType: String) + throws -> HTTPBody? + { try setOptionalRequestBody(value, headerFields: &headerFields, contentType: contentType, convert: { $0 }) } /// Sets a required request body as binary in the specified header fields and returns an `HTTPBody`. /// @@ -176,18 +152,9 @@ extension Converter { /// - Returns: An `HTTPBody` representing the binary request body. /// /// - Throws: An error if setting the request body as binary fails. - public func setRequiredRequestBodyAsBinary( - _ value: HTTPBody, - headerFields: inout HTTPFields, - contentType: String - ) throws -> HTTPBody { - try setRequiredRequestBody( - value, - headerFields: &headerFields, - contentType: contentType, - convert: { $0 } - ) - } + public func setRequiredRequestBodyAsBinary(_ value: HTTPBody, headerFields: inout HTTPFields, contentType: String) + throws -> HTTPBody + { try setRequiredRequestBody(value, headerFields: &headerFields, contentType: contentType, convert: { $0 }) } /// Sets an optional request body as URL-encoded form data in the specified header fields and returns an `HTTPBody`. /// @@ -250,9 +217,7 @@ extension Converter { from data: HTTPBody?, transforming transform: (T) -> C ) async throws -> C { - guard let data else { - throw RuntimeError.missingRequiredResponseBody - } + guard let data else { throw RuntimeError.missingRequiredResponseBody } return try await getBufferingResponseBody( type, from: data, @@ -276,14 +241,7 @@ extension Converter { from data: HTTPBody?, transforming transform: (HTTPBody) -> C ) throws -> C { - guard let data else { - throw RuntimeError.missingRequiredResponseBody - } - return try getResponseBody( - type, - from: data, - transforming: transform, - convert: { $0 } - ) + guard let data else { throw RuntimeError.missingRequiredResponseBody } + return try getResponseBody(type, from: data, transforming: transform, convert: { $0 }) } } diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift index a9c27f9c..a7f8e979 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift @@ -23,9 +23,7 @@ extension Converter { /// type header. /// - Returns: The content type value, or nil if not found or invalid. public func extractContentTypeIfPresent(in headerFields: HTTPFields) -> OpenAPIMIMEType? { - guard let rawValue = headerFields[.contentType] else { - return nil - } + guard let rawValue = headerFields[.contentType] else { return nil } return OpenAPIMIMEType(rawValue) } @@ -37,15 +35,9 @@ extension Converter { /// - Returns: The most appropriate option. /// - Throws: If none of the options match the received content type. /// - Precondition: `options` must not be empty. - public func bestContentType( - received: OpenAPIMIMEType?, - options: [String] - ) throws -> String { + public func bestContentType(received: OpenAPIMIMEType?, options: [String]) throws -> String { precondition(!options.isEmpty, "bestContentType options must not be empty.") - guard - let received, - case let .concrete(type: receivedType, subtype: receivedSubtype) = received.kind - else { + guard let received, case let .concrete(type: receivedType, subtype: receivedSubtype) = received.kind else { // If none received or if we received a wildcard, use the first one. // This behavior isn't well defined by the OpenAPI specification. // Note: We treat a partial wildcard, like `image/*` as a full @@ -66,13 +58,10 @@ extension Converter { ) return (contentType: stringOption, match: match) } - let bestOption = evaluatedOptions.max { a, b in - a.match.score < b.match.score - }! // Safe, we only get here if the array is not empty. + // The force unwrap is safe, we only get here if the array is not empty. + let bestOption = evaluatedOptions.max { a, b in a.match.score < b.match.score }! let bestContentType = bestOption.contentType - if case .incompatible = bestOption.match { - throw RuntimeError.unexpectedContentTypeHeader(bestContentType) - } + if case .incompatible = bestOption.match { throw RuntimeError.unexpectedContentTypeHeader(bestContentType) } return bestContentType } @@ -85,27 +74,13 @@ extension Converter { /// - name: The name of the header field. /// - value: The optional value to be encoded as a URI component if not nil. /// - Throws: An error if there's an issue with encoding the value as a URI component. - public func setHeaderFieldAsURI( - in headerFields: inout HTTPFields, - name: String, - value: T? - ) throws { - guard let value else { - return - } + public func setHeaderFieldAsURI(in headerFields: inout HTTPFields, name: String, value: T?) throws { + guard let value else { return } try setHeaderField( in: &headerFields, name: name, value: value, - convert: { value in - try convertToURI( - style: .simple, - explode: false, - inBody: false, - key: "", - value: value - ) - } + convert: { value in try convertToURI(style: .simple, explode: false, inBody: false, key: "", value: value) } ) } @@ -116,17 +91,8 @@ extension Converter { /// - name: The name of the header field. /// - value: The optional value to be encoded as a JSON component if not nil. /// - Throws: An error if there's an issue with encoding the value as a JSON component. - public func setHeaderFieldAsJSON( - in headerFields: inout HTTPFields, - name: String, - value: T? - ) throws { - try setHeaderField( - in: &headerFields, - name: name, - value: value, - convert: convertHeaderFieldCodableToJSON - ) + public func setHeaderFieldAsJSON(in headerFields: inout HTTPFields, name: String, value: T?) throws { + try setHeaderField(in: &headerFields, name: name, value: value, convert: convertHeaderFieldCodableToJSON) } /// Attempts to retrieve an optional header field value and decodes it as a URI component, returning it as the specified type. @@ -138,23 +104,15 @@ extension Converter { /// - Returns: The decoded header field value as the specified type, or `nil` if the field is not present. /// - Throws: An error if there's an issue with decoding the URI component or /// if the field is present but cannot be decoded as the specified type. - public func getOptionalHeaderFieldAsURI( - in headerFields: HTTPFields, - name: String, - as type: T.Type - ) throws -> T? { + public func getOptionalHeaderFieldAsURI(in headerFields: HTTPFields, name: String, as type: T.Type) + throws -> T? + { try getOptionalHeaderField( in: headerFields, name: name, as: type, convert: { encodedValue in - try convertFromURI( - style: .simple, - explode: false, - inBody: false, - key: "", - encodedValue: encodedValue - ) + try convertFromURI(style: .simple, explode: false, inBody: false, key: "", encodedValue: encodedValue) } ) } @@ -168,23 +126,15 @@ extension Converter { /// - Returns: The decoded header field value as the specified type. /// - Throws: An error if the field is not present or if there's an issue with decoding the URI component or /// if the field is present but cannot be decoded as the specified type. - public func getRequiredHeaderFieldAsURI( - in headerFields: HTTPFields, - name: String, - as type: T.Type - ) throws -> T { + public func getRequiredHeaderFieldAsURI(in headerFields: HTTPFields, name: String, as type: T.Type) + throws -> T + { try getRequiredHeaderField( in: headerFields, name: name, as: type, convert: { encodedValue in - try convertFromURI( - style: .simple, - explode: false, - inBody: false, - key: "", - encodedValue: encodedValue - ) + try convertFromURI(style: .simple, explode: false, inBody: false, key: "", encodedValue: encodedValue) } ) } @@ -198,18 +148,9 @@ extension Converter { /// - Returns: The decoded header field value as the specified type, or /// `nil` if the field is not present in the headerFields dictionary. /// - Throws: An error if there's an issue with decoding the JSON value or if the field is present but cannot be decoded as the specified type. - public func getOptionalHeaderFieldAsJSON( - in headerFields: HTTPFields, - name: String, - as type: T.Type - ) throws -> T? { - try getOptionalHeaderField( - in: headerFields, - name: name, - as: type, - convert: convertJSONToHeaderFieldCodable - ) - } + public func getOptionalHeaderFieldAsJSON(in headerFields: HTTPFields, name: String, as type: T.Type) + throws -> T? + { try getOptionalHeaderField(in: headerFields, name: name, as: type, convert: convertJSONToHeaderFieldCodable) } /// Retrieves a required header field value and decodes it as JSON, returning it as the specified type. /// @@ -220,16 +161,7 @@ extension Converter { /// - Returns: The decoded header field value as the specified type. /// - Throws: An error if the field is not present in the headerFields dictionary, if there's an issue with decoding the JSON value, /// or if the field cannot be decoded as the specified type. - public func getRequiredHeaderFieldAsJSON( - in headerFields: HTTPFields, - name: String, - as type: T.Type - ) throws -> T { - try getRequiredHeaderField( - in: headerFields, - name: name, - as: type, - convert: convertJSONToHeaderFieldCodable - ) - } + public func getRequiredHeaderFieldAsJSON(in headerFields: HTTPFields, name: String, as type: T.Type) + throws -> T + { try getRequiredHeaderField(in: headerFields, name: name, as: type, convert: convertJSONToHeaderFieldCodable) } } diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index ee84c71e..c354d4aa 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -24,17 +24,11 @@ extension Converter { /// - Returns: The parsed content types, or the default content types if /// the header was not provided. /// - Throws: An error if the "accept" header is present but malformed, or if there are issues parsing its components. - public func extractAcceptHeaderIfPresent( - in headerFields: HTTPFields - ) throws -> [AcceptHeaderContentType] { - guard let rawValue = headerFields[.accept] else { - return AcceptHeaderContentType.defaultValues - } - let rawComponents = - rawValue - .split(separator: ",") - .map(String.init) - .map(\.trimmingLeadingAndTrailingSpaces) + public func extractAcceptHeaderIfPresent(in headerFields: HTTPFields) throws + -> [AcceptHeaderContentType] + { + guard let rawValue = headerFields[.accept] else { return AcceptHeaderContentType.defaultValues } + let rawComponents = rawValue.split(separator: ",").map(String.init).map(\.trimmingLeadingAndTrailingSpaces) let parsedComponents = try rawComponents.map { rawComponent in guard let value = AcceptHeaderContentType(rawValue: rawComponent) else { throw RuntimeError.malformedAcceptHeader(rawComponent) @@ -52,39 +46,21 @@ extension Converter { /// Also supports wildcars, such as "application/\*" and "\*/\*". /// - Throws: An error if the "Accept" header is present but incompatible with the provided content type, /// or if there are issues parsing the header. - public func validateAcceptIfPresent( - _ substring: String, - in headerFields: HTTPFields - ) throws { + public func validateAcceptIfPresent(_ substring: String, in headerFields: HTTPFields) throws { // for example: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8 - guard let acceptHeader = headerFields[.accept] else { - return - } + guard let acceptHeader = headerFields[.accept] else { return } // Split with commas to get the individual values - let acceptValues = - acceptHeader - .split(separator: ",") + let acceptValues = acceptHeader.split(separator: ",") .map { value in // Drop everything after the optional semicolon (q, extensions, ...) - value - .split(separator: ";")[0] - .trimmingCharacters(in: .whitespacesAndNewlines) - .lowercased() + value.split(separator: ";")[0].trimmingCharacters(in: .whitespacesAndNewlines).lowercased() } - if acceptValues.isEmpty { - return - } - if acceptValues.contains("*/*") { - return - } - if acceptValues.contains("\(substring.split(separator: "/")[0].lowercased())/*") { - return - } - if acceptValues.contains(where: { $0.localizedCaseInsensitiveContains(substring) }) { - return - } + if acceptValues.isEmpty { return } + if acceptValues.contains("*/*") { return } + if acceptValues.contains("\(substring.split(separator: "/")[0].lowercased())/*") { return } + if acceptValues.contains(where: { $0.localizedCaseInsensitiveContains(substring) }) { return } throw RuntimeError.unexpectedAcceptHeader(acceptHeader) } @@ -114,11 +90,7 @@ extension Converter { dateTranscoder: configuration.dateTranscoder ) ) - let value = try decoder.decode( - T.self, - forKey: name, - from: encodedString - ) + let value = try decoder.decode(T.self, forKey: name, from: encodedString) return value } ) @@ -156,11 +128,7 @@ extension Converter { dateTranscoder: configuration.dateTranscoder ) ) - let value = try decoder.decodeIfPresent( - T.self, - forKey: name, - from: query - ) + let value = try decoder.decodeIfPresent(T.self, forKey: name, from: query) return value } ) @@ -198,11 +166,7 @@ extension Converter { dateTranscoder: configuration.dateTranscoder ) ) - let value = try decoder.decode( - T.self, - forKey: name, - from: query - ) + let value = try decoder.decode(T.self, forKey: name, from: query) return value } ) @@ -262,14 +226,7 @@ extension Converter { _ type: HTTPBody.Type, from data: HTTPBody?, transforming transform: (HTTPBody) -> C - ) throws -> C? { - try getOptionalRequestBody( - type, - from: data, - transforming: transform, - convert: { $0 } - ) - } + ) throws -> C? { try getOptionalRequestBody(type, from: data, transforming: transform, convert: { $0 }) } /// Retrieves and transforms a required binary request body. /// @@ -283,14 +240,7 @@ extension Converter { _ type: HTTPBody.Type, from data: HTTPBody?, transforming transform: (HTTPBody) -> C - ) throws -> C { - try getRequiredRequestBody( - type, - from: data, - transforming: transform, - convert: { $0 } - ) - } + ) throws -> C { try getRequiredRequestBody(type, from: data, transforming: transform, convert: { $0 }) } /// Retrieves and transforms an optional URL-encoded form request body. /// @@ -342,11 +292,9 @@ extension Converter { /// - contentType: The content type to set in the HTTP header fields. /// - Returns: An `HTTPBody` with the response body set as JSON data. /// - Throws: An error if serialization or setting the response body fails. - public func setResponseBodyAsJSON( - _ value: T, - headerFields: inout HTTPFields, - contentType: String - ) throws -> HTTPBody { + public func setResponseBodyAsJSON(_ value: T, headerFields: inout HTTPFields, contentType: String) + throws -> HTTPBody + { try setResponseBody( value, headerFields: &headerFields, @@ -363,16 +311,7 @@ extension Converter { /// - contentType: The content type to set in the header fields. /// - Returns: The updated `HTTPBody` containing the binary response data. /// - Throws: An error if there are issues setting the response body or updating the header fields. - public func setResponseBodyAsBinary( - _ value: HTTPBody, - headerFields: inout HTTPFields, - contentType: String - ) throws -> HTTPBody { - try setResponseBody( - value, - headerFields: &headerFields, - contentType: contentType, - convert: { $0 } - ) - } + public func setResponseBodyAsBinary(_ value: HTTPBody, headerFields: inout HTTPFields, contentType: String) throws + -> HTTPBody + { try setResponseBody(value, headerFields: &headerFields, contentType: contentType, convert: { $0 }) } } diff --git a/Sources/OpenAPIRuntime/Conversion/Converter.swift b/Sources/OpenAPIRuntime/Conversion/Converter.swift index 2cf78d99..bd7566b9 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter.swift @@ -19,8 +19,7 @@ import Foundation #endif /// Converter between generated and HTTP currency types. -@_spi(Generated) -public struct Converter: Sendable { +@_spi(Generated) public struct Converter: Sendable { /// Configuration used to set up the converter. public let configuration: Configuration @@ -35,9 +34,7 @@ public struct Converter: Sendable { internal var headerFieldEncoder: JSONEncoder /// Creates a new converter with the behavior specified by the configuration. - public init( - configuration: Configuration - ) { + public init(configuration: Configuration) { self.configuration = configuration self.encoder = JSONEncoder() diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index 3b7a7d41..55765921 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -24,11 +24,9 @@ extension ParameterStyle { /// - 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?, - explode: Bool? - ) throws -> (ParameterStyle, Bool) { + static func resolvedQueryStyleAndExplode(name: String, style: ParameterStyle?, explode: Bool?) throws -> ( + ParameterStyle, Bool + ) { let resolvedStyle = style ?? .defaultForQueryItems let resolvedExplode = explode ?? ParameterStyle.defaultExplodeFor(forStyle: resolvedStyle) guard resolvedStyle == .form else { @@ -49,9 +47,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(name) else { - throw RuntimeError.invalidHeaderFieldName(name) - } + guard let fieldName = Self(name) else { throw RuntimeError.invalidHeaderFieldName(name) } self = fieldName } } @@ -61,9 +57,7 @@ extension HTTPRequest { /// Returns the path of the request, and throws an error if it's nil. var requiredPath: Substring { get throws { - guard let path else { - throw RuntimeError.pathUnset - } + guard let path else { throw RuntimeError.pathUnset } return path[...] } } @@ -81,11 +75,7 @@ extension Converter { /// 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, - inBody: Bool - ) -> URICoderConfiguration { + func uriCoderConfiguration(style: ParameterStyle, explode: Bool, inBody: Bool) -> URICoderConfiguration { .init( style: .init(style), explode: explode, @@ -105,20 +95,10 @@ extension Converter { /// - value: The value to be encoded. /// - Returns: A URI encoded string. /// - Throws: An error if encoding fails. - func convertToURI( - style: ParameterStyle, - explode: Bool, - inBody: Bool, - key: String, - value: T - ) throws -> String { - let encoder = URIEncoder( - configuration: uriCoderConfiguration( - style: style, - explode: explode, - inBody: inBody - ) - ) + func convertToURI(style: ParameterStyle, explode: Bool, inBody: Bool, key: String, value: T) throws + -> String + { + let encoder = URIEncoder(configuration: uriCoderConfiguration(style: style, explode: explode, inBody: inBody)) let encodedString = try encoder.encode(value, forKey: key) return encodedString } @@ -141,18 +121,8 @@ extension Converter { key: String, encodedValue: Substring ) throws -> T { - let decoder = URIDecoder( - configuration: uriCoderConfiguration( - style: style, - explode: explode, - inBody: inBody - ) - ) - let value = try decoder.decode( - T.self, - forKey: key, - from: encodedValue - ) + let decoder = URIDecoder(configuration: uriCoderConfiguration(style: style, explode: explode, inBody: inBody)) + let value = try decoder.decode(T.self, forKey: key, from: encodedValue) return value } @@ -160,9 +130,7 @@ extension Converter { /// - Parameter body: The body containing the raw JSON bytes. /// - Returns: A decoded value. /// - Throws: An error if decoding from the body fails. - func convertJSONToBodyCodable( - _ body: HTTPBody - ) async throws -> T { + func convertJSONToBodyCodable(_ body: HTTPBody) async throws -> T { let data = try await Data(collecting: body, upTo: .max) return try decoder.decode(T.self, from: data) } @@ -171,9 +139,7 @@ extension Converter { /// - Parameter value: The value to encode as JSON. /// - Returns: The raw JSON body. /// - Throws: An error if encoding to JSON fails. - func convertBodyCodableToJSON( - _ value: T - ) throws -> HTTPBody { + func convertBodyCodableToJSON(_ value: T) throws -> HTTPBody { let data = try encoder.encode(value) return HTTPBody(data) } @@ -182,9 +148,7 @@ extension Converter { /// - Parameter body: The body containing the raw URL-encoded form bytes. /// - Returns: A decoded value. /// - Throws: An error if decoding from the URL-encoded form fails. - func convertURLEncodedFormToCodable( - _ body: HTTPBody - ) async throws -> T { + func convertURLEncodedFormToCodable(_ body: HTTPBody) async throws -> T { let decoder = URIDecoder( configuration: .init( style: .form, @@ -202,9 +166,7 @@ extension Converter { /// - Parameter value: The value to encode. /// - Returns: The raw URL-encoded form body. /// - Throws: An error if encoding to URL-encoded form fails. - func convertBodyCodableToURLFormData( - _ value: T - ) throws -> HTTPBody { + func convertBodyCodableToURLFormData(_ value: T) throws -> HTTPBody { let encoder = URIEncoder( configuration: .init( style: .form, @@ -221,9 +183,7 @@ extension Converter { /// - Parameter value: The value to encode. /// - Returns: A JSON string. /// - Throws: An error if encoding the value to JSON fails. - func convertHeaderFieldCodableToJSON( - _ value: T - ) throws -> String { + func convertHeaderFieldCodableToJSON(_ value: T) throws -> String { let data = try headerFieldEncoder.encode(value) let stringValue = String(decoding: data, as: UTF8.self) return stringValue @@ -233,9 +193,7 @@ extension Converter { /// - Parameter stringValue: A JSON string. /// - Returns: The decoded value. /// - Throws: An error if decoding from the JSON string fails. - func convertJSONToHeaderFieldCodable( - _ stringValue: Substring - ) throws -> T { + func convertJSONToHeaderFieldCodable(_ stringValue: Substring) throws -> T { let data = Data(stringValue.utf8) return try decoder.decode(T.self, from: data) } @@ -249,21 +207,11 @@ extension Converter { /// - value: The value of the header to set. /// - convert: The closure used to serialize the header value to string. /// - Throws: An error if an issue occurs while serializing the header value. - func setHeaderField( - in headerFields: inout HTTPFields, - name: String, - value: T?, - convert: (T) throws -> String - ) throws { - guard let value else { - return - } - try headerFields.append( - .init( - name: .init(validated: name), - value: convert(value) - ) - ) + func setHeaderField(in headerFields: inout HTTPFields, name: String, value: T?, convert: (T) throws -> String) + throws + { + guard let value else { return } + try headerFields.append(.init(name: .init(validated: name), value: convert(value))) } /// Returns the value of the header with the provided name from the provided @@ -273,10 +221,7 @@ extension Converter { /// - name: The name of the header field. /// - Returns: The value of the header field, if found. Nil otherwise. /// - Throws: An error if an issue occurs while retrieving the header value. - func getHeaderFieldValuesString( - in headerFields: HTTPFields, - name: String - ) throws -> String? { + func getHeaderFieldValuesString(in headerFields: HTTPFields, name: String) throws -> String? { try headerFields[.init(validated: name)] } @@ -294,14 +239,7 @@ extension Converter { as type: T.Type, convert: (Substring) throws -> T ) throws -> T? { - guard - let stringValue = try getHeaderFieldValuesString( - in: headerFields, - name: name - ) - else { - return nil - } + guard let stringValue = try getHeaderFieldValuesString(in: headerFields, name: name) else { return nil } return try convert(stringValue[...]) } @@ -320,12 +258,7 @@ extension Converter { as type: T.Type, convert: (Substring) throws -> T ) throws -> T { - guard - let stringValue = try getHeaderFieldValuesString( - in: headerFields, - name: name - ) - else { + guard let stringValue = try getHeaderFieldValuesString(in: headerFields, name: name) else { throw RuntimeError.missingRequiredHeaderField(name) } return try convert(stringValue[...]) @@ -349,9 +282,7 @@ extension Converter { value: T?, convert: (T, ParameterStyle, Bool) throws -> String ) throws { - guard let value else { - return - } + guard let value else { return } let (resolvedStyle, resolvedExplode) = try ParameterStyle.resolvedQueryStyleAndExplode( name: name, style: style, @@ -403,9 +334,7 @@ extension Converter { as type: T.Type, convert: (Substring, ParameterStyle, Bool) throws -> T? ) throws -> T? { - guard let query, !query.isEmpty else { - return nil - } + guard let query, !query.isEmpty else { return nil } let (resolvedStyle, resolvedExplode) = try ParameterStyle.resolvedQueryStyleAndExplode( name: name, style: style, @@ -442,9 +371,7 @@ extension Converter { as: type, convert: convert ) - else { - throw RuntimeError.missingRequiredQueryParameter(name) - } + else { throw RuntimeError.missingRequiredQueryParameter(name) } return value } @@ -482,9 +409,7 @@ extension Converter { contentType: String, convert: (T) throws -> HTTPBody ) throws -> HTTPBody? { - guard let value else { - return nil - } + guard let value else { return nil } return try setRequiredRequestBody( value, headerFields: &headerFields, @@ -507,9 +432,7 @@ extension Converter { transforming transform: (T) -> C, convert: (HTTPBody) async throws -> T ) async throws -> C? { - guard let body else { - return nil - } + guard let body else { return nil } let decoded = try await convert(body) return transform(decoded) } @@ -535,9 +458,7 @@ extension Converter { transforming: transform, convert: convert ) - else { - throw RuntimeError.missingRequiredRequestBody - } + else { throw RuntimeError.missingRequiredRequestBody } return body } @@ -555,9 +476,7 @@ extension Converter { transforming transform: (T) -> C, convert: (HTTPBody) throws -> T ) throws -> C? { - guard let body else { - return nil - } + guard let body else { return nil } let decoded = try convert(body) return transform(decoded) } @@ -576,14 +495,7 @@ extension Converter { transforming transform: (T) -> C, convert: (HTTPBody) throws -> T ) throws -> C { - guard - let body = try getOptionalRequestBody( - type, - from: body, - transforming: transform, - convert: convert - ) - else { + guard let body = try getOptionalRequestBody(type, from: body, transforming: transform, convert: convert) else { throw RuntimeError.missingRequiredRequestBody } return body @@ -660,9 +572,7 @@ extension Converter { as type: T.Type, convert: (Substring) throws -> T ) throws -> T { - guard let untypedValue = pathParameters[name] else { - throw RuntimeError.missingRequiredPathParameter(name) - } + guard let untypedValue = pathParameters[name] else { throw RuntimeError.missingRequiredPathParameter(name) } return try convert(untypedValue) } } diff --git a/Sources/OpenAPIRuntime/Conversion/ErrorExtensions.swift b/Sources/OpenAPIRuntime/Conversion/ErrorExtensions.swift index 20b7a76a..9d21513c 100644 --- a/Sources/OpenAPIRuntime/Conversion/ErrorExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/ErrorExtensions.swift @@ -24,11 +24,7 @@ extension DecodingError { /// the type. /// - errors: The errors encountered when decoding individual cases. /// - Returns: A decoding error. - static func failedToDecodeAnySchema( - type: Any.Type, - codingPath: [any CodingKey], - errors: [any Error] - ) -> Self { + static func failedToDecodeAnySchema(type: Any.Type, codingPath: [any CodingKey], errors: [any Error]) -> Self { DecodingError.valueNotFound( type, DecodingError.Context.init( @@ -48,8 +44,7 @@ extension DecodingError { /// the type. /// - errors: The errors encountered when decoding individual cases. /// - Returns: A decoding error. - @_spi(Generated) - public static func failedToDecodeOneOfSchema( + @_spi(Generated) public static func failedToDecodeOneOfSchema( type: Any.Type, codingPath: [any CodingKey], errors: [any Error] @@ -72,13 +67,12 @@ extension DecodingError { /// - codingPath: The coding path to the decoder that attempted to decode /// the type, with the discriminator value as the last component. /// - Returns: A decoding error. - @_spi(Generated) - public static func unknownOneOfDiscriminator( + @_spi(Generated) public static func unknownOneOfDiscriminator( discriminatorKey: any CodingKey, discriminatorValue: String, codingPath: [any CodingKey] ) -> Self { - return DecodingError.keyNotFound( + DecodingError.keyNotFound( discriminatorKey, DecodingError.Context.init( codingPath: codingPath, @@ -98,19 +92,14 @@ extension DecodingError { /// the type. /// - errors: The errors encountered when decoding individual cases. /// - Throws: An error of type `DecodingError.failedToDecodeAnySchema` if none of the child schemas were successfully decoded. - @_spi(Generated) - public static func verifyAtLeastOneSchemaIsNotNil( + @_spi(Generated) public static func verifyAtLeastOneSchemaIsNotNil( _ values: [Any?], type: Any.Type, codingPath: [any CodingKey], errors: [any Error] ) throws { guard values.contains(where: { $0 != nil }) else { - throw DecodingError.failedToDecodeAnySchema( - type: type, - codingPath: codingPath, - errors: errors - ) + throw DecodingError.failedToDecodeAnySchema(type: type, codingPath: codingPath, errors: errors) } } } @@ -124,21 +113,13 @@ struct MultiError: Swift.Error, LocalizedError, CustomStringConvertible { var description: String { let combinedDescription = - errors - .map { error in - guard let error = error as? (any PrettyStringConvertible) else { - return error.localizedDescription - } + errors.map { error in + guard let error = error as? (any PrettyStringConvertible) else { return error.localizedDescription } return error.prettyDescription } - .enumerated() - .map { ($0.offset + 1, $0.element) } - .map { "Error \($0.0): [\($0.1)]" } - .joined(separator: ", ") + .enumerated().map { ($0.offset + 1, $0.element) }.map { "Error \($0.0): [\($0.1)]" }.joined(separator: ", ") return "MultiError (contains \(errors.count) error\(errors.count == 1 ? "" : "s")): \(combinedDescription)" } - var errorDescription: String? { - description - } + var errorDescription: String? { description } } diff --git a/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift b/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift index 0d39d580..8f0117b3 100644 --- a/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift @@ -17,7 +17,5 @@ extension String { /// Returns the string with leading and trailing whitespace (such as spaces /// and newlines) removed. - var trimmingLeadingAndTrailingSpaces: Self { - trimmingCharacters(in: .whitespacesAndNewlines) - } + var trimmingLeadingAndTrailingSpaces: Self { trimmingCharacters(in: .whitespacesAndNewlines) } } diff --git a/Sources/OpenAPIRuntime/Conversion/ParameterStyles.swift b/Sources/OpenAPIRuntime/Conversion/ParameterStyles.swift index 032fa425..fb95bce7 100644 --- a/Sources/OpenAPIRuntime/Conversion/ParameterStyles.swift +++ b/Sources/OpenAPIRuntime/Conversion/ParameterStyles.swift @@ -15,8 +15,7 @@ /// The serialization style used by a parameter. /// /// Details: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#fixed-fields-10 -@_spi(Generated) -public enum ParameterStyle: Sendable { +@_spi(Generated) public enum ParameterStyle: Sendable { /// The form style. /// @@ -46,18 +45,14 @@ extension ParameterStyle { /// Returns the default value of the explode field for the given style /// - Parameter style: The parameter style. /// - Returns: The explode value. - static func defaultExplodeFor(forStyle style: ParameterStyle) -> Bool { - style == .form - } + static func defaultExplodeFor(forStyle style: ParameterStyle) -> Bool { style == .form } } extension URICoderConfiguration.Style { init(_ style: ParameterStyle) { switch style { - case .form: - self = .form - case .simple: - self = .simple + case .form: self = .form + case .simple: self = .simple } } } diff --git a/Sources/OpenAPIRuntime/Conversion/ServerVariable.swift b/Sources/OpenAPIRuntime/Conversion/ServerVariable.swift index 4a22853c..a9658afa 100644 --- a/Sources/OpenAPIRuntime/Conversion/ServerVariable.swift +++ b/Sources/OpenAPIRuntime/Conversion/ServerVariable.swift @@ -22,11 +22,7 @@ extension URL { /// - variables: A map of variable values to substitute into the URL /// template. /// - Throws: If the provided string doesn't convert to URL. - @_spi(Generated) - public init( - validatingOpenAPIServerURL string: String, - variables: [ServerVariable] - ) throws { + @_spi(Generated) public init(validatingOpenAPIServerURL string: String, variables: [ServerVariable]) throws { var urlString = string for variable in variables { let name = variable.name @@ -42,16 +38,13 @@ extension URL { } urlString = urlString.replacingOccurrences(of: "{\(name)}", with: value) } - guard let url = Self(string: urlString) else { - throw RuntimeError.invalidServerURL(urlString) - } + guard let url = Self(string: urlString) else { throw RuntimeError.invalidServerURL(urlString) } self = url } } /// A variable of a server URL template in the OpenAPI document. -@_spi(Generated) -public struct ServerVariable: Sendable, Hashable { +@_spi(Generated) public struct ServerVariable: Sendable, Hashable { /// The name of the variable. public var name: String diff --git a/Sources/OpenAPIRuntime/Conversion/URLExtensions.swift b/Sources/OpenAPIRuntime/Conversion/URLExtensions.swift index 9c1a66d5..432d78ae 100644 --- a/Sources/OpenAPIRuntime/Conversion/URLExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/URLExtensions.swift @@ -18,9 +18,7 @@ extension URL { /// /// Specification: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#fixed-fields public static let defaultOpenAPIServerURL: Self = { - guard let url = URL(string: "/") else { - fatalError("Failed to create an URL with the string '/'.") - } + guard let url = URL(string: "/") else { fatalError("Failed to create an URL with the string '/'.") } return url }() @@ -28,9 +26,7 @@ extension URL { /// - Parameter string: A URL string. /// - Throws: If the provided string doesn't convert to URL. public init(validatingOpenAPIServerURL string: String) throws { - guard let url = Self(string: string) else { - throw RuntimeError.invalidServerURL(string) - } + guard let url = Self(string: string) else { throw RuntimeError.invalidServerURL(string) } self = url } } diff --git a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift index 5de87792..323da60f 100644 --- a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift +++ b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift @@ -34,8 +34,7 @@ extension ClientError { renamed: "ClientError.init(operationID:operationInput:request:requestBody:baseURL:response:responseBody:causeDescription:underlyingError:)", message: "Use the initializer with a causeDescription parameter." - ) - public init( + ) public init( operationID: String, operationInput: any Sendable, request: HTTPRequest? = nil, @@ -76,8 +75,7 @@ extension ServerError { renamed: "ServerError.init(operationID:request:requestBody:requestMetadata:operationInput:operationOutput:causeDescription:underlyingError:)", message: "Use the initializer with a causeDescription parameter." - ) - public init( + ) public init( operationID: String, request: HTTPRequest, requestBody: HTTPBody?, @@ -104,8 +102,7 @@ extension Converter { /// received. /// - Parameter contentType: The content type that was received. /// - Returns: An error representing an unexpected content type. - @available(*, deprecated) - public func makeUnexpectedContentTypeError(contentType: OpenAPIMIMEType?) -> any Error { + @available(*, deprecated) public func makeUnexpectedContentTypeError(contentType: OpenAPIMIMEType?) -> any Error { RuntimeError.unexpectedContentTypeHeader(contentType?.description ?? "") } @@ -121,22 +118,17 @@ extension Converter { /// - Throws: A `RuntimeError` when `expectedRaw` is not a valid content type. /// - Returns: A Boolean value representing whether the concrete content /// type matches the expected one. - @available(*, deprecated) - public func isMatchingContentType(received: OpenAPIMIMEType?, expectedRaw: String) throws -> Bool { - guard let received else { - return false - } - guard case let .concrete(type: receivedType, subtype: receivedSubtype) = received.kind else { - return false - } + @available(*, deprecated) public func isMatchingContentType(received: OpenAPIMIMEType?, expectedRaw: String) throws + -> Bool + { + guard let received else { return false } + guard case let .concrete(type: receivedType, subtype: receivedSubtype) = received.kind else { return false } guard let expectedContentType = OpenAPIMIMEType(expectedRaw) else { throw RuntimeError.invalidExpectedContentType(expectedRaw) } switch expectedContentType.kind { - case .any: - return true - case .anySubtype(let expectedType): - return receivedType.lowercased() == expectedType.lowercased() + case .any: return true + case .anySubtype(let expectedType): return receivedType.lowercased() == expectedType.lowercased() case .concrete(let expectedType, let expectedSubtype): return receivedType.lowercased() == expectedType.lowercased() && receivedSubtype.lowercased() == expectedSubtype.lowercased() @@ -153,9 +145,7 @@ extension DecodingError { /// - codingPath: The coding path to the decoder that attempted to decode /// the type. /// - Returns: A decoding error. - @_spi(Generated) - @available(*, deprecated) - public static func failedToDecodeOneOfSchema( + @_spi(Generated) @available(*, deprecated) public static func failedToDecodeOneOfSchema( type: Any.Type, codingPath: [any CodingKey] ) -> Self { @@ -176,11 +166,7 @@ extension DecodingError { /// - codingPath: The coding path to the decoder that attempted to decode /// the type. /// - Returns: A decoding error. - @available(*, deprecated) - static func failedToDecodeAnySchema( - type: Any.Type, - codingPath: [any CodingKey] - ) -> Self { + @available(*, deprecated) static func failedToDecodeAnySchema(type: Any.Type, codingPath: [any CodingKey]) -> Self { DecodingError.valueNotFound( type, DecodingError.Context.init( @@ -199,18 +185,13 @@ extension DecodingError { /// - codingPath: The coding path to the decoder that attempted to decode /// the type. /// - Throws: An error of type `DecodingError.failedToDecodeAnySchema` if none of the child schemas were successfully decoded. - @_spi(Generated) - @available(*, deprecated) - public static func verifyAtLeastOneSchemaIsNotNil( + @_spi(Generated) @available(*, deprecated) public static func verifyAtLeastOneSchemaIsNotNil( _ values: [Any?], type: Any.Type, codingPath: [any CodingKey] ) throws { guard values.contains(where: { $0 != nil }) else { - throw DecodingError.failedToDecodeAnySchema( - type: type, - codingPath: codingPath - ) + throw DecodingError.failedToDecodeAnySchema(type: type, codingPath: codingPath) } } } diff --git a/Sources/OpenAPIRuntime/Errors/ClientError.swift b/Sources/OpenAPIRuntime/Errors/ClientError.swift index b820bd4a..5a20f224 100644 --- a/Sources/OpenAPIRuntime/Errors/ClientError.swift +++ b/Sources/OpenAPIRuntime/Errors/ClientError.swift @@ -133,7 +133,5 @@ extension ClientError: LocalizedError { /// This computed property provides a localized human-readable description of the client error, which is suitable for displaying to users. /// /// - Returns: A localized string describing the client error. - public var errorDescription: String? { - description - } + public var errorDescription: String? { description } } diff --git a/Sources/OpenAPIRuntime/Errors/CodingErrors.swift b/Sources/OpenAPIRuntime/Errors/CodingErrors.swift index c31291d7..30a04c4c 100644 --- a/Sources/OpenAPIRuntime/Errors/CodingErrors.swift +++ b/Sources/OpenAPIRuntime/Errors/CodingErrors.swift @@ -17,16 +17,11 @@ extension DecodingError: PrettyStringConvertible { var prettyDescription: String { let output: String switch self { - case .dataCorrupted(let context): - output = "dataCorrupted - \(context.prettyDescription)" - case .keyNotFound(let key, let context): - output = "keyNotFound \(key) - \(context.prettyDescription)" - case .typeMismatch(let type, let context): - output = "typeMismatch \(type) - \(context.prettyDescription)" - case .valueNotFound(let type, let context): - output = "valueNotFound \(type) - \(context.prettyDescription)" - @unknown default: - output = "unknown: \(localizedDescription)" + case .dataCorrupted(let context): output = "dataCorrupted - \(context.prettyDescription)" + case .keyNotFound(let key, let context): output = "keyNotFound \(key) - \(context.prettyDescription)" + case .typeMismatch(let type, let context): output = "typeMismatch \(type) - \(context.prettyDescription)" + case .valueNotFound(let type, let context): output = "valueNotFound \(type) - \(context.prettyDescription)" + @unknown default: output = "unknown: \(localizedDescription)" } return "DecodingError: \(output)" } @@ -43,10 +38,8 @@ extension EncodingError: PrettyStringConvertible { var prettyDescription: String { let output: String switch self { - case .invalidValue(let value, let context): - output = "invalidValue \(value) - \(context.prettyDescription)" - @unknown default: - output = "unknown: \(localizedDescription)" + case .invalidValue(let value, let context): output = "invalidValue \(value) - \(context.prettyDescription)" + @unknown default: output = "unknown: \(localizedDescription)" } return "EncodingError: \(output)" } diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index 4fbab419..ffb39ab7 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -30,9 +30,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret enum ParameterLocation: String, CustomStringConvertible { case query - var description: String { - rawValue - } + var description: String { rawValue } } case unsupportedParameterStyle(name: String, location: ParameterLocation, style: ParameterStyle, explode: Bool) @@ -65,63 +63,41 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret /// A wrapped root cause error, if one was thrown by other code. var underlyingError: (any Error)? { switch self { - case .transportFailed(let error), - .handlerFailed(let error), - .middlewareFailed(_, let error): - return error - default: - return nil + case .transportFailed(let error), .handlerFailed(let error), .middlewareFailed(_, let error): return error + default: return nil } } // MARK: CustomStringConvertible - var description: String { - prettyDescription - } + var description: String { prettyDescription } var prettyDescription: String { switch self { - case .invalidServerURL(let string): - return "Invalid server URL: \(string)" + case .invalidServerURL(let string): return "Invalid server URL: \(string)" case .invalidServerVariableValue(name: let name, value: let value, allowedValues: let allowedValues): return "Invalid server variable named: '\(name)', which has the value: '\(value)', but the only allowed values are: \(allowedValues.map { "'\($0)'" }.joined(separator: ", "))" - case .invalidExpectedContentType(let string): - return "Invalid expected content type: '\(string)'" - case .invalidHeaderFieldName(let name): - return "Invalid header field name: '\(name)'" + case .invalidExpectedContentType(let string): return "Invalid expected content type: '\(string)'" + case .invalidHeaderFieldName(let name): return "Invalid header field name: '\(name)'" case .invalidBase64String(let string): return "Invalid base64-encoded string (first 128 bytes): '\(string.prefix(128))'" - case .failedToDecodeStringConvertibleValue(let string): - return "Failed to decode a value of type '\(string)'." + 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): return "Unsupported parameter style, parameter name: '\(name)', kind: \(location), style: \(style), explode: \(explode)" - case .missingRequiredHeaderField(let name): - return "The required header field named '\(name)' is missing." - case .unexpectedContentTypeHeader(let contentType): - return "Unexpected Content-Type header: \(contentType)" - case .unexpectedAcceptHeader(let accept): - return "Unexpected Accept header: \(accept)" - case .malformedAcceptHeader(let accept): - 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: - return "Missing required request body" - case .missingRequiredResponseBody: - return "Missing required response body" - case .transportFailed: - return "Transport threw an error." - case .middlewareFailed(middlewareType: let type, _): - return "Middleware of type '\(type)' threw an error." - case .handlerFailed: - return "User handler threw an error." + case .missingRequiredHeaderField(let name): return "The required header field named '\(name)' is missing." + case .unexpectedContentTypeHeader(let contentType): return "Unexpected Content-Type header: \(contentType)" + case .unexpectedAcceptHeader(let accept): return "Unexpected Accept header: \(accept)" + case .malformedAcceptHeader(let accept): 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: return "Missing required request body" + case .missingRequiredResponseBody: return "Missing required response body" + case .transportFailed: return "Transport threw an error." + case .middlewareFailed(middlewareType: let type, _): return "Middleware of type '\(type)' threw an error." + case .handlerFailed: return "User handler threw an error." case .unexpectedResponseStatus(let expectedStatus, let response): return "Unexpected response, expected status code: \(expectedStatus), response: \(response)" case .unexpectedResponseBody(let expectedContentType, let body): @@ -136,10 +112,9 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret /// - expectedStatus: The expected HTTP response status as a string. /// - response: The HTTP response data. /// - Throws: An error indicating an unexpected response status. -@_spi(Generated) -public func throwUnexpectedResponseStatus(expectedStatus: String, response: any Sendable) throws -> Never { - throw RuntimeError.unexpectedResponseStatus(expectedStatus: expectedStatus, response: response) -} +@_spi(Generated) public func throwUnexpectedResponseStatus(expectedStatus: String, response: any Sendable) throws + -> Never +{ throw RuntimeError.unexpectedResponseStatus(expectedStatus: expectedStatus, response: response) } /// Throws an error to indicate an unexpected response body content. /// @@ -147,7 +122,6 @@ public func throwUnexpectedResponseStatus(expectedStatus: String, response: any /// - expectedContent: The expected content as a string. /// - body: The response body data. /// - Throws: An error indicating an unexpected response body content. -@_spi(Generated) -public func throwUnexpectedResponseBody(expectedContent: String, body: any Sendable) throws -> Never { +@_spi(Generated) public func throwUnexpectedResponseBody(expectedContent: String, body: any Sendable) throws -> Never { throw RuntimeError.unexpectedResponseBody(expectedContent: expectedContent, body: body) } diff --git a/Sources/OpenAPIRuntime/Errors/ServerError.swift b/Sources/OpenAPIRuntime/Errors/ServerError.swift index 7595a890..92d0552e 100644 --- a/Sources/OpenAPIRuntime/Errors/ServerError.swift +++ b/Sources/OpenAPIRuntime/Errors/ServerError.swift @@ -106,7 +106,5 @@ extension ServerError: LocalizedError { /// This computed property provides a localized human-readable description of the server error, which is suitable for displaying to users. /// /// - Returns: A localized string describing the server error. - public var errorDescription: String? { - description - } + public var errorDescription: String? { description } } diff --git a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift index 5d66ff6b..3786bcea 100644 --- a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift +++ b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift @@ -137,12 +137,9 @@ public protocol ClientTransport: Sendable { /// - operationID: The identifier of the OpenAPI operation. /// - Returns: An HTTP response and its body. /// - Throws: An error if sending the request and receiving the response fails. - func send( - _ request: HTTPRequest, - body: HTTPBody?, - baseURL: URL, - operationID: String - ) async throws -> (HTTPResponse, HTTPBody?) + func send(_ request: HTTPRequest, body: HTTPBody?, baseURL: URL, operationID: String) async throws -> ( + HTTPResponse, HTTPBody? + ) } /// A type that intercepts HTTP requests and responses. diff --git a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift index 477c5b93..7093bd75 100644 --- a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift +++ b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift @@ -24,11 +24,7 @@ public struct ServerRequestMetadata: Hashable, Sendable { /// Creates a new metadata wrapper with the specified path and query parameters. /// - Parameter pathParameters: Path parameters parsed from the URL of the HTTP /// request. - public init( - pathParameters: [String: Substring] = [:] - ) { - self.pathParameters = pathParameters - } + public init(pathParameters: [String: Substring] = [:]) { self.pathParameters = pathParameters } } extension HTTPRequest { @@ -38,31 +34,22 @@ 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()) { + @_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 - } - guard let queryStart = path.firstIndex(of: "?") else { - return nil - } + @_spi(Generated) public var soar_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)..") [\(headerFields.prettyDescription)]" - } + var prettyDescription: String { "\(method.rawValue) \(path ?? "") [\(headerFields.prettyDescription)]" } } extension HTTPResponse: PrettyStringConvertible { - var prettyDescription: String { - "\(status.code) [\(headerFields.prettyDescription)]" - } + var prettyDescription: String { "\(status.code) [\(headerFields.prettyDescription)]" } } -extension HTTPBody: PrettyStringConvertible { - var prettyDescription: String { - String(describing: self) - } -} +extension HTTPBody: PrettyStringConvertible { var prettyDescription: String { String(describing: self) } } diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index cb7e53f1..b97906ba 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -171,9 +171,7 @@ public final class HTTPBody: @unchecked Sendable { /// used for testing. internal var testing_iteratorCreated: Bool { lock.lock() - defer { - lock.unlock() - } + defer { lock.unlock() } return locked_iteratorCreated } @@ -182,15 +180,9 @@ public final class HTTPBody: @unchecked Sendable { /// - 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() - } + 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 @@ -202,12 +194,8 @@ public final class HTTPBody: @unchecked Sendable { locked_iteratorCreated = true lock.unlock() } - guard iterationBehavior == .single else { - return - } - if locked_iteratorCreated { - throw TooManyIterationsError() - } + guard iterationBehavior == .single else { return } + if locked_iteratorCreated { throw TooManyIterationsError() } } /// Creates a new body. @@ -217,11 +205,7 @@ 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: BodySequence, length: Length, iterationBehavior: IterationBehavior) { self.sequence = sequence self.length = length self.iterationBehavior = iterationBehavior @@ -255,21 +239,14 @@ extension HTTPBody: Equatable { /// /// - Returns: `true` if the object identifiers of the two HTTPBody instances are equal, /// indicating that they are the same object in memory; otherwise, returns `false`. - public static func == ( - lhs: HTTPBody, - rhs: HTTPBody - ) -> Bool { - ObjectIdentifier(lhs) == ObjectIdentifier(rhs) - } + public static func == (lhs: HTTPBody, rhs: HTTPBody) -> Bool { ObjectIdentifier(lhs) == ObjectIdentifier(rhs) } } extension HTTPBody: Hashable { /// Hashes the HTTPBody 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)) - } + public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } } // MARK: - Creating the HTTPBody @@ -278,29 +255,20 @@ extension HTTPBody { /// Creates a new empty body. @inlinable public convenience init() { - self.init( - .init(EmptySequence()), - length: .known(0), - iterationBehavior: .multiple - ) + self.init(.init(EmptySequence()), length: .known(0), iterationBehavior: .multiple) } /// 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, - length: Length - ) { + @inlinable public convenience init(_ bytes: ByteChunk, length: Length) { self.init([bytes], length: length, iterationBehavior: .multiple) } /// Creates a new body with the provided byte chunk. /// - Parameter bytes: A byte chunk. - @inlinable public convenience init( - _ bytes: ByteChunk - ) { + @inlinable public convenience init(_ bytes: ByteChunk) { self.init([bytes], length: .known(bytes.count), iterationBehavior: .multiple) } @@ -314,34 +282,19 @@ extension HTTPBody { _ bytes: some Sequence & Sendable, length: Length, iterationBehavior: IterationBehavior - ) { - self.init( - [ArraySlice(bytes)], - length: length, - iterationBehavior: iterationBehavior - ) - } + ) { 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: some Collection & Sendable, - length: Length - ) { - self.init( - ArraySlice(bytes), - length: length, - iterationBehavior: .multiple - ) + @inlinable public convenience init(_ bytes: some Collection & Sendable, length: Length) { + self.init(ArraySlice(bytes), length: length, iterationBehavior: .multiple) } /// Creates a new body with the provided byte collection. /// - Parameter bytes: A byte chunk. - @inlinable public convenience init( - _ bytes: some Collection & Sendable - ) { + @inlinable public convenience init(_ bytes: some Collection & Sendable) { self.init(bytes, length: .known(bytes.count)) } @@ -349,30 +302,16 @@ extension HTTPBody { /// - 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( - .init(stream), - length: length, - iterationBehavior: .single - ) + @inlinable public convenience init(_ stream: AsyncThrowingStream, length: HTTPBody.Length) { + self.init(.init(stream), length: length, iterationBehavior: .single) } /// 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( - .init(stream), - length: length, - iterationBehavior: .single - ) + @inlinable public convenience init(_ stream: AsyncStream, length: HTTPBody.Length) { + self.init(.init(stream), length: length, iterationBehavior: .single) } /// Creates a new body with the provided async sequence. @@ -386,11 +325,7 @@ extension HTTPBody { length: HTTPBody.Length, iterationBehavior: IterationBehavior ) where Bytes.Element == ByteChunk, Bytes: Sendable { - self.init( - .init(sequence), - length: length, - iterationBehavior: iterationBehavior - ) + self.init(.init(sequence), length: length, iterationBehavior: iterationBehavior) } /// Creates a new body with the provided async sequence of byte sequences. @@ -404,11 +339,7 @@ extension HTTPBody { length: HTTPBody.Length, iterationBehavior: IterationBehavior ) where Bytes: Sendable, Bytes.Element: Sequence & Sendable, Bytes.Element.Element == UInt8 { - self.init( - sequence.map { ArraySlice($0) }, - length: length, - iterationBehavior: iterationBehavior - ) + self.init(sequence.map { ArraySlice($0) }, length: length, iterationBehavior: iterationBehavior) } } @@ -438,13 +369,9 @@ extension HTTPBody { /// The maximum number of bytes acceptable by the user. let maxBytes: Int - var description: String { - "OpenAPIRuntime.HTTPBody contains more than the maximum allowed \(maxBytes) bytes." - } + var description: String { "OpenAPIRuntime.HTTPBody contains more than the maximum allowed \(maxBytes) bytes." } - var errorDescription: String? { - description - } + var errorDescription: String? { description } } /// An error thrown by the collecting initializer when another iteration of @@ -455,9 +382,7 @@ extension HTTPBody { "OpenAPIRuntime.HTTPBody attempted to create a second iterator, but the underlying sequence is only safe to be iterated once." } - var errorDescription: String? { - description - } + var errorDescription: String? { description } } /// Accumulates the full body in-memory into a single buffer @@ -474,17 +399,13 @@ extension HTTPBody { // 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) - } + 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 { - throw TooManyBytesError(maxBytes: maxBytes) - } + guard buffer.count + chunk.count <= maxBytes else { throw TooManyBytesError(maxBytes: maxBytes) } buffer.append(contentsOf: chunk) } return buffer @@ -527,23 +448,13 @@ extension HTTPBody { /// - Parameters: /// - string: A string to encode as bytes. /// - length: The total length of the body. - @inlinable public convenience init( - _ string: some StringProtocol & Sendable, - length: Length - ) { - self.init( - ByteChunk(string), - length: length - ) + @inlinable public convenience init(_ string: some StringProtocol & Sendable, length: Length) { + self.init(ByteChunk(string), length: length) } /// Creates a new body with the provided string encoded as UTF-8 bytes. /// - Parameter string: A string to encode as bytes. - @inlinable public convenience init( - _ string: some StringProtocol & Sendable - ) { - self.init(ByteChunk(string)) - } + @inlinable public convenience init(_ string: some StringProtocol & Sendable) { self.init(ByteChunk(string)) } /// Creates a new body with the provided async throwing stream of strings. /// - Parameters: @@ -552,27 +463,14 @@ extension HTTPBody { @inlinable public convenience init( _ stream: AsyncThrowingStream, length: HTTPBody.Length - ) { - self.init( - .init(stream.map { ByteChunk.init($0) }), - length: length, - iterationBehavior: .single - ) - } + ) { self.init(.init(stream.map { ByteChunk.init($0) }), length: length, iterationBehavior: .single) } /// 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 - ) { - self.init( - .init(stream.map { ByteChunk.init($0) }), - length: length, - iterationBehavior: .single - ) + @inlinable public convenience init(_ stream: AsyncStream, length: HTTPBody.Length) { + self.init(.init(stream.map { ByteChunk.init($0) }), length: length, iterationBehavior: .single) } /// Creates a new body with the provided async sequence of string chunks. @@ -586,11 +484,7 @@ extension HTTPBody { length: HTTPBody.Length, iterationBehavior: IterationBehavior ) where Strings.Element: StringProtocol & Sendable, Strings: Sendable { - self.init( - .init(sequence.map { ByteChunk.init($0) }), - length: length, - iterationBehavior: iterationBehavior - ) + self.init(.init(sequence.map { ByteChunk.init($0) }), length: length, iterationBehavior: iterationBehavior) } } @@ -598,9 +492,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: some StringProtocol & Sendable) { - self = Array(string.utf8)[...] - } + @inlinable init(_ string: some StringProtocol & Sendable) { self = Array(string.utf8)[...] } } extension String { @@ -613,10 +505,7 @@ extension String { /// - Throws: `TooManyBytesError` if the body contains more /// than `maxBytes`. public init(collecting body: HTTPBody, upTo maxBytes: Int) async throws { - self = try await String( - decoding: body.collect(upTo: maxBytes), - as: UTF8.self - ) + self = try await String(decoding: body.collect(upTo: maxBytes), as: UTF8.self) } } @@ -626,18 +515,14 @@ extension HTTPBody: ExpressibleByStringLiteral { /// Initializes an `HTTPBody` instance with the provided string value. /// /// - Parameter value: The string literal to use for initializing the `HTTPBody`. - public convenience init(stringLiteral value: String) { - self.init(value) - } + public convenience init(stringLiteral value: String) { self.init(value) } } 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[...]) - } + @inlinable public convenience init(_ bytes: [UInt8]) { self.init(bytes[...]) } } extension HTTPBody: ExpressibleByArrayLiteral { @@ -646,18 +531,14 @@ extension HTTPBody: ExpressibleByArrayLiteral { /// Initializes an `HTTPBody` instance with a sequence of `UInt8` elements. /// /// - Parameter elements: A variadic list of `UInt8` elements used to initialize the `HTTPBody`. - public convenience init(arrayLiteral elements: UInt8...) { - self.init(elements) - } + public convenience init(arrayLiteral elements: UInt8...) { self.init(elements) } } extension HTTPBody { /// Creates a new body from the provided data chunk. /// - Parameter data: A single data chunk. - public convenience init(_ data: Data) { - self.init(ArraySlice(data)) - } + public convenience init(_ data: Data) { self.init(ArraySlice(data)) } } extension Data { @@ -689,22 +570,17 @@ extension HTTPBody { /// 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 { + @usableFromInline init(_ iterator: Iterator) + where Iterator.Element == Element { var iterator = iterator - self.produceNext = { - try await iterator.next() - } + 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() - } + public mutating func next() async throws -> Element? { try await produceNext() } } } @@ -725,14 +601,10 @@ extension HTTPBody { /// 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()) - } + self.produceIterator = { .init(sequence.makeAsyncIterator()) } } - @usableFromInline func makeAsyncIterator() -> AsyncIterator { - produceIterator() - } + @usableFromInline func makeAsyncIterator() -> AsyncIterator { produceIterator() } } /// An async sequence wrapper for a sync sequence. @@ -754,9 +626,7 @@ extension HTTPBody { /// The underlying sync sequence iterator. var iterator: any IteratorProtocol - @usableFromInline mutating func next() async throws -> HTTPBody.ByteChunk? { - iterator.next() - } + @usableFromInline mutating func next() async throws -> HTTPBody.ByteChunk? { iterator.next() } } /// The underlying sync sequence. @@ -764,13 +634,9 @@ extension HTTPBody { /// Creates a new async sequence with the provided sync sequence. /// - Parameter sequence: The sync sequence to wrap. - @inlinable init(sequence: Bytes) { - self.sequence = sequence - } + @inlinable init(sequence: Bytes) { self.sequence = sequence } - @usableFromInline func makeAsyncIterator() -> Iterator { - Iterator(iterator: sequence.makeIterator()) - } + @usableFromInline func makeAsyncIterator() -> Iterator { Iterator(iterator: sequence.makeIterator()) } } /// An empty async sequence. @@ -788,16 +654,12 @@ extension HTTPBody { /// The byte chunk element type. @usableFromInline typealias Element = ByteChunk - @usableFromInline mutating func next() async throws -> HTTPBody.ByteChunk? { - nil - } + @usableFromInline mutating func next() async throws -> HTTPBody.ByteChunk? { nil } } /// Creates a new empty async sequence. @inlinable init() {} - @usableFromInline func makeAsyncIterator() -> EmptyIterator { - EmptyIterator() - } + @usableFromInline func makeAsyncIterator() -> EmptyIterator { EmptyIterator() } } } diff --git a/Sources/OpenAPIRuntime/Interface/UniversalClient.swift b/Sources/OpenAPIRuntime/Interface/UniversalClient.swift index 6c75a3ce..431fb8af 100644 --- a/Sources/OpenAPIRuntime/Interface/UniversalClient.swift +++ b/Sources/OpenAPIRuntime/Interface/UniversalClient.swift @@ -90,15 +90,10 @@ import Foundation serializer: @Sendable (OperationInput) throws -> (HTTPRequest, HTTPBody?), deserializer: @Sendable (HTTPResponse, HTTPBody?) async throws -> OperationOutput ) async throws -> OperationOutput where OperationInput: Sendable, OperationOutput: Sendable { - @Sendable func wrappingErrors( - work: () async throws -> R, - mapError: (any Error) -> any Error - ) async throws -> R { - do { - return try await work() - } catch let error as ClientError { - throw error - } catch { + @Sendable func wrappingErrors(work: () async throws -> R, mapError: (any Error) -> any Error) async throws + -> R + { + do { return try await work() } catch let error as ClientError { throw error } catch { throw mapError(error) } } @@ -148,12 +143,7 @@ import Foundation 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 - ) + try await transport.send(_request, body: _body, baseURL: _url, operationID: operationID) } mapError: { error in makeError( request: request, @@ -165,8 +155,7 @@ import Foundation } for middleware in middlewares.reversed() { let tmp = next - next = { - (_request, _body, _url) in + next = { (_request, _body, _url) in try await wrappingErrors { try await middleware.intercept( _request, @@ -180,10 +169,7 @@ import Foundation request: request, requestBody: requestBody, baseURL: baseURL, - error: RuntimeError.middlewareFailed( - middlewareType: type(of: middleware), - error - ) + error: RuntimeError.middlewareFailed(middlewareType: type(of: middleware), error) ) } } diff --git a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift index e523560f..80d69e25 100644 --- a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift +++ b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift @@ -43,12 +43,7 @@ import struct Foundation.URLComponents public var middlewares: [any ServerMiddleware] /// Internal initializer that takes an initialized converter. - internal init( - serverURL: URL, - converter: Converter, - handler: APIHandler, - middlewares: [any ServerMiddleware] - ) { + internal init(serverURL: URL, converter: Converter, handler: APIHandler, middlewares: [any ServerMiddleware]) { self.serverURL = serverURL self.converter = converter self.handler = handler @@ -102,23 +97,16 @@ import struct Foundation.URLComponents 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, - mapError: (any Error) -> any Error - ) async throws -> R { - do { - return try await work() - } catch let error as ServerError { - throw error - } catch { + @Sendable func wrappingErrors(work: () async throws -> R, mapError: (any Error) -> any Error) async throws + -> R + { + do { return try await work() } catch let error as ServerError { throw error } catch { throw mapError(error) } } - @Sendable func makeError( - input: OperationInput? = nil, - output: OperationOutput? = nil, - error: any Error - ) -> any Error { + @Sendable func makeError(input: OperationInput? = nil, output: OperationOutput? = nil, error: any Error) + -> any Error + { if var error = error as? ServerError { error.operationInput = error.operationInput ?? input error.operationOutput = error.operationOutput ?? output @@ -145,10 +133,7 @@ import struct Foundation.URLComponents ) } var next: @Sendable (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody?) = - { - _request, - _requestBody, - _metadata in + { _request, _requestBody, _metadata in let input: OperationInput = try await wrappingErrors { try await deserializer(_request, _requestBody, _metadata) } mapError: { error in @@ -159,10 +144,7 @@ import struct Foundation.URLComponents return try await wrappingErrors { try await method(input) } mapError: { error in - makeError( - input: input, - error: RuntimeError.handlerFailed(error) - ) + makeError(input: input, error: RuntimeError.handlerFailed(error)) } } mapError: { error in makeError(input: input, error: error) @@ -175,10 +157,7 @@ import struct Foundation.URLComponents } for middleware in middlewares.reversed() { let tmp = next - next = { - _request, - _requestBody, - _metadata in + next = { _request, _requestBody, _metadata in try await wrappingErrors { try await middleware.intercept( _request, @@ -188,12 +167,7 @@ import struct Foundation.URLComponents next: tmp ) } mapError: { error in - makeError( - error: RuntimeError.middlewareFailed( - middlewareType: type(of: middleware), - error - ) - ) + makeError(error: RuntimeError.middlewareFailed(middlewareType: type(of: middleware), error)) } } } @@ -204,9 +178,7 @@ import struct Foundation.URLComponents /// - Parameter path: The path suffix. /// - Returns: The path appended to the server URL's path. /// - Throws: An error if resolving the server URL components fails or if the server URL is invalid. - public func apiPathComponentsWithServerPrefix( - _ path: String - ) throws -> String { + public func apiPathComponentsWithServerPrefix(_ 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". @@ -214,9 +186,7 @@ import struct Foundation.URLComponents throw RuntimeError.invalidServerURL(serverURL.absoluteString) } let prefixPath = components.path - guard prefixPath == "/" else { - return prefixPath + path - } + guard prefixPath == "/" else { return prefixPath + path } return path } } diff --git a/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift b/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift index 4dc882a4..985b7715 100644 --- a/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift +++ b/Sources/OpenAPIRuntime/URICoder/Common/URIEncodedNode.swift @@ -77,12 +77,9 @@ extension URIEncodedNode { /// - Throws: If the node is already set. mutating func set(_ value: Primitive) throws { switch self { - case .unset: - self = .primitive(value) - case .primitive: - throw InsertionError.settingPrimitiveValueAgain - case .array, .dictionary: - throw InsertionError.settingValueOnAContainer + case .unset: self = .primitive(value) + case .primitive: throw InsertionError.settingPrimitiveValueAgain + case .array, .dictionary: throw InsertionError.settingValueOnAContainer } } @@ -93,10 +90,7 @@ extension URIEncodedNode { /// - key: The key to save the value for into the dictionary. /// - Throws: If the node is already set to be anything else but a /// dictionary. - mutating func insert( - _ childValue: Self, - atKey key: Key - ) throws { + mutating func insert(_ childValue: Self, atKey key: Key) throws { switch self { case .dictionary(var dictionary): self = .unset @@ -109,25 +103,18 @@ extension URIEncodedNode { guard let intValue = key.intValue else { throw InsertionError.insertingChildValueIntoArrayUsingNonIntValueKey } - precondition( - intValue == array.count, - "Unkeyed container inserting at an incorrect index" - ) + precondition(intValue == array.count, "Unkeyed container inserting at an incorrect index") self = .unset array.append(childValue) self = .array(array) case .unset: if let intValue = key.intValue { - precondition( - intValue == 0, - "Unkeyed container inserting at an incorrect index" - ) + precondition(intValue == 0, "Unkeyed container inserting at an incorrect index") self = .array([childValue]) } else { self = .dictionary([key.stringValue: childValue]) } - default: - throw InsertionError.insertingChildValueIntoNonContainer + default: throw InsertionError.insertingChildValueIntoNonContainer } } @@ -140,10 +127,8 @@ extension URIEncodedNode { self = .unset items.append(childValue) self = .array(items) - case .unset: - self = .array([childValue]) - default: - throw InsertionError.appendingToNonArrayContainer + case .unset: self = .array([childValue]) + default: throw InsertionError.appendingToNonArrayContainer } } } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift index 138d60cc..72cc077f 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift @@ -53,9 +53,7 @@ struct URIDecoder: Sendable { /// Creates a new decoder with the provided configuration. /// - Parameter configuration: The configuration used by the decoder. - init(configuration: URICoderConfiguration) { - self.configuration = configuration - } + init(configuration: URICoderConfiguration) { self.configuration = configuration } } extension URIDecoder { @@ -73,14 +71,8 @@ extension URIDecoder { /// - data: The URI-encoded string. /// - Returns: The decoded value. /// - Throws: An error if decoding fails, for example, due to incompatible data or key. - func decode( - _ type: T.Type = T.self, - forKey key: String = "", - from data: Substring - ) throws -> T { - try withCachedParser(from: data) { decoder in - try decoder.decode(type, forKey: key) - } + func decode(_ type: T.Type = T.self, forKey key: String = "", from data: Substring) throws -> T { + try withCachedParser(from: data) { decoder in try decoder.decode(type, forKey: key) } } /// Attempt to decode an object from an URI string, if present. @@ -96,15 +88,9 @@ extension URIDecoder { /// - data: The URI-encoded string. /// - Returns: The decoded value. /// - Throws: An error if decoding fails, for example, due to incompatible data or key. - func decodeIfPresent( - _ type: T.Type = T.self, - forKey key: String = "", - from data: Substring - ) throws -> T? { - try withCachedParser(from: data) { decoder in - try decoder.decodeIfPresent(type, forKey: key) - } - } + func decodeIfPresent(_ type: T.Type = T.self, forKey key: String = "", from data: Substring) throws + -> T? + { try withCachedParser(from: data) { decoder in try decoder.decodeIfPresent(type, forKey: key) } } /// Make multiple decode calls on the parsed URI. /// @@ -115,10 +101,7 @@ extension URIDecoder { /// the `decode` method on `URICachedDecoder`. /// - Returns: The result of the closure invocation. /// - Throws: An error if parsing or decoding fails. - func withCachedParser( - from data: Substring, - calls: (URICachedDecoder) throws -> R - ) throws -> R { + func withCachedParser(from data: Substring, calls: (URICachedDecoder) throws -> R) throws -> R { var parser = URIParser(configuration: configuration, data: data) let parsedNode = try parser.parseRoot() let decoder = URICachedDecoder(configuration: configuration, node: parsedNode) @@ -146,10 +129,7 @@ struct URICachedDecoder { /// and explode options, ignored otherwise. /// - Returns: The decoded value. /// - Throws: An error if decoding fails. - func decode( - _ type: T.Type = T.self, - forKey key: String = "" - ) throws -> T { + func decode(_ type: T.Type = T.self, forKey key: String = "") throws -> T { let decoder = URIValueFromNodeDecoder( node: node, rootKey: key[...], @@ -172,10 +152,7 @@ struct URICachedDecoder { /// and explode options, ignored otherwise. /// - Returns: The decoded value. /// - Throws: An error if decoding fails. - func decodeIfPresent( - _ type: T.Type = T.self, - forKey key: String = "" - ) throws -> T? { + func decodeIfPresent(_ type: T.Type = T.self, forKey key: String = "") throws -> T? { let decoder = URIValueFromNodeDecoder( node: node, rootKey: key[...], diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift index 6590be92..fd47d462 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Keyed.swift @@ -33,10 +33,7 @@ extension URIKeyedDecodingContainer { /// - Throws: An error if no value for the key was found. private func _decodeValue(forKey key: Key) throws -> URIParsedValue { guard let value = values[key.stringValue[...]]?.first else { - throw DecodingError.keyNotFound( - key, - .init(codingPath: codingPath, debugDescription: "Key not found.") - ) + throw DecodingError.keyNotFound(key, .init(codingPath: codingPath, debugDescription: "Key not found.")) } return value } @@ -49,17 +46,11 @@ extension URIKeyedDecodingContainer { /// - Returns: The converted value found for the provided key. /// - Throws: An error if no value for the key was found or if the /// conversion failed. - private func _decodeBinaryFloatingPoint( - _: T.Type = T.self, - forKey key: Key - ) throws -> T { + private func _decodeBinaryFloatingPoint(_: T.Type = T.self, forKey key: Key) throws -> T { guard let double = Double(try _decodeValue(forKey: key)) else { throw DecodingError.typeMismatch( T.self, - .init( - codingPath: codingPath, - debugDescription: "Failed to convert to Double." - ) + .init(codingPath: codingPath, debugDescription: "Failed to convert to Double.") ) } return T(double) @@ -73,17 +64,11 @@ extension URIKeyedDecodingContainer { /// - Returns: The converted value found for the provided key. /// - Throws: An error if no value for the key was found or if the /// conversion failed. - private func _decodeFixedWidthInteger( - _: T.Type = T.self, - forKey key: Key - ) throws -> T { + private func _decodeFixedWidthInteger(_: T.Type = T.self, forKey key: Key) throws -> T { guard let parsedValue = T(try _decodeValue(forKey: key)) else { throw DecodingError.typeMismatch( T.self, - .init( - codingPath: codingPath, - debugDescription: "Failed to convert to the requested type." - ) + .init(codingPath: codingPath, debugDescription: "Failed to convert to the requested type.") ) } return parsedValue @@ -97,17 +82,13 @@ extension URIKeyedDecodingContainer { /// - Returns: The converted value found for the provided key. /// - Throws: An error if no value for the key was found or if the /// conversion failed. - private func _decodeNextLosslessStringConvertible( - _: T.Type = T.self, - forKey key: Key - ) throws -> T { + private func _decodeNextLosslessStringConvertible(_: T.Type = T.self, forKey key: Key) + throws -> T + { guard let parsedValue = T(String(try _decodeValue(forKey: key))) else { throw DecodingError.typeMismatch( T.self, - .init( - codingPath: codingPath, - debugDescription: "Failed to convert to the requested type." - ) + .init(codingPath: codingPath, debugDescription: "Failed to convert to the requested type.") ) } return parsedValue @@ -116,141 +97,77 @@ extension URIKeyedDecodingContainer { extension URIKeyedDecodingContainer: KeyedDecodingContainerProtocol { - var allKeys: [Key] { - values.keys.map { key in - Key.init(stringValue: String(key))! - } - } + var allKeys: [Key] { values.keys.map { key in Key.init(stringValue: String(key))! } } - func contains(_ key: Key) -> Bool { - values[key.stringValue[...]] != nil - } + func contains(_ key: Key) -> Bool { values[key.stringValue[...]] != nil } - var codingPath: [any CodingKey] { - decoder.codingPath - } + var codingPath: [any CodingKey] { decoder.codingPath } - func decodeNil(forKey key: Key) -> Bool { - false - } + func decodeNil(forKey key: Key) -> Bool { false } func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { try _decodeNextLosslessStringConvertible(forKey: key) } - func decode(_ type: String.Type, forKey key: Key) throws -> String { - String(try _decodeValue(forKey: key)) - } + func decode(_ type: String.Type, forKey key: Key) throws -> String { String(try _decodeValue(forKey: key)) } - func decode(_ type: Double.Type, forKey key: Key) throws -> Double { - try _decodeBinaryFloatingPoint(forKey: key) - } + func decode(_ type: Double.Type, forKey key: Key) throws -> Double { try _decodeBinaryFloatingPoint(forKey: key) } - func decode(_ type: Float.Type, forKey key: Key) throws -> Float { - try _decodeBinaryFloatingPoint(forKey: key) - } + func decode(_ type: Float.Type, forKey key: Key) throws -> Float { try _decodeBinaryFloatingPoint(forKey: key) } - func decode(_ type: Int.Type, forKey key: Key) throws -> Int { - try _decodeFixedWidthInteger(forKey: key) - } + func decode(_ type: Int.Type, forKey key: Key) throws -> Int { try _decodeFixedWidthInteger(forKey: key) } - func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { - try _decodeFixedWidthInteger(forKey: key) - } + func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { try _decodeFixedWidthInteger(forKey: key) } - func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { - try _decodeFixedWidthInteger(forKey: key) - } + func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { try _decodeFixedWidthInteger(forKey: key) } - func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { - try _decodeFixedWidthInteger(forKey: key) - } + func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { try _decodeFixedWidthInteger(forKey: key) } - func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { - try _decodeFixedWidthInteger(forKey: key) - } + func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { try _decodeFixedWidthInteger(forKey: key) } - func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { - try _decodeFixedWidthInteger(forKey: key) - } + func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { try _decodeFixedWidthInteger(forKey: key) } - func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { - try _decodeFixedWidthInteger(forKey: key) - } + func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { try _decodeFixedWidthInteger(forKey: key) } - func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { - try _decodeFixedWidthInteger(forKey: key) - } + func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { try _decodeFixedWidthInteger(forKey: key) } - func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { - try _decodeFixedWidthInteger(forKey: key) - } + func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { try _decodeFixedWidthInteger(forKey: key) } - func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { - try _decodeFixedWidthInteger(forKey: key) - } + func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { try _decodeFixedWidthInteger(forKey: key) } func decode(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable { switch type { - case is Bool.Type: - return try decode(Bool.self, forKey: key) as! T - case is String.Type: - return try decode(String.self, forKey: key) as! T - case is Double.Type: - return try decode(Double.self, forKey: key) as! T - case is Float.Type: - return try decode(Float.self, forKey: key) as! T - case is Int.Type: - return try decode(Int.self, forKey: key) as! T - case is Int8.Type: - return try decode(Int8.self, forKey: key) as! T - case is Int16.Type: - return try decode(Int16.self, forKey: key) as! T - case is Int32.Type: - return try decode(Int32.self, forKey: key) as! T - case is Int64.Type: - return try decode(Int64.self, forKey: key) as! T - case is UInt.Type: - return try decode(UInt.self, forKey: key) as! T - case is UInt8.Type: - return try decode(UInt8.self, forKey: key) as! T - case is UInt16.Type: - return try decode(UInt16.self, forKey: key) as! T - case is UInt32.Type: - return try decode(UInt32.self, forKey: key) as! T - case is UInt64.Type: - return try decode(UInt64.self, forKey: key) as! T - case is Date.Type: - return try decoder - .dateTranscoder - .decode(String(_decodeValue(forKey: key))) as! T + case is Bool.Type: return try decode(Bool.self, forKey: key) as! T + case is String.Type: return try decode(String.self, forKey: key) as! T + case is Double.Type: return try decode(Double.self, forKey: key) as! T + case is Float.Type: return try decode(Float.self, forKey: key) as! T + case is Int.Type: return try decode(Int.self, forKey: key) as! T + case is Int8.Type: return try decode(Int8.self, forKey: key) as! T + case is Int16.Type: return try decode(Int16.self, forKey: key) as! T + case is Int32.Type: return try decode(Int32.self, forKey: key) as! T + case is Int64.Type: return try decode(Int64.self, forKey: key) as! T + case is UInt.Type: return try decode(UInt.self, forKey: key) as! T + case is UInt8.Type: return try decode(UInt8.self, forKey: key) as! T + case is UInt16.Type: return try decode(UInt16.self, forKey: key) as! T + case is UInt32.Type: return try decode(UInt32.self, forKey: key) as! T + case is UInt64.Type: return try decode(UInt64.self, forKey: key) as! T + case is Date.Type: return try decoder.dateTranscoder.decode(String(_decodeValue(forKey: key))) as! T default: try decoder.push(.init(key)) - defer { - decoder.pop() - } + defer { decoder.pop() } return try type.init(from: decoder) } } - func nestedContainer( - keyedBy type: NestedKey.Type, - forKey key: Key - ) throws -> KeyedDecodingContainer where NestedKey: CodingKey { - throw URIValueFromNodeDecoder.GeneralError.nestedContainersNotSupported - } + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer< + NestedKey + > where NestedKey: CodingKey { throw URIValueFromNodeDecoder.GeneralError.nestedContainersNotSupported } - func nestedUnkeyedContainer( - forKey key: Key - ) throws -> any UnkeyedDecodingContainer { + func nestedUnkeyedContainer(forKey key: Key) throws -> any UnkeyedDecodingContainer { throw URIValueFromNodeDecoder.GeneralError.nestedContainersNotSupported } - func superDecoder(forKey key: Key) throws -> any Decoder { - decoder - } + func superDecoder(forKey key: Key) throws -> any Decoder { decoder } - func superDecoder() throws -> any Decoder { - decoder - } + func superDecoder() throws -> any Decoder { decoder } } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift index 32592fd7..3c829873 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift @@ -24,11 +24,7 @@ struct URISingleValueDecodingContainer { extension URISingleValueDecodingContainer { /// The underlying value as a single value. - var value: URIParsedValue { - get throws { - try decoder.currentElementAsSingleValue() - } - } + var value: URIParsedValue { get throws { try decoder.currentElementAsSingleValue() } } /// Returns the value found in the underlying node converted to /// the provided type. @@ -36,16 +32,11 @@ extension URISingleValueDecodingContainer { /// - Parameter _: The `BinaryFloatingPoint` type to convert the value to. /// - Returns: The converted value found. /// - Throws: An error if the conversion failed. - private func _decodeBinaryFloatingPoint( - _: T.Type = T.self - ) throws -> T { + private func _decodeBinaryFloatingPoint(_: T.Type = T.self) throws -> T { guard let double = try Double(value) else { throw DecodingError.typeMismatch( T.self, - .init( - codingPath: codingPath, - debugDescription: "Failed to convert to Double." - ) + .init(codingPath: codingPath, debugDescription: "Failed to convert to Double.") ) } return T(double) @@ -57,16 +48,11 @@ extension URISingleValueDecodingContainer { /// - Parameter _: The `FixedWidthInteger` type to convert the value to. /// - Returns: The converted value found. /// - Throws: An error if the conversion failed. - private func _decodeFixedWidthInteger( - _: T.Type = T.self - ) throws -> T { + private func _decodeFixedWidthInteger(_: T.Type = T.self) throws -> T { guard let parsedValue = try T(value) else { throw DecodingError.typeMismatch( T.self, - .init( - codingPath: codingPath, - debugDescription: "Failed to convert to the requested type." - ) + .init(codingPath: codingPath, debugDescription: "Failed to convert to the requested type.") ) } return parsedValue @@ -78,16 +64,11 @@ extension URISingleValueDecodingContainer { /// - Parameter _: The `LosslessStringConvertible` type to convert the value to. /// - Returns: The converted value found. /// - Throws: An error if the conversion failed. - private func _decodeLosslessStringConvertible( - _: T.Type = T.self - ) throws -> T { + private func _decodeLosslessStringConvertible(_: T.Type = T.self) throws -> T { guard let parsedValue = try T(String(value)) else { throw DecodingError.typeMismatch( T.self, - .init( - codingPath: codingPath, - debugDescription: "Failed to convert to the requested type." - ) + .init(codingPath: codingPath, debugDescription: "Failed to convert to the requested type.") ) } return parsedValue @@ -96,104 +77,56 @@ extension URISingleValueDecodingContainer { extension URISingleValueDecodingContainer: SingleValueDecodingContainer { - var codingPath: [any CodingKey] { - decoder.codingPath - } + var codingPath: [any CodingKey] { decoder.codingPath } - func decodeNil() -> Bool { - false - } + func decodeNil() -> Bool { false } - func decode(_ type: Bool.Type) throws -> Bool { - try _decodeLosslessStringConvertible() - } + func decode(_ type: Bool.Type) throws -> Bool { try _decodeLosslessStringConvertible() } - func decode(_ type: String.Type) throws -> String { - try String(value) - } + func decode(_ type: String.Type) throws -> String { try String(value) } - func decode(_ type: Double.Type) throws -> Double { - try _decodeBinaryFloatingPoint() - } + func decode(_ type: Double.Type) throws -> Double { try _decodeBinaryFloatingPoint() } - func decode(_ type: Float.Type) throws -> Float { - try _decodeBinaryFloatingPoint() - } + func decode(_ type: Float.Type) throws -> Float { try _decodeBinaryFloatingPoint() } - func decode(_ type: Int.Type) throws -> Int { - try _decodeFixedWidthInteger() - } + func decode(_ type: Int.Type) throws -> Int { try _decodeFixedWidthInteger() } - func decode(_ type: Int8.Type) throws -> Int8 { - try _decodeFixedWidthInteger() - } + func decode(_ type: Int8.Type) throws -> Int8 { try _decodeFixedWidthInteger() } - func decode(_ type: Int16.Type) throws -> Int16 { - try _decodeFixedWidthInteger() - } + func decode(_ type: Int16.Type) throws -> Int16 { try _decodeFixedWidthInteger() } - func decode(_ type: Int32.Type) throws -> Int32 { - try _decodeFixedWidthInteger() - } + func decode(_ type: Int32.Type) throws -> Int32 { try _decodeFixedWidthInteger() } - func decode(_ type: Int64.Type) throws -> Int64 { - try _decodeFixedWidthInteger() - } + func decode(_ type: Int64.Type) throws -> Int64 { try _decodeFixedWidthInteger() } - func decode(_ type: UInt.Type) throws -> UInt { - try _decodeFixedWidthInteger() - } + func decode(_ type: UInt.Type) throws -> UInt { try _decodeFixedWidthInteger() } - func decode(_ type: UInt8.Type) throws -> UInt8 { - try _decodeFixedWidthInteger() - } + func decode(_ type: UInt8.Type) throws -> UInt8 { try _decodeFixedWidthInteger() } - func decode(_ type: UInt16.Type) throws -> UInt16 { - try _decodeFixedWidthInteger() - } + func decode(_ type: UInt16.Type) throws -> UInt16 { try _decodeFixedWidthInteger() } - func decode(_ type: UInt32.Type) throws -> UInt32 { - try _decodeFixedWidthInteger() - } + func decode(_ type: UInt32.Type) throws -> UInt32 { try _decodeFixedWidthInteger() } - func decode(_ type: UInt64.Type) throws -> UInt64 { - try _decodeFixedWidthInteger() - } + func decode(_ type: UInt64.Type) throws -> UInt64 { try _decodeFixedWidthInteger() } 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(value)) as! T - default: - return try T.init(from: decoder) + 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(value)) as! T + default: return try T.init(from: decoder) } } } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift index c985145a..44a5cd28 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift @@ -45,12 +45,8 @@ extension URIUnkeyedDecodingContainer { /// - Returns: The result of the closure. /// - Throws: An error if the container ran out of items. private mutating func _decodingNext(in work: () throws -> R) throws -> R { - guard !isAtEnd else { - throw URIValueFromNodeDecoder.GeneralError.reachedEndOfUnkeyedContainer - } - defer { - values.formIndex(after: &index) - } + guard !isAtEnd else { throw URIValueFromNodeDecoder.GeneralError.reachedEndOfUnkeyedContainer } + defer { values.formIndex(after: &index) } return try work() } @@ -59,9 +55,7 @@ extension URIUnkeyedDecodingContainer { /// - Returns: The next value found. /// - Throws: An error if the container ran out of items. private mutating func _decodeNext() throws -> URIParsedValue { - try _decodingNext { [values, index] in - values[index] - } + try _decodingNext { [values, index] in values[index] } } /// Returns the next value converted to the provided type. @@ -70,16 +64,11 @@ extension URIUnkeyedDecodingContainer { /// - Returns: The converted value. /// - Throws: An error if the container ran out of items or if /// the conversion failed. - private mutating func _decodeNextBinaryFloatingPoint( - _: T.Type = T.self - ) throws -> T { + private mutating func _decodeNextBinaryFloatingPoint(_: T.Type = T.self) throws -> T { guard let double = Double(try _decodeNext()) else { throw DecodingError.typeMismatch( T.self, - .init( - codingPath: codingPath, - debugDescription: "Failed to convert to Double." - ) + .init(codingPath: codingPath, debugDescription: "Failed to convert to Double.") ) } return T(double) @@ -91,16 +80,11 @@ extension URIUnkeyedDecodingContainer { /// - Returns: The converted value. /// - Throws: An error if the container ran out of items or if /// the conversion failed. - private mutating func _decodeNextFixedWidthInteger( - _: T.Type = T.self - ) throws -> T { + private mutating func _decodeNextFixedWidthInteger(_: T.Type = T.self) throws -> T { guard let parsedValue = T(try _decodeNext()) else { throw DecodingError.typeMismatch( T.self, - .init( - codingPath: codingPath, - debugDescription: "Failed to convert to the requested type." - ) + .init(codingPath: codingPath, debugDescription: "Failed to convert to the requested type.") ) } return parsedValue @@ -112,16 +96,13 @@ extension URIUnkeyedDecodingContainer { /// - Returns: The converted value. /// - Throws: An error if the container ran out of items or if /// the conversion failed. - private mutating func _decodeNextLosslessStringConvertible( - _: T.Type = T.self - ) throws -> T { + private mutating func _decodeNextLosslessStringConvertible(_: T.Type = T.self) throws + -> T + { guard let parsedValue = T(String(try _decodeNext())) else { throw DecodingError.typeMismatch( T.self, - .init( - codingPath: codingPath, - debugDescription: "Failed to convert to the requested type." - ) + .init(codingPath: codingPath, debugDescription: "Failed to convert to the requested type.") ) } return parsedValue @@ -130,138 +111,76 @@ extension URIUnkeyedDecodingContainer { extension URIUnkeyedDecodingContainer: UnkeyedDecodingContainer { - var count: Int? { - values.count - } + var count: Int? { values.count } - var isAtEnd: Bool { - index == values.endIndex - } + var isAtEnd: Bool { index == values.endIndex } - var currentIndex: Int { - index - } + var currentIndex: Int { index } - var codingPath: [any CodingKey] { - decoder.codingPath - } + var codingPath: [any CodingKey] { decoder.codingPath } - func decodeNil() -> Bool { - false - } + func decodeNil() -> Bool { false } - mutating func decode(_ type: Bool.Type) throws -> Bool { - try _decodeNextLosslessStringConvertible() - } + mutating func decode(_ type: Bool.Type) throws -> Bool { try _decodeNextLosslessStringConvertible() } - mutating func decode(_ type: String.Type) throws -> String { - String(try _decodeNext()) - } + mutating func decode(_ type: String.Type) throws -> String { String(try _decodeNext()) } - mutating func decode(_ type: Double.Type) throws -> Double { - try _decodeNextBinaryFloatingPoint() - } + mutating func decode(_ type: Double.Type) throws -> Double { try _decodeNextBinaryFloatingPoint() } - mutating func decode(_ type: Float.Type) throws -> Float { - try _decodeNextBinaryFloatingPoint() - } + mutating func decode(_ type: Float.Type) throws -> Float { try _decodeNextBinaryFloatingPoint() } - mutating func decode(_ type: Int.Type) throws -> Int { - try _decodeNextFixedWidthInteger() - } + mutating func decode(_ type: Int.Type) throws -> Int { try _decodeNextFixedWidthInteger() } - mutating func decode(_ type: Int8.Type) throws -> Int8 { - try _decodeNextFixedWidthInteger() - } + mutating func decode(_ type: Int8.Type) throws -> Int8 { try _decodeNextFixedWidthInteger() } - mutating func decode(_ type: Int16.Type) throws -> Int16 { - try _decodeNextFixedWidthInteger() - } + mutating func decode(_ type: Int16.Type) throws -> Int16 { try _decodeNextFixedWidthInteger() } - mutating func decode(_ type: Int32.Type) throws -> Int32 { - try _decodeNextFixedWidthInteger() - } + mutating func decode(_ type: Int32.Type) throws -> Int32 { try _decodeNextFixedWidthInteger() } - mutating func decode(_ type: Int64.Type) throws -> Int64 { - try _decodeNextFixedWidthInteger() - } + mutating func decode(_ type: Int64.Type) throws -> Int64 { try _decodeNextFixedWidthInteger() } - mutating func decode(_ type: UInt.Type) throws -> UInt { - try _decodeNextFixedWidthInteger() - } + mutating func decode(_ type: UInt.Type) throws -> UInt { try _decodeNextFixedWidthInteger() } - mutating func decode(_ type: UInt8.Type) throws -> UInt8 { - try _decodeNextFixedWidthInteger() - } + mutating func decode(_ type: UInt8.Type) throws -> UInt8 { try _decodeNextFixedWidthInteger() } - mutating func decode(_ type: UInt16.Type) throws -> UInt16 { - try _decodeNextFixedWidthInteger() - } + mutating func decode(_ type: UInt16.Type) throws -> UInt16 { try _decodeNextFixedWidthInteger() } - mutating func decode(_ type: UInt32.Type) throws -> UInt32 { - try _decodeNextFixedWidthInteger() - } + mutating func decode(_ type: UInt32.Type) throws -> UInt32 { try _decodeNextFixedWidthInteger() } - mutating func decode(_ type: UInt64.Type) throws -> UInt64 { - try _decodeNextFixedWidthInteger() - } + mutating func decode(_ type: UInt64.Type) throws -> UInt64 { try _decodeNextFixedWidthInteger() } mutating 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(_decodeNext())) as! T + 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(_decodeNext())) as! T default: return try _decodingNext { [decoder, currentIndex] in try decoder.push(.init(intValue: currentIndex)) - defer { - decoder.pop() - } + defer { decoder.pop() } return try type.init(from: decoder) } } } - mutating func nestedContainer( - keyedBy type: NestedKey.Type - ) throws -> KeyedDecodingContainer where NestedKey: CodingKey { - throw URIValueFromNodeDecoder.GeneralError.nestedContainersNotSupported - } + mutating func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer + where NestedKey: CodingKey { throw URIValueFromNodeDecoder.GeneralError.nestedContainersNotSupported } mutating func nestedUnkeyedContainer() throws -> any UnkeyedDecodingContainer { throw URIValueFromNodeDecoder.GeneralError.nestedContainersNotSupported } - mutating func superDecoder() throws -> any Decoder { - decoder - } + mutating func superDecoder() throws -> any Decoder { decoder } } diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift index a8b319f3..55982755 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift @@ -63,19 +63,15 @@ final class URIValueFromNodeDecoder { /// - Throws: When a decoding error occurs. func decodeRoot(_ type: T.Type = T.self) throws -> T { precondition(codingStack.isEmpty) - defer { - precondition(codingStack.isEmpty) - } + defer { precondition(codingStack.isEmpty) } // 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 singleValueContainer().decode(Date.self) as! T - default: - value = try T.init(from: self) + case is Date.Type: value = try singleValueContainer().decode(Date.self) as! T + default: value = try T.init(from: self) } return value } @@ -86,9 +82,7 @@ final class URIValueFromNodeDecoder { /// - Throws: When a decoding error occurs. func decodeRootIfPresent(_ type: T.Type = T.self) throws -> T? { // The root is only nil if the node is empty. - if try currentElementAsArray().isEmpty { - return nil - } + if try currentElementAsArray().isEmpty { return nil } return try decodeRoot(type) } } @@ -142,9 +136,7 @@ extension URIValueFromNodeDecoder { } /// The element at the current head of the coding stack. - private var currentElement: URIDecodedNode { - codingStack.last?.element ?? .dictionary(node) - } + private var currentElement: URIDecodedNode { codingStack.last?.element ?? .dictionary(node) } /// Pushes a new container on top of the current stack, nesting into the /// value at the provided key. @@ -165,22 +157,14 @@ extension URIValueFromNodeDecoder { /// Pops the top container from the stack and restores the previously top /// container to be the current top container. - func pop() { - codingStack.removeLast() - } + func pop() { codingStack.removeLast() } /// Throws a type mismatch error with the provided message. /// - Parameter message: The message to be embedded as debug description /// inside the thrown `DecodingError`. /// - Throws: A `DecodingError` with a type mismatch error if this function is called. private func throwMismatch(_ message: String) throws -> Never { - throw DecodingError.typeMismatch( - String.self, - .init( - codingPath: codingPath, - debugDescription: message - ) - ) + throw DecodingError.typeMismatch(String.self, .init(codingPath: codingPath, debugDescription: message)) } /// Extracts the root value of the provided node using the root key. @@ -204,9 +188,7 @@ extension URIValueFromNodeDecoder { /// as a dictionary. /// - Returns: The value if it can be treated as a dictionary. /// - Throws: An error if the current element cannot be treated as a dictionary. - private func currentElementAsDictionary() throws -> URIParsedNode { - try nodeAsDictionary(currentElement) - } + private func currentElementAsDictionary() throws -> URIParsedNode { try nodeAsDictionary(currentElement) } /// Checks if the provided node can be treated as a dictionary, and returns /// it if so. @@ -237,14 +219,8 @@ extension URIValueFromNodeDecoder { guard values.count % 2 == 0 else { try throwMismatch("Cannot parse an unexploded dictionary an odd number of elements.") } - let pairs = stride( - from: values.startIndex, - to: values.endIndex, - by: 2 - ) - .map { firstIndex in - (values[firstIndex], [values[firstIndex + 1]]) - } + let pairs = stride(from: values.startIndex, to: values.endIndex, by: 2) + .map { firstIndex in (values[firstIndex], [values[firstIndex + 1]]) } let convertedNode = Dictionary(pairs, uniquingKeysWith: { $0 + $1 }) return convertedNode } @@ -253,9 +229,7 @@ extension URIValueFromNodeDecoder { /// as an array. /// - Returns: The value if it can be treated as an array. /// - Throws: An error if the node cannot be treated as an array. - private func currentElementAsArray() throws -> URIParsedValueArray { - try nodeAsArray(currentElement) - } + private func currentElementAsArray() throws -> URIParsedValueArray { try nodeAsArray(currentElement) } /// Checks if the provided node can be treated as an array, and returns /// it if so. @@ -264,12 +238,9 @@ extension URIValueFromNodeDecoder { /// - Throws: An error if the node cannot be treated as a valid array. private func nodeAsArray(_ node: URIDecodedNode) throws -> URIParsedValueArray { switch node { - case .single(let value): - return [value] - case .array(let values): - return values - case .dictionary(let values): - return try rootValue(in: values) + case .single(let value): return [value] + case .array(let values): return values + case .dictionary(let values): return try rootValue(in: values) } } @@ -277,9 +248,7 @@ extension URIValueFromNodeDecoder { /// as a primitive value. /// - Returns: The value if it can be treated as a primitive value. /// - Throws: An error if the node cannot be treated as a primitive value. - func currentElementAsSingleValue() throws -> URIParsedValue { - try nodeAsSingleValue(currentElement) - } + func currentElementAsSingleValue() throws -> URIParsedValue { try nodeAsSingleValue(currentElement) } /// Checks if the provided node can be treated as a primitive value, and /// returns it if so. @@ -292,17 +261,12 @@ extension URIValueFromNodeDecoder { // 2. The value array has a single element. let array: URIParsedValueArray switch node { - case .single(let value): - return value - case .array(let values): - array = values - case .dictionary(let values): - array = try rootValue(in: values) + case .single(let value): return value + case .array(let values): array = values + case .dictionary(let values): array = try rootValue(in: values) } guard array.count == 1 else { - if style == .simple { - return Substring(array.joined(separator: ",")) - } + if style == .simple { return Substring(array.joined(separator: ",")) } let reason = array.isEmpty ? "an empty node" : "a node with multiple values" try throwMismatch("Cannot parse a value from \(reason).") } @@ -316,13 +280,9 @@ extension URIValueFromNodeDecoder { /// - Returns: The nested value. /// - Throws: An error if the current node is not a valid array, or if the /// index is out of bounds. - private func nestedValueInCurrentElementAsArray( - at index: Int - ) throws -> URIParsedValue { + private func nestedValueInCurrentElementAsArray(at index: Int) throws -> URIParsedValue { let values = try currentElementAsArray() - guard index < values.count else { - throw GeneralError.codingKeyOutOfBounds - } + guard index < values.count else { throw GeneralError.codingKeyOutOfBounds } return values[index] } @@ -332,48 +292,30 @@ extension URIValueFromNodeDecoder { /// - Returns: The nested value. /// - Throws: An error if the current node is not a valid dictionary, or /// if no value exists for the key. - private func nestedValuesInCurrentElementAsDictionary( - forKey key: String - ) throws -> URIParsedValueArray { + private func nestedValuesInCurrentElementAsDictionary(forKey key: String) throws -> URIParsedValueArray { let values = try currentElementAsDictionary() - guard let value = values[key[...]] else { - throw GeneralError.codingKeyNotFound - } + guard let value = values[key[...]] else { throw GeneralError.codingKeyNotFound } return value } } extension URIValueFromNodeDecoder: Decoder { - var codingPath: [any CodingKey] { - codingStack.map(\.key) - } + var codingPath: [any CodingKey] { codingStack.map(\.key) } - var userInfo: [CodingUserInfoKey: Any] { - [:] - } + var userInfo: [CodingUserInfoKey: Any] { [:] } - func container( - keyedBy type: Key.Type - ) throws -> KeyedDecodingContainer where Key: CodingKey { + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key: CodingKey { let values = try currentElementAsDictionary() - return .init( - URIKeyedDecodingContainer( - decoder: self, - values: values - ) - ) + return .init(URIKeyedDecodingContainer(decoder: self, values: values)) } func unkeyedContainer() throws -> any UnkeyedDecodingContainer { let values = try currentElementAsArray() - return URIUnkeyedDecodingContainer( - decoder: self, - values: values - ) + return URIUnkeyedDecodingContainer(decoder: self, values: values) } func singleValueContainer() throws -> any SingleValueDecodingContainer { - return URISingleValueDecodingContainer(decoder: self) + URISingleValueDecodingContainer(decoder: self) } } diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift index de400dc1..21028207 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIEncoder.swift @@ -53,16 +53,12 @@ struct URIEncoder: Sendable { /// Creates a new encoder. /// - Parameter serializer: The serializer used to turn `URIEncodedNode` /// values to a string. - init(serializer: URISerializer) { - self.serializer = serializer - } + init(serializer: URISerializer) { self.serializer = serializer } /// Creates a new encoder. /// - Parameter configuration: The configuration instructing the encoder /// how to serialize the value into an URI-encoded string. - init(configuration: URICoderConfiguration) { - self.init(serializer: .init(configuration: configuration)) - } + init(configuration: URICoderConfiguration) { self.init(serializer: .init(configuration: configuration)) } } extension URIEncoder { @@ -80,10 +76,7 @@ extension URIEncoder { /// in which case you still get a key-value pair, like `=foo`. /// - Returns: The URI string. /// - Throws: An error if encoding the object into a URI string fails - func encode( - _ value: some Encodable, - forKey key: String - ) throws -> String { + func encode(_ value: some Encodable, forKey key: String) throws -> String { let encoder = URIValueToNodeEncoder() let node = try encoder.encodeValue(value) var serializer = serializer @@ -104,13 +97,8 @@ extension URIEncoder { /// in which case you still get a key-value pair, like `=foo`. /// - Returns: The URI string. /// - Throws: An error if encoding the object into a URI string fails. - func encodeIfPresent( - _ value: (some Encodable)?, - forKey key: String - ) throws -> String { - guard let value else { - return "" - } + func encodeIfPresent(_ value: (some Encodable)?, forKey key: String) throws -> String { + guard let value else { return "" } let encoder = URIValueToNodeEncoder() let node = try encoder.encodeValue(value) var serializer = serializer diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift index 1361a307..296ab578 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Keyed.swift @@ -49,10 +49,7 @@ extension URIKeyedEncodingContainer { /// - value: The value to insert. /// - key: The key for the value. /// - Throws: An error if inserting the value into the underlying dictionary at the provided key fails. - private func _insertBinaryFloatingPoint( - _ value: some BinaryFloatingPoint, - atKey key: Key - ) throws { + private func _insertBinaryFloatingPoint(_ value: some BinaryFloatingPoint, atKey key: Key) throws { try _insertValue(.double(Double(value)), atKey: key) } @@ -63,10 +60,7 @@ extension URIKeyedEncodingContainer { /// - key: The key for the value. /// - Throws: An error if the provided value is outside the valid range for an integer, /// or if inserting the value into the underlying dictionary at the provided key fails. - private func _insertFixedWidthInteger( - _ value: some FixedWidthInteger, - atKey key: Key - ) throws { + private func _insertFixedWidthInteger(_ value: some FixedWidthInteger, atKey key: Key) throws { guard let validatedValue = Int(exactly: value) else { throw URIValueToNodeEncoder.GeneralError.integerOutOfRange } @@ -76,102 +70,57 @@ extension URIKeyedEncodingContainer { extension URIKeyedEncodingContainer: KeyedEncodingContainerProtocol { - var codingPath: [any CodingKey] { - encoder.codingPath - } + var codingPath: [any CodingKey] { encoder.codingPath } mutating func encodeNil(forKey key: Key) throws { // Setting a nil value is equivalent to not encoding the value at all. } - mutating func encode(_ value: Bool, forKey key: Key) throws { - try _insertValue(.bool(value), atKey: key) - } + mutating func encode(_ value: Bool, forKey key: Key) throws { try _insertValue(.bool(value), atKey: key) } - mutating func encode(_ value: String, forKey key: Key) throws { - try _insertValue(.string(value), atKey: key) - } + mutating func encode(_ value: String, forKey key: Key) throws { try _insertValue(.string(value), atKey: key) } - mutating func encode(_ value: Double, forKey key: Key) throws { - try _insertBinaryFloatingPoint(value, atKey: key) - } + mutating func encode(_ value: Double, forKey key: Key) throws { try _insertBinaryFloatingPoint(value, atKey: key) } - mutating func encode(_ value: Float, forKey key: Key) throws { - try _insertBinaryFloatingPoint(value, atKey: key) - } + mutating func encode(_ value: Float, forKey key: Key) throws { try _insertBinaryFloatingPoint(value, atKey: key) } - mutating func encode(_ value: Int, forKey key: Key) throws { - try _insertFixedWidthInteger(value, atKey: key) - } + mutating func encode(_ value: Int, forKey key: Key) throws { try _insertFixedWidthInteger(value, atKey: key) } - mutating func encode(_ value: Int8, forKey key: Key) throws { - try _insertFixedWidthInteger(value, atKey: key) - } + mutating func encode(_ value: Int8, forKey key: Key) throws { try _insertFixedWidthInteger(value, atKey: key) } - mutating func encode(_ value: Int16, forKey key: Key) throws { - try _insertFixedWidthInteger(value, atKey: key) - } + mutating func encode(_ value: Int16, forKey key: Key) throws { try _insertFixedWidthInteger(value, atKey: key) } - mutating func encode(_ value: Int32, forKey key: Key) throws { - try _insertFixedWidthInteger(value, atKey: key) - } + mutating func encode(_ value: Int32, forKey key: Key) throws { try _insertFixedWidthInteger(value, atKey: key) } - mutating func encode(_ value: Int64, forKey key: Key) throws { - try _insertFixedWidthInteger(value, atKey: key) - } + mutating func encode(_ value: Int64, forKey key: Key) throws { try _insertFixedWidthInteger(value, atKey: key) } - mutating func encode(_ value: UInt, forKey key: Key) throws { - try _insertFixedWidthInteger(value, atKey: key) - } + mutating func encode(_ value: UInt, forKey key: Key) throws { try _insertFixedWidthInteger(value, atKey: key) } - mutating func encode(_ value: UInt8, forKey key: Key) throws { - try _insertFixedWidthInteger(value, atKey: key) - } + mutating func encode(_ value: UInt8, forKey key: Key) throws { try _insertFixedWidthInteger(value, atKey: key) } - mutating func encode(_ value: UInt16, forKey key: Key) throws { - try _insertFixedWidthInteger(value, atKey: key) - } + mutating func encode(_ value: UInt16, forKey key: Key) throws { try _insertFixedWidthInteger(value, atKey: key) } - mutating func encode(_ value: UInt32, forKey key: Key) throws { - try _insertFixedWidthInteger(value, atKey: key) - } + mutating func encode(_ value: UInt32, forKey key: Key) throws { try _insertFixedWidthInteger(value, atKey: key) } - mutating func encode(_ value: UInt64, forKey key: Key) throws { - try _insertFixedWidthInteger(value, atKey: key) - } + mutating func encode(_ value: UInt64, forKey key: Key) throws { try _insertFixedWidthInteger(value, atKey: key) } mutating func encode(_ value: T, forKey key: Key) throws where T: Encodable { switch value { - case let value as UInt8: - try encode(value, forKey: key) - case let value as Int8: - try encode(value, forKey: key) - case let value as UInt16: - try encode(value, forKey: key) - case let value as Int16: - try encode(value, forKey: key) - case let value as UInt32: - try encode(value, forKey: key) - case let value as Int32: - try encode(value, forKey: key) - case let value as UInt64: - try encode(value, forKey: key) - case let value as Int64: - try encode(value, forKey: key) - case let value as Int: - try encode(value, forKey: key) - case let value as UInt: - try encode(value, forKey: key) - case let value as Float: - try encode(value, forKey: key) - case let value as Double: - try encode(value, forKey: key) - case let value as String: - try encode(value, forKey: key) - case let value as Bool: - try encode(value, forKey: key) - case let value as Date: - try _insertValue(.date(value), atKey: key) + case let value as UInt8: try encode(value, forKey: key) + case let value as Int8: try encode(value, forKey: key) + case let value as UInt16: try encode(value, forKey: key) + case let value as Int16: try encode(value, forKey: key) + case let value as UInt32: try encode(value, forKey: key) + case let value as Int32: try encode(value, forKey: key) + case let value as UInt64: try encode(value, forKey: key) + case let value as Int64: try encode(value, forKey: key) + case let value as Int: try encode(value, forKey: key) + case let value as UInt: try encode(value, forKey: key) + case let value as Float: try encode(value, forKey: key) + case let value as Double: try encode(value, forKey: key) + case let value as String: try encode(value, forKey: key) + case let value as Bool: try encode(value, forKey: key) + case let value as Date: try _insertValue(.date(value), atKey: key) default: encoder.push(key: .init(key), newStorage: .unset) try value.encode(to: encoder) @@ -179,24 +128,13 @@ extension URIKeyedEncodingContainer: KeyedEncodingContainerProtocol { } } - mutating func nestedContainer( - keyedBy keyType: NestedKey.Type, - forKey key: Key - ) -> KeyedEncodingContainer where NestedKey: CodingKey { - encoder.container(keyedBy: NestedKey.self) - } + 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 nestedUnkeyedContainer(forKey key: Key) -> any UnkeyedEncodingContainer { encoder.unkeyedContainer() } - mutating func superEncoder() -> any Encoder { - encoder - } + mutating func superEncoder() -> any Encoder { encoder } - mutating func superEncoder(forKey key: Key) -> any Encoder { - encoder - } + mutating func superEncoder(forKey key: Key) -> any Encoder { encoder } } diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift index e2a45b6a..31a82f60 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift @@ -26,9 +26,7 @@ extension URISingleValueEncodingContainer { /// Sets the provided primitive value to the underlying node. /// - Parameter node: The primitive value to set. /// - Throws: An error if setting the primitive value to the underlying node fails. - private func _setValue(_ node: URIEncodedNode.Primitive) throws { - try encoder.currentStackEntry.storage.set(node) - } + private func _setValue(_ node: URIEncodedNode.Primitive) throws { try encoder.currentStackEntry.storage.set(node) } /// Sets the provided value to the underlying node. /// - Parameter value: The value to set. @@ -50,104 +48,58 @@ extension URISingleValueEncodingContainer { extension URISingleValueEncodingContainer: SingleValueEncodingContainer { - var codingPath: [any CodingKey] { - encoder.codingPath - } + var codingPath: [any CodingKey] { encoder.codingPath } func encodeNil() throws { // Nil is encoded as no value. } - func encode(_ value: Bool) throws { - try _setValue(.bool(value)) - } + func encode(_ value: Bool) throws { try _setValue(.bool(value)) } - func encode(_ value: String) throws { - try _setValue(.string(value)) - } + func encode(_ value: String) throws { try _setValue(.string(value)) } - func encode(_ value: Double) throws { - try _setBinaryFloatingPoint(value) - } + func encode(_ value: Double) throws { try _setBinaryFloatingPoint(value) } - func encode(_ value: Float) throws { - try _setBinaryFloatingPoint(value) - } + func encode(_ value: Float) throws { try _setBinaryFloatingPoint(value) } - func encode(_ value: Int) throws { - try _setFixedWidthInteger(value) - } + func encode(_ value: Int) throws { try _setFixedWidthInteger(value) } - func encode(_ value: Int8) throws { - try _setFixedWidthInteger(value) - } + func encode(_ value: Int8) throws { try _setFixedWidthInteger(value) } - func encode(_ value: Int16) throws { - try _setFixedWidthInteger(value) - } + func encode(_ value: Int16) throws { try _setFixedWidthInteger(value) } - func encode(_ value: Int32) throws { - try _setFixedWidthInteger(value) - } + func encode(_ value: Int32) throws { try _setFixedWidthInteger(value) } - func encode(_ value: Int64) throws { - try _setFixedWidthInteger(value) - } + func encode(_ value: Int64) throws { try _setFixedWidthInteger(value) } - func encode(_ value: UInt) throws { - try _setFixedWidthInteger(value) - } + func encode(_ value: UInt) throws { try _setFixedWidthInteger(value) } - func encode(_ value: UInt8) throws { - try _setFixedWidthInteger(value) - } + func encode(_ value: UInt8) throws { try _setFixedWidthInteger(value) } - func encode(_ value: UInt16) throws { - try _setFixedWidthInteger(value) - } + func encode(_ value: UInt16) throws { try _setFixedWidthInteger(value) } - func encode(_ value: UInt32) throws { - try _setFixedWidthInteger(value) - } + func encode(_ value: UInt32) throws { try _setFixedWidthInteger(value) } - func encode(_ value: UInt64) throws { - try _setFixedWidthInteger(value) - } + func encode(_ value: UInt64) throws { try _setFixedWidthInteger(value) } 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 _setValue(.date(value)) - default: - try value.encode(to: encoder) + 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 _setValue(.date(value)) + default: try value.encode(to: encoder) } } } diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift index 7dbf7d7a..35f71884 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Unkeyed.swift @@ -26,16 +26,12 @@ extension URIUnkeyedEncodingContainer { /// Appends the provided node to the underlying array. /// - Parameter node: The node to append. /// - Throws: An error if appending the node to the underlying array fails. - private func _appendValue(_ node: URIEncodedNode) throws { - try encoder.currentStackEntry.storage.append(node) - } + private func _appendValue(_ node: URIEncodedNode) throws { try encoder.currentStackEntry.storage.append(node) } /// Appends the provided primitive value as a node to the underlying array. /// - Parameter node: The value to append. /// - Throws: An error if appending the node to the underlying array fails. - private func _appendValue(_ node: URIEncodedNode.Primitive) throws { - try _appendValue(.primitive(node)) - } + private func _appendValue(_ node: URIEncodedNode.Primitive) throws { try _appendValue(.primitive(node)) } /// Appends the provided value as a node to the underlying array. /// - Parameter value: The value to append. @@ -57,127 +53,70 @@ extension URIUnkeyedEncodingContainer { extension URIUnkeyedEncodingContainer: UnkeyedEncodingContainer { - var codingPath: [any CodingKey] { - encoder.codingPath - } + var codingPath: [any CodingKey] { encoder.codingPath } var count: Int { switch encoder.currentStackEntry.storage { - case .array(let array): - return array.count - case .unset: - return 0 - default: - fatalError("Cannot have an unkeyed container at \(encoder.currentStackEntry).") + case .array(let array): return array.count + case .unset: return 0 + default: fatalError("Cannot have an unkeyed container at \(encoder.currentStackEntry).") } } - func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { - encoder.unkeyedContainer() - } + func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { encoder.unkeyedContainer() } - func nestedContainer( - keyedBy keyType: NestedKey.Type - ) -> KeyedEncodingContainer where NestedKey: CodingKey { - encoder.container(keyedBy: NestedKey.self) - } + func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer + where NestedKey: CodingKey { encoder.container(keyedBy: NestedKey.self) } - func superEncoder() -> any Encoder { - encoder - } + func superEncoder() -> any Encoder { encoder } - func encodeNil() throws { - throw URIValueToNodeEncoder.GeneralError.nilNotSupported - } + func encodeNil() throws { throw URIValueToNodeEncoder.GeneralError.nilNotSupported } - func encode(_ value: Bool) throws { - try _appendValue(.bool(value)) - } + func encode(_ value: Bool) throws { try _appendValue(.bool(value)) } - func encode(_ value: String) throws { - try _appendValue(.string(value)) - } + func encode(_ value: String) throws { try _appendValue(.string(value)) } - func encode(_ value: Double) throws { - try _appendBinaryFloatingPoint(value) - } + func encode(_ value: Double) throws { try _appendBinaryFloatingPoint(value) } - func encode(_ value: Float) throws { - try _appendBinaryFloatingPoint(value) - } + func encode(_ value: Float) throws { try _appendBinaryFloatingPoint(value) } - func encode(_ value: Int) throws { - try _appendFixedWidthInteger(value) - } + func encode(_ value: Int) throws { try _appendFixedWidthInteger(value) } - func encode(_ value: Int8) throws { - try _appendFixedWidthInteger(value) - } + func encode(_ value: Int8) throws { try _appendFixedWidthInteger(value) } - func encode(_ value: Int16) throws { - try _appendFixedWidthInteger(value) - } + func encode(_ value: Int16) throws { try _appendFixedWidthInteger(value) } - func encode(_ value: Int32) throws { - try _appendFixedWidthInteger(value) - } + func encode(_ value: Int32) throws { try _appendFixedWidthInteger(value) } - func encode(_ value: Int64) throws { - try _appendFixedWidthInteger(value) - } + func encode(_ value: Int64) throws { try _appendFixedWidthInteger(value) } - func encode(_ value: UInt) throws { - try _appendFixedWidthInteger(value) - } + func encode(_ value: UInt) throws { try _appendFixedWidthInteger(value) } - func encode(_ value: UInt8) throws { - try _appendFixedWidthInteger(value) - } + func encode(_ value: UInt8) throws { try _appendFixedWidthInteger(value) } - func encode(_ value: UInt16) throws { - try _appendFixedWidthInteger(value) - } + func encode(_ value: UInt16) throws { try _appendFixedWidthInteger(value) } - func encode(_ value: UInt32) throws { - try _appendFixedWidthInteger(value) - } + func encode(_ value: UInt32) throws { try _appendFixedWidthInteger(value) } - func encode(_ value: UInt64) throws { - try _appendFixedWidthInteger(value) - } + func encode(_ value: UInt64) throws { try _appendFixedWidthInteger(value) } 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 _appendValue(.date(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 _appendValue(.date(value)) default: encoder.push(key: .init(intValue: count), newStorage: .unset) try value.encode(to: encoder) diff --git a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift index d46ec9df..b48f2c2f 100644 --- a/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift @@ -52,10 +52,7 @@ final class URIValueToNodeEncoder { /// Creates a new encoder. init() { self._codingPath = [] - self.currentStackEntry = CodingStackEntry( - key: .init(stringValue: ""), - storage: .unset - ) + self.currentStackEntry = CodingStackEntry(key: .init(stringValue: ""), storage: .unset) } /// Encodes the provided value into a node. @@ -65,10 +62,7 @@ final class URIValueToNodeEncoder { func encodeValue(_ value: some Encodable) throws -> URIEncodedNode { defer { _codingPath = [] - currentStackEntry = CodingStackEntry( - key: .init(stringValue: ""), - storage: .unset - ) + currentStackEntry = CodingStackEntry(key: .init(stringValue: ""), storage: .unset) } // We have to catch the special values early, otherwise we fall @@ -117,28 +111,16 @@ extension URIValueToNodeEncoder: Encoder { // The coding path meaningful to the types conforming to Codable. // 1. Omit the root coding path. // 2. Add the current stack entry's coding path. - (_codingPath - .dropFirst() - .map(\.key) - + [currentStackEntry.key]) - .map { $0 as any CodingKey } + (_codingPath.dropFirst().map(\.key) + [currentStackEntry.key]).map { $0 as any CodingKey } } - var userInfo: [CodingUserInfoKey: Any] { - [:] - } + var userInfo: [CodingUserInfoKey: Any] { [:] } - func container( - keyedBy type: Key.Type - ) -> KeyedEncodingContainer where Key: CodingKey { + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key: CodingKey { KeyedEncodingContainer(URIKeyedEncodingContainer(encoder: self)) } - func unkeyedContainer() -> any UnkeyedEncodingContainer { - URIUnkeyedEncodingContainer(encoder: self) - } + func unkeyedContainer() -> any UnkeyedEncodingContainer { URIUnkeyedEncodingContainer(encoder: self) } - func singleValueContainer() -> any SingleValueEncodingContainer { - URISingleValueEncodingContainer(encoder: self) - } + func singleValueContainer() -> any SingleValueEncodingContainer { URISingleValueEncodingContainer(encoder: self) } } diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift index 793f7fcc..3be75420 100644 --- a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift @@ -59,21 +59,15 @@ extension URIParser { // if the style is simple, otherwise it's an empty dictionary. if data.isEmpty { switch configuration.style { - case .form: - return [:] - case .simple: - return ["": [""]] + case .form: return [:] + case .simple: return ["": [""]] } } switch (configuration.style, configuration.explode) { - case (.form, true): - return try parseExplodedFormRoot() - case (.form, false): - return try parseUnexplodedFormRoot() - case (.simple, true): - return try parseExplodedSimpleRoot() - case (.simple, false): - return try parseUnexplodedSimpleRoot() + case (.form, true): return try parseExplodedFormRoot() + case (.form, false): return try parseUnexplodedFormRoot() + case (.simple, true): return try parseExplodedSimpleRoot() + case (.simple, false): return try parseUnexplodedSimpleRoot() } } @@ -154,8 +148,7 @@ extension URIParser { } key = firstValue values = accumulatedValues - case .foundSecondOrEnd: - throw ParsingError.malformedKeyValuePair(firstValue) + case .foundSecondOrEnd: throw ParsingError.malformedKeyValuePair(firstValue) } appendPair(key, values) } @@ -207,9 +200,7 @@ extension URIParser { try parseGenericRoot { data, appendPair in let pairSeparator: Character = "," while !data.isEmpty { - let value = data.parseUpToCharacterOrEnd( - pairSeparator - ) + let value = data.parseUpToCharacterOrEnd(pairSeparator) appendPair(.init(), [value]) } } @@ -225,18 +216,14 @@ extension URIParser { /// be called 0 or more times, once for each parsed key-value pair. /// - Returns: The accumulated node. /// - Throws: An error if parsing using the provided parser closure fails, - private mutating func parseGenericRoot( - _ parser: (inout Raw, (Raw, [Raw]) -> Void) throws -> Void - ) throws -> URIParsedNode { + private mutating func parseGenericRoot(_ parser: (inout Raw, (Raw, [Raw]) -> Void) throws -> Void) throws + -> URIParsedNode + { var root = URIParsedNode() let spaceEscapingCharacter = configuration.spaceEscapingCharacter - let unescapeValue: (Raw) -> Raw = { - Self.unescapeValue($0, spaceEscapingCharacter: spaceEscapingCharacter) - } + let unescapeValue: (Raw) -> Raw = { Self.unescapeValue($0, spaceEscapingCharacter: spaceEscapingCharacter) } try parser(&data) { key, values in - let newItem = [ - unescapeValue(key): values.map(unescapeValue) - ] + let newItem = [unescapeValue(key): values.map(unescapeValue)] root.merge(newItem) { $0 + $1 } } return root @@ -246,10 +233,7 @@ extension URIParser { /// - Parameter escapedValue: An escaped string. /// - Returns: The provided string with escaping removed. private func unescapeValue(_ escapedValue: Raw) -> Raw { - Self.unescapeValue( - escapedValue, - spaceEscapingCharacter: configuration.spaceEscapingCharacter - ) + Self.unescapeValue(escapedValue, spaceEscapingCharacter: configuration.spaceEscapingCharacter) } /// Removes escaping from the provided string. @@ -263,10 +247,7 @@ extension URIParser { spaceEscapingCharacter: URICoderConfiguration.SpaceEscapingCharacter ) -> Raw { // The inverse of URISerializer.computeSafeString. - let partiallyDecoded = escapedValue.replacingOccurrences( - of: spaceEscapingCharacter.rawValue, - with: " " - ) + let partiallyDecoded = escapedValue.replacingOccurrences(of: spaceEscapingCharacter.rawValue, with: " ") return (partiallyDecoded.removingPercentEncoding ?? "")[...] } } @@ -292,19 +273,14 @@ extension String.SubSequence { /// - second: Another character to stop at. /// - Returns: A result indicating which character was detected, if any, and /// the accumulated substring. - fileprivate mutating func parseUpToEitherCharacterOrEnd( - first: Character, - second: Character - ) -> (ParseUpToEitherCharacterResult, Self) { + fileprivate mutating func parseUpToEitherCharacterOrEnd(first: Character, second: Character) -> ( + ParseUpToEitherCharacterResult, Self + ) { let startIndex = startIndex - guard startIndex != endIndex else { - return (.foundSecondOrEnd, .init()) - } + guard startIndex != endIndex else { return (.foundSecondOrEnd, .init()) } var currentIndex = startIndex - func finalize( - _ result: ParseUpToEitherCharacterResult - ) -> (ParseUpToEitherCharacterResult, Self) { + func finalize(_ result: ParseUpToEitherCharacterResult) -> (ParseUpToEitherCharacterResult, Self) { let parsed = self[startIndex.. Self { + fileprivate mutating func parseUpToCharacterOrEnd(_ character: Character) -> Self { let startIndex = startIndex - guard startIndex != endIndex else { - return .init() - } + guard startIndex != endIndex else { return .init() } var currentIndex = startIndex func finalize() -> Self { @@ -350,11 +322,7 @@ extension String.SubSequence { } while currentIndex != endIndex { let currentChar = self[currentIndex] - if currentChar == character { - return finalize() - } else { - formIndex(after: ¤tIndex) - } + if currentChar == character { return finalize() } else { formIndex(after: ¤tIndex) } } return finalize() } diff --git a/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift index 2e3e8b20..26071f85 100644 --- a/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift +++ b/Sources/OpenAPIRuntime/URICoder/Serialization/URISerializer.swift @@ -39,13 +39,8 @@ struct URISerializer { /// style and explode parameters in the configuration). /// - Returns: The URI-encoded data for the provided node. /// - Throws: An error if serialization of the node fails. - mutating func serializeNode( - _ value: URIEncodedNode, - forKey key: String - ) throws -> String { - defer { - data.removeAll(keepingCapacity: true) - } + mutating func serializeNode(_ value: URIEncodedNode, forKey key: String) throws -> String { + defer { data.removeAll(keepingCapacity: true) } try serializeTopLevelNode(value, forKey: key) return data } @@ -83,10 +78,7 @@ extension URISerializer { // The space character needs to be encoded based on the config, // so first allow it to be unescaped, and then we'll do a second // pass and only encode the space based on the config. - let partiallyEncoded = - unsafeString.addingPercentEncoding( - withAllowedCharacters: .unreservedAndSpace - ) ?? "" + let partiallyEncoded = unsafeString.addingPercentEncoding(withAllowedCharacters: .unreservedAndSpace) ?? "" let fullyEncoded = partiallyEncoded.replacingOccurrences( of: " ", with: configuration.spaceEscapingCharacter.rawValue @@ -100,9 +92,7 @@ extension URISerializer { /// - Throws: An error if the key cannot be converted to an escaped string. private func stringifiedKey(_ key: String) throws -> String { // The root key is handled separately. - guard !key.isEmpty else { - return "" - } + guard !key.isEmpty else { return "" } let safeTopLevelKey = computeSafeString(key) return safeTopLevelKey } @@ -113,14 +103,9 @@ extension URISerializer { /// - key: The key to serialize the value under (details depend on the /// style and explode parameters in the configuration). /// - Throws: An error if serialization of the value fails. - private mutating func serializeTopLevelNode( - _ value: URIEncodedNode, - forKey key: String - ) throws { + private mutating func serializeTopLevelNode(_ value: URIEncodedNode, forKey key: String) throws { func unwrapPrimitiveValue(_ node: URIEncodedNode) throws -> URIEncodedNode.Primitive { - guard case let .primitive(primitive) = node else { - throw SerializationError.nestedContainersNotSupported - } + guard case let .primitive(primitive) = node else { throw SerializationError.nestedContainersNotSupported } return primitive } switch value { @@ -130,47 +115,27 @@ extension URISerializer { case .primitive(let primitive): let keyAndValueSeparator: String? switch configuration.style { - case .form: - keyAndValueSeparator = "=" - case .simple: - keyAndValueSeparator = nil + case .form: keyAndValueSeparator = "=" + case .simple: keyAndValueSeparator = nil } - try serializePrimitiveKeyValuePair( - primitive, - forKey: key, - separator: keyAndValueSeparator - ) - case .array(let array): - try serializeArray( - array.map(unwrapPrimitiveValue), - forKey: key - ) + try serializePrimitiveKeyValuePair(primitive, forKey: key, separator: keyAndValueSeparator) + case .array(let array): try serializeArray(array.map(unwrapPrimitiveValue), forKey: key) case .dictionary(let dictionary): - try serializeDictionary( - dictionary.mapValues(unwrapPrimitiveValue), - forKey: key - ) + try serializeDictionary(dictionary.mapValues(unwrapPrimitiveValue), forKey: key) } } /// Serializes the provided value into the underlying string. /// - Parameter value: The primitive value to serialize. /// - Throws: An error if serialization of the primitive value fails. - private mutating func serializePrimitiveValue( - _ value: URIEncodedNode.Primitive - ) throws { + private mutating func serializePrimitiveValue(_ value: URIEncodedNode.Primitive) throws { let stringValue: String switch value { - case .bool(let bool): - stringValue = bool.description - case .string(let string): - stringValue = computeSafeString(string) - case .integer(let int): - stringValue = int.description - case .double(let double): - stringValue = double.description - case .date(let date): - stringValue = try computeSafeString(configuration.dateTranscoder.encode(date)) + case .bool(let bool): stringValue = bool.description + case .string(let string): stringValue = computeSafeString(string) + case .integer(let int): stringValue = int.description + case .double(let double): stringValue = double.description + case .date(let date): stringValue = try computeSafeString(configuration.dateTranscoder.encode(date)) } data.append(stringValue) } @@ -201,13 +166,8 @@ extension URISerializer { /// - key: The key to serialize the value under (details depend on the /// style and explode parameters in the configuration). /// - Throws: An error if serialization of the array fails. - private mutating func serializeArray( - _ array: [URIEncodedNode.Primitive], - forKey key: String - ) throws { - guard !array.isEmpty else { - return - } + private mutating func serializeArray(_ array: [URIEncodedNode.Primitive], forKey key: String) throws { + guard !array.isEmpty else { return } let keyAndValueSeparator: String? let pairSeparator: String switch (configuration.style, configuration.explode) { @@ -223,11 +183,7 @@ extension URISerializer { } func serializeNext(_ element: URIEncodedNode.Primitive) throws { if let keyAndValueSeparator { - try serializePrimitiveKeyValuePair( - element, - forKey: key, - separator: keyAndValueSeparator - ) + try serializePrimitiveKeyValuePair(element, forKey: key, separator: keyAndValueSeparator) } else { try serializePrimitiveValue(element) } @@ -240,9 +196,7 @@ extension URISerializer { try serializeNext(element) data.append(pairSeparator) } - if let element = array.last { - try serializeNext(element) - } + if let element = array.last { try serializeNext(element) } } /// Serializes the provided dictionary into the underlying string. @@ -251,19 +205,13 @@ extension URISerializer { /// - key: The key to serialize the value under (details depend on the /// style and explode parameters in the configuration). /// - Throws: An error if serialization of the dictionary fails. - private mutating func serializeDictionary( - _ dictionary: [String: URIEncodedNode.Primitive], - forKey key: String - ) throws { - guard !dictionary.isEmpty else { - return + private mutating func serializeDictionary(_ dictionary: [String: URIEncodedNode.Primitive], forKey key: String) + throws + { + guard !dictionary.isEmpty else { return } + let sortedDictionary = dictionary.sorted { a, b in + a.key.localizedCaseInsensitiveCompare(b.key) == .orderedAscending } - let sortedDictionary = - dictionary - .sorted { a, b in - a.key.localizedCaseInsensitiveCompare(b.key) - == .orderedAscending - } let keyAndValueSeparator: String let pairSeparator: String @@ -283,11 +231,7 @@ extension URISerializer { } func serializeNext(_ element: URIEncodedNode.Primitive, forKey elementKey: String) throws { - try serializePrimitiveKeyValuePair( - element, - forKey: elementKey, - separator: keyAndValueSeparator - ) + try serializePrimitiveKeyValuePair(element, forKey: elementKey, separator: keyAndValueSeparator) } if let containerKeyAndValue = configuration.containerKeyAndValueSeparator { data.append(try stringifiedKey(key)) @@ -297,9 +241,7 @@ extension URISerializer { try serializeNext(element, forKey: elementKey) data.append(pairSeparator) } - if let (elementKey, element) = sortedDictionary.last { - try serializeNext(element, forKey: elementKey) - } + if let (elementKey, element) = sortedDictionary.last { try serializeNext(element, forKey: elementKey) } } } @@ -310,10 +252,8 @@ extension URICoderConfiguration { /// serialized, only the value. fileprivate var containerKeyAndValueSeparator: String? { switch (style, explode) { - case (.form, false): - return "=" - default: - return nil + case (.form, false): return "=" + default: return nil } } } diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_Acceptable.swift b/Tests/OpenAPIRuntimeTests/Base/Test_Acceptable.swift index 405af225..31927b0b 100644 --- a/Tests/OpenAPIRuntimeTests/Base/Test_Acceptable.swift +++ b/Tests/OpenAPIRuntimeTests/Base/Test_Acceptable.swift @@ -20,25 +20,19 @@ enum TestAcceptable: AcceptableProtocol { init?(rawValue: String) { switch rawValue { - case "application/json": - self = .json - default: - self = .other(rawValue) + case "application/json": self = .json + default: self = .other(rawValue) } } var rawValue: String { switch self { - case .json: - return "application/json" - case .other(let string): - return string + case .json: return "application/json" + case .other(let string): return string } } - static var allCases: [TestAcceptable] { - [.json] - } + static var allCases: [TestAcceptable] { [.json] } } final class Test_AcceptHeaderContentType: Test_Runtime { @@ -48,43 +42,23 @@ final class Test_AcceptHeaderContentType: Test_Runtime { XCTAssertEqual(contentType.contentType, .json) XCTAssertEqual(contentType.quality, 1.0) XCTAssertEqual(contentType.rawValue, "application/json") - XCTAssertEqual( - AcceptHeaderContentType(rawValue: "application/json"), - contentType - ) + XCTAssertEqual(AcceptHeaderContentType(rawValue: "application/json"), contentType) } do { - let contentType = AcceptHeaderContentType( - contentType: TestAcceptable.json, - quality: 0.5 - ) + let contentType = AcceptHeaderContentType(contentType: TestAcceptable.json, quality: 0.5) XCTAssertEqual(contentType.contentType, .json) XCTAssertEqual(contentType.quality, 0.5) XCTAssertEqual(contentType.rawValue, "application/json; q=0.500") - XCTAssertEqual( - AcceptHeaderContentType(rawValue: "application/json; q=0.500"), - contentType - ) - } - do { - XCTAssertEqual( - AcceptHeaderContentType.defaultValues, - [ - .init(contentType: .json) - ] - ) + XCTAssertEqual(AcceptHeaderContentType(rawValue: "application/json; q=0.500"), contentType) } + do { XCTAssertEqual(AcceptHeaderContentType.defaultValues, [.init(contentType: .json)]) } do { let unsorted: [AcceptHeaderContentType] = [ - .init(contentType: .other("*/*"), quality: 0.3), - .init(contentType: .json, quality: 0.5), + .init(contentType: .other("*/*"), quality: 0.3), .init(contentType: .json, quality: 0.5), ] XCTAssertEqual( unsorted.sortedByQuality(), - [ - .init(contentType: .json, quality: 0.5), - .init(contentType: .other("*/*"), quality: 0.3), - ] + [.init(contentType: .json, quality: 0.5), .init(contentType: .other("*/*"), quality: 0.3)] ) } } diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_CopyOnWriteBox.swift b/Tests/OpenAPIRuntimeTests/Base/Test_CopyOnWriteBox.swift index 59c9bd56..36121d7b 100644 --- a/Tests/OpenAPIRuntimeTests/Base/Test_CopyOnWriteBox.swift +++ b/Tests/OpenAPIRuntimeTests/Base/Test_CopyOnWriteBox.swift @@ -22,62 +22,19 @@ final class Test_CopyOnWriteBox: Test_Runtime { } func testModification() throws { - var value = Node( - id: 3, - parent: .init( - value: .init( - id: 2 - ) - ) - ) - XCTAssertEqual( - value, - Node( - id: 3, - parent: .init( - value: .init( - id: 2 - ) - ) - ) - ) + var value = Node(id: 3, parent: .init(value: .init(id: 2))) + XCTAssertEqual(value, Node(id: 3, parent: .init(value: .init(id: 2)))) value.parent!.value.parent = .init(value: .init(id: 1)) - XCTAssertEqual( - value, - Node( - id: 3, - parent: .init( - value: .init( - id: 2, - parent: .init( - value: .init(id: 1) - ) - ) - ) - ) - ) + XCTAssertEqual(value, Node(id: 3, parent: .init(value: .init(id: 2, parent: .init(value: .init(id: 1)))))) } func testSerialization() throws { let value = CopyOnWriteBox(value: "Hello") - try testRoundtrip( - value, - expectedJSON: #""Hello""# - ) + try testRoundtrip(value, expectedJSON: #""Hello""#) } func testIntegration() throws { - let value = Node( - id: 3, - parent: .init( - value: .init( - id: 2, - parent: .init( - value: .init(id: 1) - ) - ) - ) - ) + let value = Node(id: 3, parent: .init(value: .init(id: 2, parent: .init(value: .init(id: 1))))) try testRoundtrip( value, expectedJSON: #""" diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIMIMEType.swift b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIMIMEType.swift index 3fbbf97d..207d3920 100644 --- a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIMIMEType.swift +++ b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIMIMEType.swift @@ -20,35 +20,23 @@ final class Test_OpenAPIMIMEType: Test_Runtime { // Common ( - "application/json", - OpenAPIMIMEType(kind: .concrete(type: "application", subtype: "json")), + "application/json", OpenAPIMIMEType(kind: .concrete(type: "application", subtype: "json")), "application/json" ), // Subtype wildcard - ( - "application/*", - OpenAPIMIMEType(kind: .anySubtype(type: "application")), - "application/*" - ), + ("application/*", OpenAPIMIMEType(kind: .anySubtype(type: "application")), "application/*"), // Type wildcard - ( - "*/*", - OpenAPIMIMEType(kind: .any), - "*/*" - ), + ("*/*", OpenAPIMIMEType(kind: .any), "*/*"), // Common with a parameter ( "application/json; charset=UTF-8", OpenAPIMIMEType( kind: .concrete(type: "application", subtype: "json"), - parameters: [ - "charset": "UTF-8" - ] - ), - "application/json; charset=UTF-8" + parameters: ["charset": "UTF-8"] + ), "application/json; charset=UTF-8" ), // Common with two parameters @@ -56,12 +44,8 @@ final class Test_OpenAPIMIMEType: Test_Runtime { "application/json; charset=UTF-8; boundary=1234", OpenAPIMIMEType( kind: .concrete(type: "application", subtype: "json"), - parameters: [ - "charset": "UTF-8", - "boundary": "1234", - ] - ), - "application/json; boundary=1234; charset=UTF-8" + parameters: ["charset": "UTF-8", "boundary": "1234"] + ), "application/json; boundary=1234; charset=UTF-8" ), // Common case preserving, but case insensitive equality @@ -69,17 +53,12 @@ final class Test_OpenAPIMIMEType: Test_Runtime { "APPLICATION/JSON;CHARSET=UTF-8", OpenAPIMIMEType( kind: .concrete(type: "application", subtype: "json"), - parameters: [ - "charset": "UTF-8" - ] - ), - "APPLICATION/JSON; CHARSET=UTF-8" + parameters: ["charset": "UTF-8"] + ), "APPLICATION/JSON; CHARSET=UTF-8" ), // Invalid - ("application", nil, nil), - ("application/foo/bar", nil, nil), - ("", nil, nil), + ("application", nil, nil), ("application/foo/bar", nil, nil), ("", nil, nil), ] for (inputString, expectedMIME, outputString) in cases { let mime = OpenAPIMIMEType(inputString) @@ -91,20 +70,15 @@ final class Test_OpenAPIMIMEType: Test_Runtime { func testScore() throws { let cases: [(OpenAPIMIMEType.Match, Int)] = [ - (.incompatible(.type), 0), - (.incompatible(.subtype), 0), - (.incompatible(.parameter(name: "foo")), 0), + (.incompatible(.type), 0), (.incompatible(.subtype), 0), (.incompatible(.parameter(name: "foo")), 0), (.wildcard, 1), (.subtypeWildcard, 2), - (.typeAndSubtype(matchedParameterCount: 0), 3), - (.typeAndSubtype(matchedParameterCount: 2), 5), + (.typeAndSubtype(matchedParameterCount: 0), 3), (.typeAndSubtype(matchedParameterCount: 2), 5), ] - for (match, score) in cases { - XCTAssertEqual(match.score, score, "Mismatch for match: \(match)") - } + for (match, score) in cases { XCTAssertEqual(match.score, score, "Mismatch for match: \(match)") } } func testEvaluate() throws { @@ -141,10 +115,7 @@ final class Test_OpenAPIMIMEType: Test_Runtime { testCase( receivedType: "application", receivedSubtype: "json", - receivedParameters: [ - "charset": "utf-8", - "version": "1", - ], + receivedParameters: ["charset": "utf-8", "version": "1"], against: option, expected: expectedMatch, file: file, @@ -154,25 +125,10 @@ final class Test_OpenAPIMIMEType: Test_Runtime { // Actual test cases start here. - testJSONWith2Params( - against: jsonWith2Params, - expected: .typeAndSubtype(matchedParameterCount: 2) - ) - testJSONWith2Params( - against: jsonWith1Param, - expected: .typeAndSubtype(matchedParameterCount: 1) - ) - testJSONWith2Params( - against: json, - expected: .typeAndSubtype(matchedParameterCount: 0) - ) - testJSONWith2Params( - against: subtypeWildcard, - expected: .subtypeWildcard - ) - testJSONWith2Params( - against: fullWildcard, - expected: .wildcard - ) + testJSONWith2Params(against: jsonWith2Params, expected: .typeAndSubtype(matchedParameterCount: 2)) + testJSONWith2Params(against: jsonWith1Param, expected: .typeAndSubtype(matchedParameterCount: 1)) + testJSONWith2Params(against: json, expected: .typeAndSubtype(matchedParameterCount: 0)) + testJSONWith2Params(against: subtypeWildcard, expected: .subtypeWildcard) + testJSONWith2Params(against: fullWildcard, expected: .wildcard) } } diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift index 5549e88a..080f5dd1 100644 --- a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift +++ b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift @@ -36,18 +36,8 @@ final class Test_OpenAPIValue: Test_Runtime { func testEncoding_container_success() throws { let values: [(any Sendable)?] = [ - nil, - "Hello", - [ - "key": "value", - "anotherKey": [ - 1, - "two", - ] as [any Sendable], - ] as [String: any Sendable], - 1 as Int, - 2.5 as Double, - [true], + nil, "Hello", ["key": "value", "anotherKey": [1, "two"] as [any Sendable]] as [String: any Sendable], + 1 as Int, 2.5 as Double, [true], ] let container = try OpenAPIValueContainer(unvalidatedValue: values) let expectedString = #""" @@ -127,12 +117,7 @@ final class Test_OpenAPIValue: Test_Runtime { } func testEncoding_object_success() throws { - let values: [String: (any Sendable)?] = [ - "key": "value", - "keyMore": [ - true - ], - ] + let values: [String: (any Sendable)?] = ["key": "value", "keyMore": [true]] let container = try OpenAPIObjectContainer(unvalidatedValue: values) let expectedString = #""" { @@ -162,10 +147,7 @@ final class Test_OpenAPIValue: Test_Runtime { } func testEncoding_array_success() throws { - let values: [(any Sendable)?] = [ - "one", - ["two": 2], - ] + let values: [(any Sendable)?] = ["one", ["two": 2]] let container = try OpenAPIArrayContainer(unvalidatedValue: values) let expectedString = #""" [ @@ -203,17 +185,8 @@ final class Test_OpenAPIValue: Test_Runtime { Foo( bar: "hi", dict: try .init(unvalidatedValue: [ - "baz": "bar", - "number": 1, - "nestedArray": [ - 1, - [ - "k": "v" - ], - ] as [(any Sendable)?], - "nestedDict": [ - "nested": 2 - ], + "baz": "bar", "number": 1, "nestedArray": [1, ["k": "v"]] as [(any Sendable)?], + "nestedDict": ["nested": 2], ]) ), expectedJSON: #""" @@ -280,10 +253,7 @@ final class Test_OpenAPIValue: Test_Runtime { // `testStructBase64EncodedString` quoted and base64-encoded again let JSONEncoded = Data(base64Encoded: "ImV5SnVZVzFsSWpvaVJteDFabVo2SW4wPSI=")! - XCTAssertEqual( - try JSONDecoder().decode(Base64EncodedData.self, from: JSONEncoded), - encodedData - ) + XCTAssertEqual(try JSONDecoder().decode(Base64EncodedData.self, from: JSONEncoded), encodedData) } func testEncodingDecodingRoundtrip_base64_success() throws { diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_CodableExtensions.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_CodableExtensions.swift index 03d02e12..8c613659 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_CodableExtensions.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_CodableExtensions.swift @@ -16,9 +16,7 @@ import XCTest final class Test_CodableExtensions: Test_Runtime { - var testDecoder: JSONDecoder { - JSONDecoder() - } + var testDecoder: JSONDecoder { JSONDecoder() } var testEncoder: JSONEncoder { let encoder = JSONEncoder() @@ -31,16 +29,12 @@ final class Test_CodableExtensions: Test_Runtime { struct Foo: Decodable { var bar: String - enum CodingKeys: String, CodingKey { - case bar - } + enum CodingKeys: String, CodingKey { case bar } init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.bar = try container.decode(String.self, forKey: .bar) - try decoder.ensureNoAdditionalProperties( - knownKeys: ["bar"] - ) + try decoder.ensureNoAdditionalProperties(knownKeys: ["bar"]) } } @@ -87,16 +81,12 @@ final class Test_CodableExtensions: Test_Runtime { var bar: String var additionalProperties: OpenAPIObjectContainer - enum CodingKeys: String, CodingKey { - case bar - } + enum CodingKeys: String, CodingKey { case bar } init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.bar = try container.decode(String.self, forKey: .bar) - self.additionalProperties = - try decoder - .decodeAdditionalProperties(knownKeys: ["bar"]) + self.additionalProperties = try decoder.decodeAdditionalProperties(knownKeys: ["bar"]) } } @@ -138,16 +128,12 @@ final class Test_CodableExtensions: Test_Runtime { var bar: String var additionalProperties: [String: Int] - enum CodingKeys: String, CodingKey { - case bar - } + enum CodingKeys: String, CodingKey { case bar } init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.bar = try container.decode(String.self, forKey: .bar) - self.additionalProperties = - try decoder - .decodeAdditionalProperties(knownKeys: ["bar"]) + self.additionalProperties = try decoder.decodeAdditionalProperties(knownKeys: ["bar"]) } } @@ -189,9 +175,7 @@ final class Test_CodableExtensions: Test_Runtime { var bar: String var additionalProperties = OpenAPIObjectContainer() - enum CodingKeys: String, CodingKey { - case bar - } + enum CodingKeys: String, CodingKey { case bar } func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -201,9 +185,7 @@ final class Test_CodableExtensions: Test_Runtime { } do { - let value = Foo( - bar: "hi" - ) + let value = Foo(bar: "hi") let data = try testEncoder.encode(value) XCTAssertEqual( String(decoding: data, as: UTF8.self), @@ -216,13 +198,7 @@ final class Test_CodableExtensions: Test_Runtime { } do { - let value = Foo( - bar: "hi", - additionalProperties: try .init(unvalidatedValue: [ - "baz": "bar", - "number": 1, - ]) - ) + let value = Foo(bar: "hi", additionalProperties: try .init(unvalidatedValue: ["baz": "bar", "number": 1])) let data = try testEncoder.encode(value) XCTAssertEqual( String(decoding: data, as: UTF8.self), @@ -243,9 +219,7 @@ final class Test_CodableExtensions: Test_Runtime { var bar: String var additionalProperties: [String: Int] = [:] - enum CodingKeys: String, CodingKey { - case bar - } + enum CodingKeys: String, CodingKey { case bar } func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -255,9 +229,7 @@ final class Test_CodableExtensions: Test_Runtime { } do { - let value = Foo( - bar: "hi" - ) + let value = Foo(bar: "hi") let data = try testEncoder.encode(value) XCTAssertEqual( String(decoding: data, as: UTF8.self), @@ -270,12 +242,7 @@ final class Test_CodableExtensions: Test_Runtime { } do { - let value = Foo( - bar: "hi", - additionalProperties: [ - "number": 1 - ] - ) + let value = Foo(bar: "hi", additionalProperties: ["number": 1]) let data = try testEncoder.encode(value) XCTAssertEqual( String(decoding: data, as: UTF8.self), diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift index 250642ce..135bdf46 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -23,12 +23,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { in: &headerFields, contentTypes: [.init(contentType: TestAcceptable.json, quality: 0.8)] ) - XCTAssertEqual( - headerFields, - [ - .accept: "application/json; q=0.800" - ] - ) + XCTAssertEqual(headerFields, [.accept: "application/json; q=0.800"]) } // MARK: Converter helper methods @@ -37,11 +32,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { 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], - ] + parameters: [1 as Int, "foo" as String, [.land, .air] as [TestHabitat]] ) XCTAssertEqual(renderedPath, "/items/1/detail/foo/habitats/land,air") } @@ -49,37 +40,19 @@ final class Test_ClientConverterExtensions: Test_Runtime { // | 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" - ) + try converter.setQueryItemAsURI(in: &request, style: nil, explode: nil, name: "search", value: "foo") XCTAssertEqual(request.soar_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" - ) + try converter.setQueryItemAsURI(in: &request, style: nil, explode: nil, name: "search", value: "h%llo") XCTAssertEqual(request.soar_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"] - ) + try converter.setQueryItemAsURI(in: &request, style: nil, explode: nil, name: "search", value: ["foo", "bar"]) XCTAssertEqual(request.soar_query, "search=foo&search=bar") } @@ -97,13 +70,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { func test_setQueryItemAsURI_date() throws { var request = testRequest - try converter.setQueryItemAsURI( - in: &request, - style: nil, - explode: nil, - name: "search", - value: testDate - ) + try converter.setQueryItemAsURI(in: &request, style: nil, explode: nil, name: "search", value: testDate) XCTAssertEqual(request.soar_query, "search=2023-01-18T10%3A04%3A11Z") } @@ -128,12 +95,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { contentType: "application/json" ) try await XCTAssertEqualStringifiedData(body, testStructPrettyString) - XCTAssertEqual( - headerFields, - [ - .contentType: "application/json" - ] - ) + XCTAssertEqual(headerFields, [.contentType: "application/json"]) } func test_setOptionalRequestBodyAsJSON_codable_string() async throws { @@ -144,12 +106,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { contentType: "application/json" ) try await XCTAssertEqualStringifiedData(body, testQuotedString) - XCTAssertEqual( - headerFields, - [ - .contentType: "application/json" - ] - ) + XCTAssertEqual(headerFields, [.contentType: "application/json"]) } // | client | set | request body | JSON | required | setRequiredRequestBodyAsJSON | @@ -161,12 +118,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { contentType: "application/json" ) try await XCTAssertEqualStringifiedData(body, testStructPrettyString) - XCTAssertEqual( - headerFields, - [ - .contentType: "application/json" - ] - ) + XCTAssertEqual(headerFields, [.contentType: "application/json"]) } // | client | set | request body | urlEncodedForm | codable | optional | setRequiredRequestBodyAsURLEncodedForm | @@ -184,12 +136,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { } try await XCTAssertEqualStringifiedData(body, testStructURLFormString) - XCTAssertEqual( - headerFields, - [ - .contentType: "application/x-www-form-urlencoded" - ] - ) + XCTAssertEqual(headerFields, [.contentType: "application/x-www-form-urlencoded"]) } // | client | set | request body | urlEncodedForm | codable | required | setRequiredRequestBodyAsURLEncodedForm | @@ -201,12 +148,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { contentType: "application/x-www-form-urlencoded" ) try await XCTAssertEqualStringifiedData(body, testStructURLFormString) - XCTAssertEqual( - headerFields, - [ - .contentType: "application/x-www-form-urlencoded" - ] - ) + XCTAssertEqual(headerFields, [.contentType: "application/x-www-form-urlencoded"]) } // | client | set | request body | binary | optional | setOptionalRequestBodyAsBinary | @@ -218,12 +160,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { contentType: "application/octet-stream" ) try await XCTAssertEqualStringifiedData(body, testString) - XCTAssertEqual( - headerFields, - [ - .contentType: "application/octet-stream" - ] - ) + XCTAssertEqual(headerFields, [.contentType: "application/octet-stream"]) } // | client | set | request body | binary | required | setRequiredRequestBodyAsBinary | @@ -235,12 +172,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { contentType: "application/octet-stream" ) try await XCTAssertEqualStringifiedData(body, testString) - XCTAssertEqual( - headerFields, - [ - .contentType: "application/octet-stream" - ] - ) + XCTAssertEqual(headerFields, [.contentType: "application/octet-stream"]) } // | client | get | response body | JSON | required | getResponseBodyAsJSON | @@ -282,7 +214,5 @@ public func XCTAssertEqualStringifiedData( do { let actualString = String(decoding: try expression1(), as: UTF8.self) XCTAssertEqual(actualString, try expression2(), file: file, line: line) - } catch { - XCTFail(error.localizedDescription, file: file, line: line) - } + } catch { XCTFail(error.localizedDescription, file: file, line: line) } } diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift index 4819e912..da68208f 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift @@ -15,40 +15,26 @@ import XCTest @_spi(Generated) import OpenAPIRuntime import HTTPTypes -extension HTTPField.Name { - static var foo: Self { - Self("foo")! - } -} +extension HTTPField.Name { static var foo: Self { Self("foo")! } } final class Test_CommonConverterExtensions: Test_Runtime { // MARK: Miscs - @available(*, deprecated) - func testContentTypeMatching() throws { + @available(*, deprecated) 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", "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), + ("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 - ), + try converter.isMatchingContentType(received: .init(testCase.received), expectedRaw: testCase.expected), testCase.isMatch, "Wrong result for (\(testCase.received), \(testCase.expected), \(testCase.isMatch))" ) @@ -63,63 +49,21 @@ final class Test_CommonConverterExtensions: Test_Runtime { file: StaticString = #file, line: UInt = #line ) throws { - let choice = try converter.bestContentType( - received: received.map { .init($0)! }, - options: options - ) + let choice = try converter.bestContentType(received: received.map { .init($0)! }, options: options) XCTAssertEqual(choice, expectedChoice, file: file, line: line) } - try testCase( - received: nil, - options: [ - "application/json", - "*/*", - ], - expected: "application/json" - ) - try testCase( - received: "*/*", - options: [ - "application/json", - "*/*", - ], - expected: "application/json" - ) - try testCase( - received: "application/*", - options: [ - "application/json", - "*/*", - ], - expected: "application/json" - ) - XCTAssertThrowsError( - try testCase( - received: "application/json", - options: [ - "whoops" - ], - expected: "-" - ) - ) + try testCase(received: nil, options: ["application/json", "*/*"], expected: "application/json") + try testCase(received: "*/*", options: ["application/json", "*/*"], expected: "application/json") + try testCase(received: "application/*", options: ["application/json", "*/*"], expected: "application/json") + XCTAssertThrowsError(try testCase(received: "application/json", options: ["whoops"], expected: "-")) XCTAssertThrowsError( - try testCase( - received: "application/json", - options: [ - "text/plain", - "image/*", - ], - expected: "-" - ) + try testCase(received: "application/json", options: ["text/plain", "image/*"], expected: "-") ) try testCase( received: "application/json; charset=utf-8; version=1", options: [ - "*/*", - "application/*", - "application/json", - "application/json; charset=utf-8", + "*/*", "application/*", "application/json", "application/json; charset=utf-8", "application/json; charset=utf-8; version=1", ], expected: "application/json; charset=utf-8; version=1" @@ -127,10 +71,7 @@ final class Test_CommonConverterExtensions: Test_Runtime { try testCase( received: "application/json; version=1; CHARSET=utf-8", options: [ - "*/*", - "application/*", - "application/json", - "application/json; charset=utf-8", + "*/*", "application/*", "application/json", "application/json; charset=utf-8", "application/json; charset=utf-8; version=1", ], expected: "application/json; charset=utf-8; version=1" @@ -138,67 +79,31 @@ final class Test_CommonConverterExtensions: Test_Runtime { try testCase( received: "application/json", options: [ - "application/json; charset=utf-8", - "application/json; charset=utf-8; version=1", - "*/*", - "application/*", + "application/json; charset=utf-8", "application/json; charset=utf-8; version=1", "*/*", "application/*", "application/json", ], expected: "application/json" ) try testCase( received: "application/json; charset=utf-8", - options: [ - "application/json; charset=utf-8; version=1", - "*/*", - "application/*", - "application/json", - ], + options: ["application/json; charset=utf-8; version=1", "*/*", "application/*", "application/json"], expected: "application/json" ) try testCase( received: "application/json; charset=utf-8; version=1", - options: [ - "*/*", - "application/*", - "application/json; charset=utf-8", - "application/json", - ], + options: ["*/*", "application/*", "application/json; charset=utf-8", "application/json"], expected: "application/json; charset=utf-8" ) try testCase( received: "application/json; charset=utf-8; version=1", - options: [ - "*/*", - "application/*", - ], + options: ["*/*", "application/*"], expected: "application/*" ) - try testCase( - received: "application/json; charset=utf-8; version=1", - options: [ - "*/*" - ], - expected: "*/*" - ) + try testCase(received: "application/json; charset=utf-8; version=1", options: ["*/*"], expected: "*/*") - try testCase( - received: "image/png", - options: [ - "image/*", - "*/*", - ], - expected: "image/*" - ) + try testCase(received: "image/png", options: ["image/*", "*/*"], expected: "image/*") XCTAssertThrowsError( - try testCase( - received: "text/csv", - options: [ - "text/html", - "application/json", - ], - expected: "-" - ) + try testCase(received: "text/csv", options: ["text/html", "application/json"], expected: "-") ) } @@ -207,140 +112,63 @@ final class Test_CommonConverterExtensions: Test_Runtime { // | 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" - ] - ) + 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" - ] - ) + 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 - ] - ) + 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)" - ] - ) + 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" - ] - ) + 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 - ] - ) + 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\"" - ] - ) + 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 - ) + 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 - ) + 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 headerFields: HTTPFields = [.foo: "bar,baz"] let value: [String]? = try converter.getOptionalHeaderFieldAsURI( in: headerFields, name: "foo", @@ -350,14 +178,8 @@ final class Test_CommonConverterExtensions: Test_Runtime { } func test_getOptionalHeaderFieldAsURI_date() throws { - let headerFields: HTTPFields = [ - .foo: testDateEscapedString - ] - let value: Date? = try converter.getOptionalHeaderFieldAsURI( - in: headerFields, - name: "foo", - as: Date.self - ) + let headerFields: HTTPFields = [.foo: testDateEscapedString] + let value: Date? = try converter.getOptionalHeaderFieldAsURI(in: headerFields, name: "foo", as: Date.self) XCTAssertEqual(value, testDate) } @@ -365,31 +187,19 @@ final class Test_CommonConverterExtensions: Test_Runtime { let headerFields: HTTPFields = [ .foo: "\(testDateString),\(testDateEscapedString)" // escaped and unescaped ] - let value: [Date] = try converter.getRequiredHeaderFieldAsURI( - in: headerFields, - name: "foo", - as: [Date].self - ) + 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 - ) + 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 headerFields: HTTPFields = [.foo: testStructString] let value: TestPet? = try converter.getOptionalHeaderFieldAsJSON( in: headerFields, name: "foo", @@ -400,14 +210,8 @@ final class Test_CommonConverterExtensions: Test_Runtime { // | 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 - ) + 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 6617f60a..91525af4 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift @@ -18,18 +18,13 @@ import HTTPTypes final class Test_ServerConverterExtensions: Test_Runtime { func testExtractAccept() throws { - let headerFields: HTTPFields = [ - .accept: "application/json, */*; q=0.8" - ] + 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), - ] + [.init(contentType: .json, quality: 1.0), .init(contentType: .other("*/*"), quality: 0.8)] ) } @@ -37,21 +32,13 @@ final class Test_ServerConverterExtensions: Test_Runtime { func testValidateAccept() throws { let emptyHeaders: HTTPFields = [:] - let wildcard: HTTPFields = [ - .accept: "*/*" - ] - let partialWildcard: HTTPFields = [ - .accept: "text/*" - ] - let short: HTTPFields = [ - .accept: "text/plain" - ] + 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 multiple: HTTPFields = [.accept: "text/plain, application/json"] let cases: [(HTTPFields, String, Bool)] = [ // No Accept header, any string validates successfully (emptyHeaders, "foobar", true), @@ -60,40 +47,27 @@ final class Test_ServerConverterExtensions: Test_Runtime { (wildcard, "foobar", true), // Accept: text/*, so text/plain succeeds, application/json fails - (partialWildcard, "text/plain", true), - (partialWildcard, "application/json", false), + (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), + (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), + (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), + (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 - ), + try converter.validateAcceptIfPresent(contentType, in: headers), "Unexpected error when validating string: \(contentType) against headers: \(headers)" ) } else { XCTAssertThrowsError( - try converter.validateAcceptIfPresent( - contentType, - in: headers - ), + try converter.validateAcceptIfPresent(contentType, in: headers), "Expected to throw error when validating string: \(contentType) against headers: \(headers)" ) } @@ -105,41 +79,22 @@ final class Test_ServerConverterExtensions: Test_Runtime { // | server | get | request path | URI | required | getPathParameterAsURI | func test_getPathParameterAsURI_various() throws { let path: [String: Substring] = [ - "foo": "bar", - "number": "1", - "habitats": "land,air", - "withEscaping": "Hello%20world%21", + "foo": "bar", "number": "1", "habitats": "land,air", "withEscaping": "Hello%20world%21", ] do { - let value = try converter.getPathParameterAsURI( - in: path, - name: "foo", - as: String.self - ) + 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 - ) + 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 - ) + let value = try converter.getPathParameterAsURI(in: path, name: "habitats", as: [TestHabitat].self) XCTAssertEqual(value, [.land, .air]) } do { - let value = try converter.getPathParameterAsURI( - in: path, - name: "withEscaping", - as: String.self - ) + let value = try converter.getPathParameterAsURI(in: path, name: "withEscaping", as: String.self) XCTAssertEqual(value, "Hello world!") } } @@ -342,12 +297,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { contentType: "application/json" ) try await XCTAssertEqualStringifiedData(data, testStructPrettyString) - XCTAssertEqual( - headers, - [ - .contentType: "application/json" - ] - ) + XCTAssertEqual(headers, [.contentType: "application/json"]) } // | server | set | response body | binary | required | setResponseBodyAsBinary | @@ -359,11 +309,6 @@ final class Test_ServerConverterExtensions: Test_Runtime { contentType: "application/octet-stream" ) try await XCTAssertEqualStringifiedData(data, testString) - XCTAssertEqual( - headers, - [ - .contentType: "application/octet-stream" - ] - ) + XCTAssertEqual(headers, [.contentType: "application/octet-stream"]) } } diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_ServerVariable.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_ServerVariable.swift index 0b134b2e..793a2d71 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_ServerVariable.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_ServerVariable.swift @@ -18,29 +18,14 @@ final class Test_ServerVariable: Test_Runtime { func testOnlyConstants() throws { XCTAssertEqual( - try URL( - validatingOpenAPIServerURL: "https://example.com", - variables: [] - ) - .absoluteString, + try URL(validatingOpenAPIServerURL: "https://example.com", variables: []).absoluteString, "https://example.com" ) XCTAssertEqual( - try URL( - validatingOpenAPIServerURL: "https://example.com/api", - variables: [] - ) - .absoluteString, + try URL(validatingOpenAPIServerURL: "https://example.com/api", variables: []).absoluteString, "https://example.com/api" ) - XCTAssertEqual( - try URL( - validatingOpenAPIServerURL: "/api", - variables: [] - ) - .absoluteString, - "/api" - ) + XCTAssertEqual(try URL(validatingOpenAPIServerURL: "/api", variables: []).absoluteString, "/api") } func testVariables() throws { diff --git a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift index bc6cc623..16a684c1 100644 --- a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift +++ b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift @@ -22,65 +22,44 @@ final class Test_Body: Test_Runtime { // A single string. do { let body: HTTPBody = HTTPBody("hello") - try await _testConsume( - body, - expected: "hello" - ) + try await _testConsume(body, expected: "hello") } // A literal string. do { let body: HTTPBody = "hello" - try await _testConsume( - body, - expected: "hello" - ) + try await _testConsume(body, expected: "hello") } // A single substring. do { let substring: Substring = "hello" let body: HTTPBody = HTTPBody(substring) - try await _testConsume( - body, - expected: "hello" - ) + try await _testConsume(body, expected: "hello") } // A single array of bytes. do { let body: HTTPBody = HTTPBody([0]) - try await _testConsume( - body, - expected: [0] - ) + try await _testConsume(body, expected: [0]) } // A literal array of bytes. do { let body: HTTPBody = [0] - try await _testConsume( - body, - expected: [0] - ) + try await _testConsume(body, expected: [0]) } // A single data. do { let body: HTTPBody = HTTPBody(Data([0])) - try await _testConsume( - body, - expected: [0] - ) + try await _testConsume(body, expected: [0]) } // A single slice of an array of bytes. do { let body: HTTPBody = HTTPBody([0][...]) - try await _testConsume( - body, - expected: [0][...] - ) + try await _testConsume(body, expected: [0][...]) } // An async throwing stream. @@ -96,10 +75,7 @@ final class Test_Body: Test_Runtime { ), length: .known(5) ) - try await _testConsume( - body, - expected: "hello" - ) + try await _testConsume(body, expected: "hello") } // An async throwing stream, unknown length. @@ -115,10 +91,7 @@ final class Test_Body: Test_Runtime { ), length: .unknown ) - try await _testConsume( - body, - expected: "hello" - ) + try await _testConsume(body, expected: "hello") } // An async stream. @@ -134,10 +107,7 @@ final class Test_Body: Test_Runtime { ), length: .known(5) ) - try await _testConsume( - body, - expected: "hello" - ) + try await _testConsume(body, expected: "hello") } // Another async sequence. @@ -151,15 +121,8 @@ final class Test_Body: Test_Runtime { } ) .map { $0 } - let body: HTTPBody = HTTPBody( - sequence, - length: .known(5), - iterationBehavior: .single - ) - try await _testConsume( - body, - expected: "hello" - ) + let body: HTTPBody = HTTPBody(sequence, length: .known(5), iterationBehavior: .single) + try await _testConsume(body, expected: "hello") } } @@ -173,15 +136,9 @@ final class Test_Body: Test_Runtime { } ) .map { $0 } - let body: HTTPBody = HTTPBody( - sequence, - length: .known(5), - iterationBehavior: .single - ) + let body: HTTPBody = HTTPBody(sequence, length: .known(5), iterationBehavior: .single) var chunks: [HTTPBody.ByteChunk] = [] - for try await chunk in body { - chunks.append(chunk) - } + for try await chunk in body { chunks.append(chunk) } XCTAssertEqual(chunks, ["hel", "lo"].map { Array($0.utf8)[...] }) } @@ -202,18 +159,12 @@ final class Test_Body: Test_Runtime { } ) .map { $0 } - let body: HTTPBody = HTTPBody( - sequence, - length: .unknown, - iterationBehavior: .single - ) + let body: HTTPBody = HTTPBody(sequence, length: .unknown, iterationBehavior: .single) XCTAssertFalse(body.testing_iteratorCreated) var chunkCount = 0 - for try await _ in body { - chunkCount += 1 - } + for try await _ in body { chunkCount += 1 } XCTAssertEqual(chunkCount, 2) XCTAssertTrue(body.testing_iteratorCreated) @@ -231,9 +182,7 @@ final class Test_Body: Test_Runtime { do { var chunkCount = 0 - for try await _ in body { - chunkCount += 1 - } + for try await _ in body { chunkCount += 1 } XCTAssertEqual(chunkCount, 1) } @@ -241,9 +190,7 @@ final class Test_Body: Test_Runtime { do { var chunkCount = 0 - for try await _ in body { - chunkCount += 1 - } + for try await _ in body { chunkCount += 1 } XCTAssertEqual(chunkCount, 1) } @@ -277,22 +224,16 @@ final class Test_Body: Test_Runtime { } extension Test_Body { - func _testConsume( - _ body: HTTPBody, - expected: HTTPBody.ByteChunk, - file: StaticString = #file, - line: UInt = #line - ) async throws { + func _testConsume(_ body: HTTPBody, expected: HTTPBody.ByteChunk, file: StaticString = #file, line: UInt = #line) + async throws + { let output = try await ArraySlice(collecting: body, upTo: .max) XCTAssertEqual(output, expected, file: file, line: line) } - func _testConsume( - _ body: HTTPBody, - expected: some StringProtocol, - file: StaticString = #file, - line: UInt = #line - ) async throws { + func _testConsume(_ body: HTTPBody, expected: some StringProtocol, file: StaticString = #file, line: UInt = #line) + async throws + { let output = try await String(collecting: body, upTo: .max) XCTAssertEqual(output, expected.description, file: file, line: line) } diff --git a/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalClient.swift b/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalClient.swift index 64e38c86..b0063e70 100644 --- a/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalClient.swift +++ b/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalClient.swift @@ -18,29 +18,16 @@ import Foundation struct MockClientTransport: ClientTransport { var sendBlock: @Sendable (HTTPRequest, HTTPBody?, URL, String) async throws -> (HTTPResponse, HTTPBody?) - func send( - _ request: HTTPRequest, - body: HTTPBody?, - baseURL: URL, - operationID: String - ) async throws -> (HTTPResponse, HTTPBody?) { - try await sendBlock(request, body, baseURL, operationID) - } + func send(_ request: HTTPRequest, body: HTTPBody?, baseURL: URL, operationID: String) async throws -> ( + HTTPResponse, HTTPBody? + ) { try await sendBlock(request, body, baseURL, operationID) } static let requestBody: HTTPBody = HTTPBody("hello") static let responseBody: HTTPBody = HTTPBody("bye") - static var successful: Self { - MockClientTransport { _, _, _, _ in - (HTTPResponse(status: .ok), responseBody) - } - } + static var successful: Self { MockClientTransport { _, _, _, _ in (HTTPResponse(status: .ok), responseBody) } } - static var failing: Self { - MockClientTransport { _, _, _, _ in - throw TestError() - } - } + static var failing: Self { MockClientTransport { _, _, _, _ in throw TestError() } } } final class Test_UniversalClient: Test_Runtime { @@ -50,12 +37,7 @@ final class Test_UniversalClient: Test_Runtime { let output = try await client.send( input: "input", forOperation: "op", - serializer: { input in - ( - HTTPRequest(soar_path: "/", method: .post), - MockClientTransport.requestBody - ) - }, + serializer: { input in (HTTPRequest(soar_path: "/", method: .post), MockClientTransport.requestBody) }, deserializer: { response, body in let body = try XCTUnwrap(body) let string = try await String(collecting: body, upTo: 10) @@ -71,12 +53,8 @@ final class Test_UniversalClient: Test_Runtime { try await client.send( input: "input", forOperation: "op", - serializer: { input in - throw TestError() - }, - deserializer: { response, body in - fatalError() - } + serializer: { input in throw TestError() }, + deserializer: { response, body in fatalError() } ) } catch { let clientError = try XCTUnwrap(error as? ClientError) @@ -96,22 +74,13 @@ final class Test_UniversalClient: Test_Runtime { do { let client = UniversalClient( transport: MockClientTransport.successful, - middlewares: [ - MockMiddleware(failurePhase: .onRequest) - ] + middlewares: [MockMiddleware(failurePhase: .onRequest)] ) try await client.send( input: "input", forOperation: "op", - serializer: { input in - ( - HTTPRequest(soar_path: "/", method: .post), - MockClientTransport.requestBody - ) - }, - deserializer: { response, body in - fatalError() - } + serializer: { input in (HTTPRequest(soar_path: "/", method: .post), MockClientTransport.requestBody) }, + deserializer: { response, body in fatalError() } ) } catch { let clientError = try XCTUnwrap(error as? ClientError) @@ -129,24 +98,12 @@ final class Test_UniversalClient: Test_Runtime { func testErrorPropagation_transport() async throws { do { - let client = UniversalClient( - transport: MockClientTransport.failing, - middlewares: [ - MockMiddleware() - ] - ) + let client = UniversalClient(transport: MockClientTransport.failing, middlewares: [MockMiddleware()]) try await client.send( input: "input", forOperation: "op", - serializer: { input in - ( - HTTPRequest(soar_path: "/", method: .post), - MockClientTransport.requestBody - ) - }, - deserializer: { response, body in - fatalError() - } + serializer: { input in (HTTPRequest(soar_path: "/", method: .post), MockClientTransport.requestBody) }, + deserializer: { response, body in fatalError() } ) } catch { let clientError = try XCTUnwrap(error as? ClientError) @@ -166,22 +123,13 @@ final class Test_UniversalClient: Test_Runtime { do { let client = UniversalClient( transport: MockClientTransport.successful, - middlewares: [ - MockMiddleware(failurePhase: .onResponse) - ] + middlewares: [MockMiddleware(failurePhase: .onResponse)] ) try await client.send( input: "input", forOperation: "op", - serializer: { input in - ( - HTTPRequest(soar_path: "/", method: .post), - MockClientTransport.requestBody - ) - }, - deserializer: { response, body in - fatalError() - } + serializer: { input in (HTTPRequest(soar_path: "/", method: .post), MockClientTransport.requestBody) }, + deserializer: { response, body in fatalError() } ) } catch { let clientError = try XCTUnwrap(error as? ClientError) @@ -203,15 +151,8 @@ final class Test_UniversalClient: Test_Runtime { try await client.send( input: "input", forOperation: "op", - serializer: { input in - ( - HTTPRequest(soar_path: "/", method: .post), - MockClientTransport.requestBody - ) - }, - deserializer: { response, body in - throw TestError() - } + serializer: { input in (HTTPRequest(soar_path: "/", method: .post), MockClientTransport.requestBody) }, + deserializer: { response, body in throw TestError() } ) } catch { let clientError = try XCTUnwrap(error as? ClientError) diff --git a/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift b/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift index 88b2ae96..e65afe4f 100644 --- a/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift +++ b/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift @@ -19,12 +19,8 @@ import Foundation struct MockHandler: Sendable { var shouldFail: Bool = false func greet(_ input: String) async throws -> String { - if shouldFail { - throw TestError() - } - guard input == "hello" else { - throw TestError() - } + if shouldFail { throw TestError() } + guard input == "hello" else { throw TestError() } return "bye" } @@ -46,9 +42,7 @@ final class Test_UniversalServer: Test_Runtime { let body = try XCTUnwrap(body) return try await String(collecting: body, upTo: 10) }, - serializer: { output, _ in - (HTTPResponse(status: .ok), MockHandler.responseBody) - } + serializer: { output, _ in (HTTPResponse(status: .ok), MockHandler.responseBody) } ) XCTAssertEqual(response, HTTPResponse(status: .ok)) XCTAssertEqual(responseBody, MockHandler.responseBody) @@ -58,9 +52,7 @@ final class Test_UniversalServer: Test_Runtime { do { let server = UniversalServer( handler: MockHandler(), - middlewares: [ - MockMiddleware(failurePhase: .onRequest) - ] + middlewares: [MockMiddleware(failurePhase: .onRequest)] ) _ = try await server.handle( request: .init(soar_path: "/", method: .post), @@ -68,12 +60,8 @@ final class Test_UniversalServer: Test_Runtime { metadata: .init(), forOperation: "op", using: { MockHandler.greet($0) }, - deserializer: { request, body, metadata in - fatalError() - }, - serializer: { output, _ in - fatalError() - } + deserializer: { request, body, metadata in fatalError() }, + serializer: { output, _ in fatalError() } ) } catch { let serverError = try XCTUnwrap(error as? ServerError) @@ -97,12 +85,8 @@ final class Test_UniversalServer: Test_Runtime { metadata: .init(), forOperation: "op", using: { MockHandler.greet($0) }, - deserializer: { request, body, metadata in - throw TestError() - }, - serializer: { output, _ in - fatalError() - } + deserializer: { request, body, metadata in throw TestError() }, + serializer: { output, _ in fatalError() } ) } catch { let serverError = try XCTUnwrap(error as? ServerError) @@ -130,9 +114,7 @@ final class Test_UniversalServer: Test_Runtime { let body = try XCTUnwrap(body) return try await String(collecting: body, upTo: 10) }, - serializer: { output, _ in - fatalError() - } + serializer: { output, _ in fatalError() } ) } catch { let serverError = try XCTUnwrap(error as? ServerError) @@ -160,9 +142,7 @@ final class Test_UniversalServer: Test_Runtime { let body = try XCTUnwrap(body) return try await String(collecting: body, upTo: 10) }, - serializer: { output, _ in - throw TestError() - } + serializer: { output, _ in throw TestError() } ) } catch { let serverError = try XCTUnwrap(error as? ServerError) @@ -181,9 +161,7 @@ final class Test_UniversalServer: Test_Runtime { do { let server = UniversalServer( handler: MockHandler(), - middlewares: [ - MockMiddleware(failurePhase: .onResponse) - ] + middlewares: [MockMiddleware(failurePhase: .onResponse)] ) _ = try await server.handle( request: .init(soar_path: "/", method: .post), @@ -195,9 +173,7 @@ final class Test_UniversalServer: Test_Runtime { let body = try XCTUnwrap(body) return try await String(collecting: body, upTo: 10) }, - serializer: { output, _ in - (HTTPResponse(status: .ok), MockHandler.responseBody) - } + serializer: { output, _ in (HTTPResponse(status: .ok), MockHandler.responseBody) } ) } catch { let serverError = try XCTUnwrap(error as? ServerError) @@ -213,9 +189,7 @@ final class Test_UniversalServer: Test_Runtime { } func testApiPathComponentsWithServerPrefix_noPrefix() throws { - let server = UniversalServer( - handler: MockHandler() - ) + let server = UniversalServer(handler: MockHandler()) let components = "/foo/{bar}" let prefixed = try server.apiPathComponentsWithServerPrefix(components) // When no server path prefix, components stay the same @@ -223,10 +197,7 @@ final class Test_UniversalServer: Test_Runtime { } func testApiPathComponentsWithServerPrefix_withPrefix() throws { - let server = UniversalServer( - serverURL: try serverURL, - handler: MockHandler() - ) + let server = UniversalServer(serverURL: try serverURL, handler: MockHandler()) let components = "/foo/{bar}" let prefixed = try server.apiPathComponentsWithServerPrefix(components) let expected = "/api/foo/{bar}" diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index 704b1ef6..29666cc1 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -24,19 +24,11 @@ class Test_Runtime: XCTestCase { continueAfterFailure = false } - var serverURL: URL { - get throws { - try URL(validatingOpenAPIServerURL: "/api") - } - } + var serverURL: URL { get throws { try URL(validatingOpenAPIServerURL: "/api") } } - var configuration: Configuration { - .init() - } + var configuration: Configuration { .init() } - var converter: Converter { - .init(configuration: configuration) - } + var converter: Converter { .init(configuration: configuration) } var testComponents: URLComponents { var components = URLComponents() @@ -44,57 +36,31 @@ class Test_Runtime: XCTestCase { return components } - var testRequest: HTTPRequest { - .init(soar_path: "/api", method: .get) - } + var testRequest: HTTPRequest { .init(soar_path: "/api", method: .get) } - var testDate: Date { - Date(timeIntervalSince1970: 1_674_036_251) - } + var testDate: Date { Date(timeIntervalSince1970: 1_674_036_251) } - var testDateString: String { - "2023-01-18T10:04:11Z" - } + var testDateString: String { "2023-01-18T10:04:11Z" } - var testDateEscapedString: String { - "2023-01-18T10%3A04%3A11Z" - } + var testDateEscapedString: String { "2023-01-18T10%3A04%3A11Z" } - var testDateStringData: Data { - Data(testDateString.utf8) - } + var testDateStringData: Data { Data(testDateString.utf8) } - var testDateEscapedStringData: Data { - Data(testDateEscapedString.utf8) - } + var testDateEscapedStringData: Data { Data(testDateEscapedString.utf8) } - var testString: String { - "hello" - } + var testString: String { "hello" } - var testStringData: Data { - Data(testString.utf8) - } + var testStringData: Data { Data(testString.utf8) } - var testQuotedString: String { - "\"hello\"" - } + var testQuotedString: String { "\"hello\"" } - var testQuotedStringData: Data { - Data(testQuotedString.utf8) - } + var testQuotedStringData: Data { Data(testQuotedString.utf8) } - var testStruct: TestPet { - .init(name: "Fluffz") - } + var testStruct: TestPet { .init(name: "Fluffz") } - var testStructDetailed: TestPetDetailed { - .init(name: "Rover!", type: "Golden Retriever", age: "3") - } + var testStructDetailed: TestPetDetailed { .init(name: "Rover!", type: "Golden Retriever", age: "3") } - var testStructString: String { - #"{"name":"Fluffz"}"# - } + var testStructString: String { #"{"name":"Fluffz"}"# } var testStructPrettyString: String { #""" @@ -104,36 +70,24 @@ class Test_Runtime: XCTestCase { """# } - var testStructURLFormString: String { - "age=3&name=Rover%21&type=Golden+Retriever" - } + var testStructURLFormString: String { "age=3&name=Rover%21&type=Golden+Retriever" } var testStructBase64EncodedString: String { #""eyJuYW1lIjoiRmx1ZmZ6In0=""# // {"name":"Fluffz"} } - var testEnum: TestHabitat { - .water - } + var testEnum: TestHabitat { .water } - var testEnumString: String { - "water" - } + var testEnumString: String { "water" } - var testStructData: Data { - Data(testStructString.utf8) - } + var testStructData: Data { Data(testStructString.utf8) } - var testStructPrettyData: Data { - Data(testStructPrettyString.utf8) - } + var testStructPrettyData: Data { Data(testStructPrettyString.utf8) } - var testStructURLFormData: Data { - Data(testStructURLFormString.utf8) - } + var testStructURLFormData: Data { Data(testStructURLFormString.utf8) } - @discardableResult - func _testPrettyEncoded(_ value: Value, expectedJSON: String) throws -> String { + @discardableResult func _testPrettyEncoded(_ value: Value, expectedJSON: String) throws -> String + { let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] let data = try encoder.encode(value) @@ -172,13 +126,9 @@ struct MockMiddleware: ClientMiddleware, ServerMiddleware { operationID: String, next: (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) ) async throws -> (HTTPResponse, HTTPBody?) { - if failurePhase == .onRequest { - throw TestError() - } + if failurePhase == .onRequest { throw TestError() } let (response, responseBody) = try await next(request, body, baseURL) - if failurePhase == .onResponse { - throw TestError() - } + if failurePhase == .onResponse { throw TestError() } return (response, responseBody) } @@ -189,13 +139,9 @@ struct MockMiddleware: ClientMiddleware, ServerMiddleware { operationID: String, next: (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody?) ) async throws -> (HTTPResponse, HTTPBody?) { - if failurePhase == .onRequest { - throw TestError() - } + if failurePhase == .onRequest { throw TestError() } let (response, responseBody) = try await next(request, body, metadata) - if failurePhase == .onResponse { - throw TestError() - } + if failurePhase == .onResponse { throw TestError() } return (response, responseBody) } } @@ -215,9 +161,7 @@ public func XCTAssertEqualURLString(_ lhs: URL?, _ rhs: String, file: StaticStri XCTAssertEqual(lhs.absoluteString, rhs, file: file, line: line) } -struct TestPet: Codable, Equatable { - var name: String -} +struct TestPet: Codable, Equatable { var name: String } struct TestPetDetailed: Codable, Equatable { var name: String @@ -293,9 +237,7 @@ public func XCTAssertEqualStringifiedData( } 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) - } + } catch { XCTFail(error.localizedDescription, file: file, line: line) } } /// Asserts that the string representation of binary data in an HTTP body is equal to an expected string. @@ -314,10 +256,6 @@ public func XCTAssertEqualStringifiedData( line: UInt = #line ) async throws { let data: Data - if let body = try expression1() { - data = try await Data(collecting: body, upTo: .max) - } else { - data = .init() - } + 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) } diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift index a98e91b5..c02c83c3 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIDecoder.swift @@ -17,15 +17,9 @@ import XCTest final class Test_URIDecoder: Test_Runtime { func testDecoding() throws { - struct Foo: Decodable, Equatable { - var bar: String - } + struct Foo: Decodable, Equatable { var bar: String } let decoder = URIDecoder(configuration: .formDataExplode) - let decodedValue = try decoder.decode( - Foo.self, - forKey: "", - from: "bar=hello+world" - ) + let decodedValue = try decoder.decode(Foo.self, forKey: "", from: "bar=hello+world") XCTAssertEqual(decodedValue, Foo(bar: "hello world")) } @@ -36,19 +30,11 @@ final class Test_URIDecoder: Test_Runtime { } let decoder = URIDecoder(configuration: .formDataExplode) do { - let decodedValue = try decoder.decode( - Foo.self, - forKey: "", - from: "baz=1&bar=hello+world" - ) + let decodedValue = try decoder.decode(Foo.self, forKey: "", from: "baz=1&bar=hello+world") XCTAssertEqual(decodedValue, Foo(bar: "hello world", baz: 1)) } do { - let decodedValue = try decoder.decode( - Foo.self, - forKey: "", - from: "baz=1" - ) + let decodedValue = try decoder.decode(Foo.self, forKey: "", from: "baz=1") XCTAssertEqual(decodedValue, Foo(baz: 1)) } } @@ -56,27 +42,15 @@ final class Test_URIDecoder: Test_Runtime { func testDecoding_rootValue() throws { let decoder = URIDecoder(configuration: .formDataExplode) do { - let decodedValue = try decoder.decode( - Int.self, - forKey: "root", - from: "root=1" - ) + let decodedValue = try decoder.decode(Int.self, forKey: "root", from: "root=1") XCTAssertEqual(decodedValue, 1) } do { - let decodedValue = try decoder.decodeIfPresent( - Int.self, - forKey: "root", - from: "baz=1" - ) + let decodedValue = try decoder.decodeIfPresent(Int.self, forKey: "root", from: "baz=1") XCTAssertEqual(decodedValue, nil) } do { - let decodedValue = try decoder.decodeIfPresent( - Int.self, - forKey: "root", - from: "" - ) + let decodedValue = try decoder.decodeIfPresent(Int.self, forKey: "root", from: "") XCTAssertEqual(decodedValue, nil) } } @@ -85,11 +59,7 @@ final class Test_URIDecoder: Test_Runtime { let decoder = URIDecoder(configuration: .simpleUnexplode) do { - let decodedValue = try decoder.decode( - String.self, - forKey: "", - from: "foo%2C%20bar" - ) + let decodedValue = try decoder.decode(String.self, forKey: "", from: "foo%2C%20bar") XCTAssertEqual(decodedValue, "foo, bar") } } @@ -98,11 +68,7 @@ final class Test_URIDecoder: Test_Runtime { let decoder = URIDecoder(configuration: .simpleUnexplode) do { - let decodedValue = try decoder.decode( - String.self, - forKey: "", - from: "foo, bar" - ) + let decodedValue = try decoder.decode(String.self, forKey: "", from: "foo, bar") XCTAssertEqual(decodedValue, "foo, bar") } } diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift index 8a67ac0e..bbbf4dae 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Decoder/Test_URIValueFromNodeDecoder.swift @@ -30,97 +30,43 @@ final class Test_URIValueFromNodeDecoder: Test_Runtime { } // An empty string. - try test( - ["root": [""]], - "", - key: "root" - ) + try test(["root": [""]], "", key: "root") // An empty string with a simple style. - try test( - ["root": [""]], - "", - key: "root", - style: .simple - ) + try test(["root": [""]], "", key: "root", style: .simple) // A string with a space. - try test( - ["root": ["Hello World"]], - "Hello World", - key: "root" - ) + try test(["root": ["Hello World"]], "Hello World", key: "root") // An enum. - try test( - ["root": ["red"]], - SimpleEnum.red, - key: "root" - ) + try test(["root": ["red"]], SimpleEnum.red, key: "root") // An integer. - try test( - ["root": ["1234"]], - 1234, - key: "root" - ) + try test(["root": ["1234"]], 1234, key: "root") // A float. - try test( - ["root": ["12.34"]], - 12.34, - key: "root" - ) + try test(["root": ["12.34"]], 12.34, key: "root") // A bool. - try test( - ["root": ["true"]], - true, - key: "root" - ) + try test(["root": ["true"]], true, key: "root") // A simple array of strings. - try test( - ["root": ["a", "b", "c"]], - ["a", "b", "c"], - key: "root" - ) + try test(["root": ["a", "b", "c"]], ["a", "b", "c"], key: "root") // A simple array of enums. - try test( - ["root": ["red", "green", "blue"]], - [.red, .green, .blue] as [SimpleEnum], - key: "root" - ) + try test(["root": ["red", "green", "blue"]], [.red, .green, .blue] as [SimpleEnum], key: "root") // A struct. - try test( - ["foo": ["bar"]], - SimpleStruct(foo: "bar"), - key: "root" - ) + try test(["foo": ["bar"]], SimpleStruct(foo: "bar"), key: "root") // A struct with a nested enum. - try test( - ["foo": ["bar"], "color": ["blue"]], - SimpleStruct(foo: "bar", color: .blue), - key: "root" - ) + try test(["foo": ["bar"], "color": ["blue"]], SimpleStruct(foo: "bar", color: .blue), key: "root") // A simple dictionary. - try test( - ["one": ["1"], "two": ["2"]], - ["one": 1, "two": 2], - key: "root" - ) + try test(["one": ["1"], "two": ["2"]], ["one": 1, "two": 2], key: "root") // A unexploded simple dictionary. - try test( - ["root": ["one", "1", "two", "2"]], - ["one": 1, "two": 2], - key: "root", - explode: false - ) + try test(["root": ["one", "1", "two", "2"]], ["one": 1, "two": 2], key: "root", explode: false) // A dictionary of enums. try test( @@ -146,12 +92,7 @@ final class Test_URIValueFromNodeDecoder: Test_Runtime { dateTranscoder: .iso8601 ) let decodedValue = try decoder.decodeRoot(T.self) - XCTAssertEqual( - decodedValue, - expectedValue, - file: file, - line: line - ) + XCTAssertEqual(decodedValue, expectedValue, file: file, line: line) } } } diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIEncoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIEncoder.swift index 4250db26..fe9d445e 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIEncoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIEncoder.swift @@ -17,15 +17,10 @@ import XCTest final class Test_URIEncoder: Test_Runtime { func testEncoding() throws { - struct Foo: Encodable { - var bar: String - } + struct Foo: Encodable { var bar: String } let serializer = URISerializer(configuration: .formDataExplode) let encoder = URIEncoder(serializer: serializer) - let encodedString = try encoder.encode( - Foo(bar: "hello world"), - forKey: "root" - ) + let encodedString = try encoder.encode(Foo(bar: "hello world"), forKey: "root") XCTAssertEqual(encodedString, "bar=hello+world") } } diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift b/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift index d6967014..913511b6 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Encoding/Test_URIValueToNodeEncoder.swift @@ -28,11 +28,7 @@ final class Test_URIValueToNodeEncoder: Test_Runtime { _ expectedNode: URIEncodedNode, file: StaticString = #file, line: UInt = #line - ) - -> Case - { - .init(value: value, expectedNode: expectedNode, file: file, line: line) - } + ) -> Case { .init(value: value, expectedNode: expectedNode, file: file, line: line) } enum SimpleEnum: String, Encodable { case foo @@ -45,226 +41,118 @@ final class Test_URIValueToNodeEncoder: Test_Runtime { var val: SimpleEnum? } - struct NestedStruct: Encodable { - var simple: SimpleStruct - } + struct NestedStruct: Encodable { var simple: SimpleStruct } let cases: [Case] = [ // An empty string. - makeCase( - "", - .primitive(.string("")) - ), + makeCase("", .primitive(.string(""))), // A string with a space. - makeCase( - "Hello World", - .primitive(.string("Hello World")) - ), + makeCase("Hello World", .primitive(.string("Hello World"))), // An integer. - makeCase( - 1234, - .primitive(.integer(1234)) - ), + makeCase(1234, .primitive(.integer(1234))), // A float. - makeCase( - 12.34, - .primitive(.double(12.34)) - ), + makeCase(12.34, .primitive(.double(12.34))), // A bool. - makeCase( - true, - .primitive(.bool(true)) - ), + makeCase(true, .primitive(.bool(true))), // An enum. - makeCase( - SimpleEnum.foo, - .primitive(.string("foo")) - ), + makeCase(SimpleEnum.foo, .primitive(.string("foo"))), // A simple array of strings. makeCase( ["a", "b", "c"], - .array([ - .primitive(.string("a")), - .primitive(.string("b")), - .primitive(.string("c")), - ]) + .array([.primitive(.string("a")), .primitive(.string("b")), .primitive(.string("c"))]) ), // A simple array of enums. makeCase( [SimpleEnum.foo, SimpleEnum.bar], - .array([ - .primitive(.string("foo")), - .primitive(.string("bar")), - ]) + .array([.primitive(.string("foo")), .primitive(.string("bar"))]) ), // A nested array. makeCase( [["a"], ["b", "c"]], .array([ - .array([ - .primitive(.string("a")) - ]), - .array([ - .primitive(.string("b")), - .primitive(.string("c")), - ]), + .array([.primitive(.string("a"))]), .array([.primitive(.string("b")), .primitive(.string("c"))]), ]) ), // A struct. makeCase( SimpleStruct(foo: "bar", val: .foo), - .dictionary([ - "foo": .primitive(.string("bar")), - "val": .primitive(.string("foo")), - ]) + .dictionary(["foo": .primitive(.string("bar")), "val": .primitive(.string("foo"))]) ), // A nested struct. makeCase( NestedStruct(simple: SimpleStruct(foo: "bar")), - .dictionary([ - "simple": .dictionary([ - "foo": .primitive(.string("bar")) - ]) - ]) + .dictionary(["simple": .dictionary(["foo": .primitive(.string("bar"))])]) ), // An array of structs. makeCase( - [ - SimpleStruct(foo: "bar"), - SimpleStruct(foo: "baz", val: .bar), - ], + [SimpleStruct(foo: "bar"), SimpleStruct(foo: "baz", val: .bar)], .array([ - .dictionary([ - "foo": .primitive(.string("bar")) - ]), - .dictionary([ - "foo": .primitive(.string("baz")), - "val": .primitive(.string("bar")), - ]), + .dictionary(["foo": .primitive(.string("bar"))]), + .dictionary(["foo": .primitive(.string("baz")), "val": .primitive(.string("bar"))]), ]) ), // An array of arrays of structs. makeCase( - [ - [ - SimpleStruct(foo: "bar") - ], - [ - SimpleStruct(foo: "baz") - ], - ], + [[SimpleStruct(foo: "bar")], [SimpleStruct(foo: "baz")]], .array([ - .array([ - .dictionary([ - "foo": .primitive(.string("bar")) - ]) - ]), - .array([ - .dictionary([ - "foo": .primitive(.string("baz")) - ]) - ]), + .array([.dictionary(["foo": .primitive(.string("bar"))])]), + .array([.dictionary(["foo": .primitive(.string("baz"))])]), ]) ), // A simple dictionary of string -> int pairs. makeCase( ["one": 1, "two": 2], - .dictionary([ - "one": .primitive(.integer(1)), - "two": .primitive(.integer(2)), - ]) + .dictionary(["one": .primitive(.integer(1)), "two": .primitive(.integer(2))]) ), // A simple dictionary of string -> enum pairs. - makeCase( - ["one": SimpleEnum.bar], - .dictionary([ - "one": .primitive(.string("bar")) - ]) - ), + makeCase(["one": SimpleEnum.bar], .dictionary(["one": .primitive(.string("bar"))])), // A nested dictionary. makeCase( - [ - "A": ["one": 1, "two": 2], - "B": ["three": 3, "four": 4], - ], + ["A": ["one": 1, "two": 2], "B": ["three": 3, "four": 4]], .dictionary([ - "A": .dictionary([ - "one": .primitive(.integer(1)), - "two": .primitive(.integer(2)), - ]), - "B": .dictionary([ - "three": .primitive(.integer(3)), - "four": .primitive(.integer(4)), - ]), + "A": .dictionary(["one": .primitive(.integer(1)), "two": .primitive(.integer(2))]), + "B": .dictionary(["three": .primitive(.integer(3)), "four": .primitive(.integer(4))]), ]) ), // A dictionary of structs. makeCase( - [ - "barkey": SimpleStruct(foo: "bar"), - "bazkey": SimpleStruct(foo: "baz"), - ], + ["barkey": SimpleStruct(foo: "bar"), "bazkey": SimpleStruct(foo: "baz")], .dictionary([ - "barkey": .dictionary([ - "foo": .primitive(.string("bar")) - ]), - "bazkey": .dictionary([ - "foo": .primitive(.string("baz")) - ]), + "barkey": .dictionary(["foo": .primitive(.string("bar"))]), + "bazkey": .dictionary(["foo": .primitive(.string("baz"))]), ]) ), // An dictionary of dictionaries of structs. makeCase( - [ - "outBar": - [ - "inBar": SimpleStruct(foo: "bar") - ], - "outBaz": [ - "inBaz": SimpleStruct(foo: "baz") - ], - ], + ["outBar": ["inBar": SimpleStruct(foo: "bar")], "outBaz": ["inBaz": SimpleStruct(foo: "baz")]], .dictionary([ - "outBar": .dictionary([ - "inBar": .dictionary([ - "foo": .primitive(.string("bar")) - ]) - ]), - "outBaz": .dictionary([ - "inBaz": .dictionary([ - "foo": .primitive(.string("baz")) - ]) - ]), + "outBar": .dictionary(["inBar": .dictionary(["foo": .primitive(.string("bar"))])]), + "outBaz": .dictionary(["inBaz": .dictionary(["foo": .primitive(.string("baz"))])]), ]) ), ] let encoder = URIValueToNodeEncoder() for testCase in cases { let encodedNode = try encoder.encodeValue(testCase.value) - XCTAssertEqual( - encodedNode, - testCase.expectedNode, - file: testCase.file, - line: testCase.line - ) + XCTAssertEqual(encodedNode, testCase.expectedNode, file: testCase.file, line: testCase.line) } } } diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift index 33ede7b9..9bd8f3e8 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift @@ -17,12 +17,7 @@ import XCTest final class Test_URIParser: Test_Runtime { let testedVariants: [URICoderConfiguration] = [ - .formExplode, - .formUnexplode, - .simpleExplode, - .simpleUnexplode, - .formDataExplode, - .formDataUnexplode, + .formExplode, .formUnexplode, .simpleExplode, .simpleUnexplode, .formDataExplode, .formDataUnexplode, ] func testParsing() throws { @@ -36,9 +31,7 @@ final class Test_URIParser: Test_Runtime { formDataExplode: "empty=", formDataUnexplode: "empty=" ), - value: [ - "empty": [""] - ] + value: ["empty": [""]] ), makeCase( .init( @@ -60,9 +53,7 @@ final class Test_URIParser: Test_Runtime { formDataExplode: "who=fred", formDataUnexplode: "who=fred" ), - value: [ - "who": ["fred"] - ] + value: ["who": ["fred"]] ), makeCase( .init( @@ -73,28 +64,18 @@ final class Test_URIParser: Test_Runtime { formDataExplode: "hello=Hello+World", formDataUnexplode: "hello=Hello+World" ), - value: [ - "hello": ["Hello World"] - ] + value: ["hello": ["Hello World"]] ), makeCase( .init( formExplode: "list=red&list=green&list=blue", formUnexplode: "list=red,green,blue", - simpleExplode: .custom( - "red,green,blue", - value: ["": ["red", "green", "blue"]] - ), - simpleUnexplode: .custom( - "red,green,blue", - value: ["": ["red", "green", "blue"]] - ), + simpleExplode: .custom("red,green,blue", value: ["": ["red", "green", "blue"]]), + simpleUnexplode: .custom("red,green,blue", value: ["": ["red", "green", "blue"]]), formDataExplode: "list=red&list=green&list=blue", formDataUnexplode: "list=red,green,blue" ), - value: [ - "list": ["red", "green", "blue"] - ] + value: ["list": ["red", "green", "blue"]] ), makeCase( .init( @@ -114,22 +95,12 @@ final class Test_URIParser: Test_Runtime { value: ["keys": ["comma", ",", "dot", ".", "semi", ";"]] ) ), - value: [ - "semi": [";"], - "dot": ["."], - "comma": [","], - ] + value: ["semi": [";"], "dot": ["."], "comma": [","]] ), ] for testCase in cases { - func testVariant( - _ variant: Case.Variant, - _ input: Case.Variants.Input - ) throws { - var parser = URIParser( - configuration: variant.config, - data: input.string[...] - ) + func testVariant(_ variant: Case.Variant, _ input: Case.Variants.Input) throws { + var parser = URIParser(configuration: variant.config, data: input.string[...]) let parsedNode = try parser.parseRoot() XCTAssertEqual( parsedNode, @@ -156,30 +127,12 @@ extension Test_URIParser { var name: String var config: URICoderConfiguration - static let formExplode: Self = .init( - name: "formExplode", - config: .formExplode - ) - static let formUnexplode: Self = .init( - name: "formUnexplode", - config: .formUnexplode - ) - static let simpleExplode: Self = .init( - name: "simpleExplode", - config: .simpleExplode - ) - static let simpleUnexplode: Self = .init( - name: "simpleUnexplode", - config: .simpleUnexplode - ) - static let formDataExplode: Self = .init( - name: "formDataExplode", - config: .formDataExplode - ) - static let formDataUnexplode: Self = .init( - name: "formDataUnexplode", - config: .formDataUnexplode - ) + static let formExplode: Self = .init(name: "formExplode", config: .formExplode) + static let formUnexplode: Self = .init(name: "formUnexplode", config: .formUnexplode) + static let simpleExplode: Self = .init(name: "simpleExplode", config: .simpleExplode) + static let simpleUnexplode: Self = .init(name: "simpleUnexplode", config: .simpleUnexplode) + static let formDataExplode: Self = .init(name: "formDataExplode", config: .formDataExplode) + static let formDataUnexplode: Self = .init(name: "formDataUnexplode", config: .formDataUnexplode) } struct Variants { @@ -214,17 +167,7 @@ extension Test_URIParser { var file: StaticString = #file var line: UInt = #line } - func makeCase( - _ variants: Case.Variants, - value: URIParsedNode, - file: StaticString = #file, - line: UInt = #line - ) -> Case { - .init( - variants: variants, - value: value, - file: file, - line: line - ) - } + func makeCase(_ variants: Case.Variants, value: URIParsedNode, file: StaticString = #file, line: UInt = #line) + -> Case + { .init(variants: variants, value: value, file: file, line: line) } } diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift b/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift index 1e25109b..f93fabed 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Serialization/Test_URISerializer.swift @@ -17,12 +17,7 @@ import XCTest final class Test_URISerializer: Test_Runtime { let testedVariants: [URICoderConfiguration] = [ - .formExplode, - .formUnexplode, - .simpleExplode, - .simpleUnexplode, - .formDataExplode, - .formDataUnexplode, + .formExplode, .formUnexplode, .simpleExplode, .simpleUnexplode, .formDataExplode, .formDataUnexplode, ] func testSerializing() throws { @@ -100,11 +95,7 @@ final class Test_URISerializer: Test_Runtime { ) ), makeCase( - value: .array([ - .primitive(.string("red")), - .primitive(.string("green")), - .primitive(.string("blue")), - ]), + value: .array([.primitive(.string("red")), .primitive(.string("green")), .primitive(.string("blue"))]), key: "list", .init( formExplode: "list=red&list=green&list=blue", @@ -117,8 +108,7 @@ final class Test_URISerializer: Test_Runtime { ), makeCase( value: .dictionary([ - "semi": .primitive(.string(";")), - "dot": .primitive(.string(".")), + "semi": .primitive(.string(";")), "dot": .primitive(.string(".")), "comma": .primitive(.string(",")), ]), key: "keys", @@ -135,10 +125,7 @@ final class Test_URISerializer: Test_Runtime { for testCase in cases { func testVariant(_ variant: Case.Variant, _ expectedString: String) throws { var serializer = URISerializer(configuration: variant.config) - let encodedString = try serializer.serializeNode( - testCase.value, - forKey: testCase.key - ) + let encodedString = try serializer.serializeNode(testCase.value, forKey: testCase.key) XCTAssertEqual( encodedString, expectedString, @@ -163,30 +150,12 @@ extension Test_URISerializer { var name: String var config: URICoderConfiguration - static let formExplode: Self = .init( - name: "formExplode", - config: .formExplode - ) - static let formUnexplode: Self = .init( - name: "formUnexplode", - config: .formUnexplode - ) - static let simpleExplode: Self = .init( - name: "simpleExplode", - config: .simpleExplode - ) - static let simpleUnexplode: Self = .init( - name: "simpleUnexplode", - config: .simpleUnexplode - ) - static let formDataExplode: Self = .init( - name: "formDataExplode", - config: .formDataExplode - ) - static let formDataUnexplode: Self = .init( - name: "formDataUnexplode", - config: .formDataUnexplode - ) + static let formExplode: Self = .init(name: "formExplode", config: .formExplode) + static let formUnexplode: Self = .init(name: "formUnexplode", config: .formUnexplode) + static let simpleExplode: Self = .init(name: "simpleExplode", config: .simpleExplode) + static let simpleUnexplode: Self = .init(name: "simpleUnexplode", config: .simpleUnexplode) + static let formDataExplode: Self = .init(name: "formDataExplode", config: .formDataExplode) + static let formDataUnexplode: Self = .init(name: "formDataUnexplode", config: .formDataUnexplode) } struct Variants { var formExplode: String @@ -208,13 +177,5 @@ extension Test_URISerializer { _ variants: Case.Variants, file: StaticString = #file, line: UInt = #line - ) -> Case { - .init( - value: value, - key: key, - variants: variants, - file: file, - line: line - ) - } + ) -> Case { .init(value: value, key: key, variants: variants, file: file, line: line) } } diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift index 44c62520..0487c756 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift @@ -30,9 +30,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { var maybeFoo: String? } - struct TrivialStruct: Codable, Equatable { - var foo: String - } + struct TrivialStruct: Codable, Equatable { var foo: String } enum SimpleEnum: String, Codable, Equatable { case red @@ -54,21 +52,15 @@ final class Test_URICodingRoundtrip: Test_Runtime { do { let container = try decoder.singleValueContainer() value1 = try container.decode(Foundation.Date.self) - } catch { - errors.append(error) - } + } catch { errors.append(error) } do { let container = try decoder.singleValueContainer() value2 = try container.decode(SimpleEnum.self) - } catch { - errors.append(error) - } + } catch { errors.append(error) } do { let container = try decoder.singleValueContainer() value3 = try container.decode(TrivialStruct.self) - } catch { - errors.append(error) - } + } catch { errors.append(error) } try DecodingError.verifyAtLeastOneSchemaIsNotNil( [value1, value2, value3], type: Self.self, @@ -206,10 +198,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { // A simple array of dates. try _test( - [ - Date(timeIntervalSince1970: 1_692_948_899), - Date(timeIntervalSince1970: 1_692_948_901), - ], + [Date(timeIntervalSince1970: 1_692_948_899), Date(timeIntervalSince1970: 1_692_948_901)], key: "list", .init( formExplode: "list=2023-08-25T07%3A34%3A59Z&list=2023-08-25T07%3A35%3A01Z", @@ -251,13 +240,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { // A struct. try _test( - SimpleStruct( - foo: "hi!", - bar: 24, - color: .red, - empty: "", - date: Date(timeIntervalSince1970: 1_692_948_899) - ), + SimpleStruct(foo: "hi!", bar: 24, color: .red, empty: "", date: Date(timeIntervalSince1970: 1_692_948_899)), key: "keys", .init( formExplode: "bar=24&color=red&date=2023-08-25T07%3A34%3A59Z&empty=&foo=hi%21", @@ -272,9 +255,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { // A struct with a custom Codable implementation that forwards // decoding to nested values. try _test( - AnyOf( - value1: Date(timeIntervalSince1970: 1_674_036_251) - ), + AnyOf(value1: Date(timeIntervalSince1970: 1_674_036_251)), key: "root", .init( formExplode: "root=2023-01-18T10%3A04%3A11Z", @@ -286,9 +267,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { ) ) try _test( - AnyOf( - value2: .green - ), + AnyOf(value2: .green), key: "root", .init( formExplode: "root=green", @@ -300,9 +279,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { ) ) try _test( - AnyOf( - value3: .init(foo: "bar") - ), + AnyOf(value3: .init(foo: "bar")), key: "root", .init( formExplode: "foo=bar", @@ -362,30 +339,12 @@ final class Test_URICodingRoundtrip: Test_Runtime { var name: String var configuration: URICoderConfiguration - static let formExplode: Self = .init( - name: "formExplode", - configuration: .formExplode - ) - static let formUnexplode: Self = .init( - name: "formUnexplode", - configuration: .formUnexplode - ) - static let simpleExplode: Self = .init( - name: "simpleExplode", - configuration: .simpleExplode - ) - static let simpleUnexplode: Self = .init( - name: "simpleUnexplode", - configuration: .simpleUnexplode - ) - static let formDataExplode: Self = .init( - name: "formDataExplode", - configuration: .formDataExplode - ) - static let formDataUnexplode: Self = .init( - name: "formDataUnexplode", - configuration: .formDataUnexplode - ) + static let formExplode: Self = .init(name: "formExplode", configuration: .formExplode) + static let formUnexplode: Self = .init(name: "formUnexplode", configuration: .formUnexplode) + static let simpleExplode: Self = .init(name: "simpleExplode", configuration: .simpleExplode) + static let simpleUnexplode: Self = .init(name: "simpleUnexplode", configuration: .simpleUnexplode) + static let formDataExplode: Self = .init(name: "formDataExplode", configuration: .formDataExplode) + static let formDataUnexplode: Self = .init(name: "formDataUnexplode", configuration: .formDataUnexplode) } struct Variants { @@ -398,13 +357,9 @@ final class Test_URICodingRoundtrip: Test_Runtime { self.customValue = customValue } - init(stringLiteral value: String) { - self.init(string: value, customValue: nil) - } + init(stringLiteral value: String) { self.init(string: value, customValue: nil) } - static func custom(_ string: String, value: T) -> Self { - .init(string: string, customValue: value) - } + static func custom(_ string: String, value: T) -> Self { .init(string: string, customValue: value) } } var formExplode: Input @@ -422,59 +377,19 @@ final class Test_URICodingRoundtrip: Test_Runtime { file: StaticString = #file, line: UInt = #line ) throws { - func testVariant( - name: String, - configuration: URICoderConfiguration, - variant: Variants.Input - ) throws { + func testVariant(name: String, configuration: URICoderConfiguration, variant: Variants.Input) throws { let encoder = URIEncoder(configuration: configuration) let encodedString = try encoder.encode(value, forKey: key) - XCTAssertEqual( - encodedString, - variant.string, - "Variant: \(name)", - file: file, - line: line - ) + XCTAssertEqual(encodedString, variant.string, "Variant: \(name)", file: file, line: line) let decoder = URIDecoder(configuration: configuration) - let decodedValue = try decoder.decode( - T.self, - forKey: key, - from: encodedString[...] - ) - XCTAssertEqual( - decodedValue, - variant.customValue ?? value, - "Variant: \(name)", - file: file, - line: line - ) + let decodedValue = try decoder.decode(T.self, forKey: key, from: encodedString[...]) + XCTAssertEqual(decodedValue, variant.customValue ?? value, "Variant: \(name)", file: file, line: line) } - try testVariant( - name: "formExplode", - configuration: .formExplode, - variant: variants.formExplode - ) - try testVariant( - name: "formUnexplode", - configuration: .formUnexplode, - variant: variants.formUnexplode - ) - try testVariant( - name: "simpleExplode", - configuration: .simpleExplode, - variant: variants.simpleExplode - ) - try testVariant( - name: "simpleUnexplode", - configuration: .simpleUnexplode, - variant: variants.simpleUnexplode - ) - try testVariant( - name: "formDataExplode", - configuration: .formDataExplode, - variant: variants.formDataExplode - ) + try testVariant(name: "formExplode", configuration: .formExplode, variant: variants.formExplode) + try testVariant(name: "formUnexplode", configuration: .formUnexplode, variant: variants.formUnexplode) + try testVariant(name: "simpleExplode", configuration: .simpleExplode, variant: variants.simpleExplode) + try testVariant(name: "simpleUnexplode", configuration: .simpleUnexplode, variant: variants.simpleUnexplode) + try testVariant(name: "formDataExplode", configuration: .formDataExplode, variant: variants.formDataExplode) try testVariant( name: "formDataUnexplode", configuration: .formDataUnexplode, From 9da9ad67b09d38e307d95ce15fabd9bd0769a80b Mon Sep 17 00:00:00 2001 From: PARAIPAN SORIN <51127880+PARAIPAN9@users.noreply.github.com> Date: Thu, 9 Nov 2023 23:57:41 +0200 Subject: [PATCH 02/20] Base64EncodedData initializer taking an array slice shouldn't have a label (#71) ### Motivation - Fixes [#369](https://github.com/apple/swift-openapi-generator/issues/368) ### Modifications - Remove init label argument from Base64EncodedData ### Result - The Base64EncodedData init will be called without the label. ### Test Plan - Adjust some tests from Test_OpenAPIValue --- .../Base/Base64EncodedData.swift | 18 +++++++++++++++++- .../Base/Test_OpenAPIValue.swift | 6 +++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Sources/OpenAPIRuntime/Base/Base64EncodedData.swift b/Sources/OpenAPIRuntime/Base/Base64EncodedData.swift index f5116c38..408e9f90 100644 --- a/Sources/OpenAPIRuntime/Base/Base64EncodedData.swift +++ b/Sources/OpenAPIRuntime/Base/Base64EncodedData.swift @@ -56,7 +56,23 @@ public struct Base64EncodedData: Sendable, Hashable { /// Initializes an instance of ``Base64EncodedData`` wrapping the provided slice of bytes. /// - Parameter data: The underlying bytes to wrap. + @available(*, deprecated, renamed: "init(_:)") + public init(data: ArraySlice) { self.data = data } + + /// Initializes an instance of ``Base64EncodedData`` wrapping the provided slice of bytes. + /// - Parameter data: The underlying bytes to wrap. + public init(_ data: ArraySlice) { self.data = data } + + /// Initializes an instance of ``Base64EncodedData`` wrapping the provided sequence of bytes. + /// - Parameter data: The underlying bytes to wrap. + public init(_ data: some Sequence) { self.init(ArraySlice(data)) } +} + +extension Base64EncodedData: ExpressibleByArrayLiteral { + /// Initializes an instance of ``Base64EncodedData`` with a sequence of bytes provided as an array literal. + /// - Parameter elements: The sequence of `UInt8` elements representing the underlying bytes. + public init(arrayLiteral elements: UInt8...) { self.init(elements) } } extension Base64EncodedData: Codable { @@ -74,7 +90,7 @@ extension Base64EncodedData: Codable { guard let data = Data(base64Encoded: base64EncodedString, options: options) else { throw RuntimeError.invalidBase64String(base64EncodedString) } - self.init(data: ArraySlice(data)) + self.init(data) } /// Encodes the binary data as a base64-encoded string. diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift index 080f5dd1..d95ee8c4 100644 --- a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift +++ b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift @@ -241,14 +241,14 @@ final class Test_OpenAPIValue: Test_Runtime { } func testEncoding_base64_success() throws { - let encodedData = Base64EncodedData(data: ArraySlice(testStructData)) + let encodedData = Base64EncodedData(testStructData) let JSONEncoded = try JSONEncoder().encode(encodedData) XCTAssertEqual(String(data: JSONEncoded, encoding: .utf8)!, testStructBase64EncodedString) } func testDecoding_base64_success() throws { - let encodedData = Base64EncodedData(data: ArraySlice(testStructData)) + let encodedData = Base64EncodedData(testStructData) // `testStructBase64EncodedString` quoted and base64-encoded again let JSONEncoded = Data(base64Encoded: "ImV5SnVZVzFsSWpvaVJteDFabVo2SW4wPSI=")! @@ -257,7 +257,7 @@ final class Test_OpenAPIValue: Test_Runtime { } func testEncodingDecodingRoundtrip_base64_success() throws { - let encodedData = Base64EncodedData(data: ArraySlice(testStructData)) + let encodedData = Base64EncodedData(testStructData) XCTAssertEqual( try JSONDecoder().decode(Base64EncodedData.self, from: JSONEncoder().encode(encodedData)), encodedData From cefdc80e08be81bdc436bbc28ffe71421bb93af4 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 15 Nov 2023 09:48:20 +0100 Subject: [PATCH 03/20] [Multipart] Introduce a bytes -> frames parser (#72) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Multipart] Introduce a bytes -> frames parser ### Motivation Start landing the parts of the multipart machinery that is unlikely to change as part of the multipart proposal that's finishing review tomorrow. ### Modifications Introduce a bytes -> frames parser and an async sequence that wraps it. A "frame" is either the full header fields section or a single chunk of a part body. ### Result We can now frame bytes of a multipart body. ### Test Plan Added unit tests for the state machine, the parser, and the async sequence. Reviewed by: simonjbeaumont Builds: ✔︎ pull request validation (5.10) - Build finished. ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (api breakage) - Build finished. ✔︎ pull request validation (docc test) - Build finished. ✔︎ pull request validation (integration test) - Build finished. ✔︎ pull request validation (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. https://github.com/apple/swift-openapi-runtime/pull/72 --- NOTICE.txt | 9 + .../Multipart/ByteUtilities.swift | 121 ++++++ .../MultipartBytesToFramesSequence.swift | 67 ++++ .../Multipart/MultipartInternalTypes.swift | 26 ++ .../Multipart/MultipartParser.swift | 350 ++++++++++++++++++ .../Test_MultipartBytesToFramesSequence.swift | 46 +++ .../Multipart/Test_MultipartParser.swift | 159 ++++++++ Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 21 ++ 8 files changed, 799 insertions(+) create mode 100644 Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift create mode 100644 Sources/OpenAPIRuntime/Multipart/MultipartBytesToFramesSequence.swift create mode 100644 Sources/OpenAPIRuntime/Multipart/MultipartInternalTypes.swift create mode 100644 Sources/OpenAPIRuntime/Multipart/MultipartParser.swift create mode 100644 Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBytesToFramesSequence.swift create mode 100644 Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartParser.swift diff --git a/NOTICE.txt b/NOTICE.txt index 7b160cf4..cd34ef6d 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -41,3 +41,12 @@ This product contains coder implementations inspired by swift-http-structured-he * https://www.apache.org/licenses/LICENSE-2.0 * HOMEPAGE: * https://github.com/apple/swift-http-structured-headers + +--- + +This product contains header character set validation logic inspired by swift-http-types. + + * LICENSE (Apache License 2.0): + * https://www.apache.org/licenses/LICENSE-2.0 + * HOMEPAGE: + * https://github.com/apple/swift-http-types diff --git a/Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift b/Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift new file mode 100644 index 00000000..05c47f1c --- /dev/null +++ b/Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift @@ -0,0 +1,121 @@ +//===----------------------------------------------------------------------===// +// +// 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 namespace of utilities for byte parsers and serializers. +enum ASCII { + + /// The dash `-` character. + static let dash: UInt8 = 0x2d + + /// The carriage return `` character. + static let cr: UInt8 = 0x0d + + /// The line feed `` character. + static let lf: UInt8 = 0x0a + + /// The colon `:` character. + static let colon: UInt8 = 0x3a + + /// The space ` ` character. + static let space: UInt8 = 0x20 + + /// The horizontal tab `` character. + static let tab: UInt8 = 0x09 + + /// Two dash characters. + static let dashes: [UInt8] = [dash, dash] + + /// The `` character follow by the `` character. + static let crlf: [UInt8] = [cr, lf] + + /// The characters that represent optional whitespace (OWS). + static let optionalWhitespace: Set = [space, tab] + + /// Checks whether the provided byte can appear in a header field name. + /// - Parameter byte: The byte to check. + /// - Returns: A Boolean value; `true` if the byte is valid in a header field + /// name, `false` otherwise. + static func isValidHeaderFieldNameByte(_ byte: UInt8) -> Bool { + // Copied from swift-http-types, because we create HTTPField.Name from these anyway later. + switch byte { + case 0x21, 0x23, 0x24, 0x25, 0x26, 0x27, 0x2A, 0x2B, 0x2D, 0x2E, 0x5E, 0x5F, 0x60, 0x7C, 0x7E: return true + case 0x30...0x39, 0x41...0x5A, 0x61...0x7A: // DIGHT, ALPHA + return true + default: return false + } + } +} + +/// A value returned by the `firstIndexAfterPrefix` method. +enum FirstIndexAfterPrefixResult { + + /// The index after the end of the prefix match. + case index(C.Index) + + /// Matched all characters so far, but reached the end of self before matching all. + /// When more data is fetched, it's possible this will fully match. + case reachedEndOfSelf + + /// The character at the provided index does not match the expected character. + case unexpectedPrefix(C.Index) +} + +extension RandomAccessCollection where Element: Equatable { + + /// Verifies that the elements match the provided sequence and returns the first index past the match. + /// - Parameter expectedElements: The elements to match against. + /// - Returns: The result. + func firstIndexAfterPrefix(_ expectedElements: some Sequence) -> FirstIndexAfterPrefixResult { + var index = startIndex + for expectedElement in expectedElements { + guard index < endIndex else { return .reachedEndOfSelf } + guard self[index] == expectedElement else { return .unexpectedPrefix(index) } + formIndex(after: &index) + } + return .index(index) + } +} + +/// A value returned by the `longestMatch` method. +enum LongestMatchResult { + + /// No match found at any position in self. + case noMatch + + /// Found a prefix match but reached the end of self. + /// Provides the index of the first matching character. + /// When more data is fetched, this might become a full match. + case prefixMatch(fromIndex: C.Index) + + /// Found a full match within self at the provided range. + case fullMatch(Range) +} + +extension RandomAccessCollection where Element: Equatable { + + /// Returns the longest match found within the sequence. + /// - Parameter expectedElements: The elements to match in the sequence. + /// - Returns: The result. + func longestMatch(_ expectedElements: some Sequence) -> LongestMatchResult { + var index = startIndex + while index < endIndex { + switch self[index...].firstIndexAfterPrefix(expectedElements) { + case .index(let end): return .fullMatch(index..: Sendable +where Upstream.Element == ArraySlice { + + /// The source of byte chunks. + var upstream: Upstream + + /// The boundary string used to separate multipart parts. + var boundary: String +} + +extension MultipartBytesToFramesSequence: AsyncSequence { + + /// The type of element produced by this asynchronous sequence. + typealias Element = MultipartFrame + + /// Creates the asynchronous iterator that produces elements of this + /// asynchronous sequence. + /// + /// - Returns: An instance of the `AsyncIterator` type used to produce + /// elements of the asynchronous sequence. + func makeAsyncIterator() -> Iterator { + Iterator(upstream: upstream.makeAsyncIterator(), boundary: boundary) + } + + /// An iterator that pulls byte chunks from the upstream iterator and provides + /// parsed multipart frames. + struct Iterator: AsyncIteratorProtocol + where UpstreamIterator.Element == ArraySlice { + /// The iterator that provides the byte chunks. + private var upstream: UpstreamIterator + + /// The multipart frame parser. + private var parser: MultipartParser + /// Creates a new iterator from the provided source of byte chunks and a boundary string. + /// - Parameters: + /// - upstream: The iterator that provides the byte chunks. + /// - boundary: The boundary separating the multipart parts. + init(upstream: UpstreamIterator, boundary: String) { + self.upstream = upstream + self.parser = .init(boundary: boundary) + } + + /// Asynchronously advances to the next element and returns it, or ends the + /// sequence if there is no next element. + /// + /// - Returns: The next element, if it exists, or `nil` to signal the end of + /// the sequence. + mutating func next() async throws -> MultipartFrame? { try await parser.next { try await upstream.next() } } + } +} diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartInternalTypes.swift b/Sources/OpenAPIRuntime/Multipart/MultipartInternalTypes.swift new file mode 100644 index 00000000..49e57b9f --- /dev/null +++ b/Sources/OpenAPIRuntime/Multipart/MultipartInternalTypes.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// 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 HTTPTypes + +/// A frame of a multipart message, either the whole header fields +/// section or a chunk of the body bytes. +enum MultipartFrame: Sendable, Hashable { + + /// The header fields section. + case headerFields(HTTPFields) + + /// One byte chunk of the part's body. + case bodyChunk(ArraySlice) +} diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartParser.swift b/Sources/OpenAPIRuntime/Multipart/MultipartParser.swift new file mode 100644 index 00000000..87267a6c --- /dev/null +++ b/Sources/OpenAPIRuntime/Multipart/MultipartParser.swift @@ -0,0 +1,350 @@ +//===----------------------------------------------------------------------===// +// +// 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 +import HTTPTypes + +/// A parser of mutlipart frames from bytes. +struct MultipartParser { + + /// The underlying state machine. + private var stateMachine: StateMachine + + /// Creates a new parser. + /// - Parameter boundary: The boundary that separates parts. + init(boundary: String) { self.stateMachine = .init(boundary: boundary) } + + /// Parses the next frame. + /// - Parameter fetchChunk: A closure that is called when the parser + /// needs more bytes to parse the next frame. + /// - Returns: A parsed frame, or nil at the end of the message. + /// - Throws: When a parsing error is encountered. + mutating func next(_ fetchChunk: () async throws -> ArraySlice?) async throws -> MultipartFrame? { + while true { + switch stateMachine.readNextPart() { + case .none: continue + case .emitError(let actionError): throw ParserError(error: actionError) + case .returnNil: return nil + case .emitHeaderFields(let httpFields): return .headerFields(httpFields) + case .emitBodyChunk(let bodyChunk): return .bodyChunk(bodyChunk) + case .needsMore: + let chunk = try await fetchChunk() + switch stateMachine.receivedChunk(chunk) { + case .none: continue + case .returnNil: return nil + case .emitError(let actionError): throw ParserError(error: actionError) + } + } + } + } +} +extension MultipartParser { + + /// An error thrown by the parser. + struct ParserError: Swift.Error, CustomStringConvertible, LocalizedError { + + /// The underlying error emitted by the state machine. + let error: MultipartParser.StateMachine.ActionError + + var description: String { + switch error { + case .invalidInitialBoundary: return "Invalid initial boundary." + case .invalidCRLFAtStartOfHeaderField: return "Invalid CRLF at the start of a header field." + case .missingColonAfterHeaderName: return "Missing colon after header field name." + case .invalidCharactersInHeaderFieldName: return "Invalid characters in a header field name." + case .incompleteMultipartMessage: return "Incomplete multipart message." + case .receivedChunkWhenFinished: return "Received a chunk after being finished." + } + } + + var errorDescription: String? { description } + } +} + +extension MultipartParser { + + /// A state machine representing the byte to multipart frame parser. + struct StateMachine { + + /// The possible states of the state machine. + enum State: Hashable { + + /// Has not yet fully parsed the initial boundary. + case parsingInitialBoundary([UInt8]) + + /// A substate when parsing a part. + enum PartState: Hashable { + + /// Accumulating part headers. + case parsingHeaderFields(HTTPFields) + + /// Forwarding body chunks. + case parsingBody + } + + /// Is parsing a part. + case parsingPart([UInt8], PartState) + + /// Finished, the terminal state. + case finished + + /// Helper state to avoid copy-on-write copies. + case mutating + } + + /// The current state of the state machine. + private(set) var state: State + + /// The bytes of the boundary. + private let boundary: ArraySlice + + /// The bytes of the boundary with the double dash prepended. + private let dashDashBoundary: ArraySlice + + /// The bytes of the boundary prepended by CRLF + double dash. + private let crlfDashDashBoundary: ArraySlice + + /// Creates a new state machine. + /// - Parameter boundary: The boundary used to separate parts. + init(boundary: String) { + self.state = .parsingInitialBoundary([]) + self.boundary = ArraySlice(boundary.utf8) + self.dashDashBoundary = ASCII.dashes + self.boundary + self.crlfDashDashBoundary = ASCII.crlf + dashDashBoundary + } + + /// An error returned by the state machine. + enum ActionError: Hashable { + + /// The initial boundary is malformed. + case invalidInitialBoundary + + /// The expected CRLF at the start of a header is missing. + case invalidCRLFAtStartOfHeaderField + + /// A header field name contains an invalid character. + case invalidCharactersInHeaderFieldName + + /// The header field name is not followed by a colon. + case missingColonAfterHeaderName + + /// More bytes were received after completion. + case receivedChunkWhenFinished + + /// Ran out of bytes without the message being complete. + case incompleteMultipartMessage + } + + /// An action returned by the `readNextPart` method. + enum ReadNextPartAction: Hashable { + + /// No action, call `readNextPart` again. + case none + + /// Throw the provided error. + case emitError(ActionError) + + /// Return nil to the caller, no more frames. + case returnNil + + /// Emit a frame with the provided header fields. + case emitHeaderFields(HTTPFields) + + /// Emit a frame with the provided part body chunk. + case emitBodyChunk(ArraySlice) + + /// Needs more bytes to parse the next frame. + case needsMore + } + + /// Read the next part from the accumulated bytes. + /// - Returns: An action to perform. + mutating func readNextPart() -> ReadNextPartAction { + switch state { + case .mutating: preconditionFailure("Invalid state: \(state)") + case .finished: return .returnNil + case .parsingInitialBoundary(var buffer): + state = .mutating + // These first bytes must be the boundary already, otherwise this is a malformed multipart body. + switch buffer.firstIndexAfterPrefix(dashDashBoundary) { + case .index(let index): + buffer.removeSubrange(buffer.startIndex...Index + switch buffer.firstIndexAfterPrefix(ASCII.crlf) { + case .index(let index): indexAfterFirstCRLF = index + case .reachedEndOfSelf: + state = .parsingPart(buffer, .parsingHeaderFields(headerFields)) + return .needsMore + case .unexpectedPrefix: + state = .finished + return .emitError(.invalidCRLFAtStartOfHeaderField) + } + // If CRLF is here, this is the end of header fields section. + switch buffer[indexAfterFirstCRLF...].firstIndexAfterPrefix(ASCII.crlf) { + case .index(let index): + buffer.removeSubrange(buffer.startIndex...Index + // Check that what follows is a colon, otherwise this is a malformed header field line. + // Source: RFC 7230, section 3.2.4. + switch buffer[endHeaderNameIndex...].firstIndexAfterPrefix([ASCII.colon]) { + case .index(let index): startHeaderValueWithWhitespaceIndex = index + case .reachedEndOfSelf: + state = .parsingPart(buffer, .parsingHeaderFields(headerFields)) + return .needsMore + case .unexpectedPrefix: + state = .finished + return .emitError(.missingColonAfterHeaderName) + } + guard + let startHeaderValueIndex = buffer[startHeaderValueWithWhitespaceIndex...] + .firstIndex(where: { !ASCII.optionalWhitespace.contains($0) }) + else { + state = .parsingPart(buffer, .parsingHeaderFields(headerFields)) + return .needsMore + } + + // Find the CRLF first, then remove any trailing whitespace. + guard + let endHeaderValueWithWhitespaceRange = buffer[startHeaderValueIndex...] + .firstRange(of: ASCII.crlf) + else { + state = .parsingPart(buffer, .parsingHeaderFields(headerFields)) + return .needsMore + } + let headerFieldValueBytes = buffer[ + startHeaderValueIndex..?) -> ReceivedChunkAction { + switch state { + case .parsingInitialBoundary(var buffer): + guard let chunk else { return .emitError(.incompleteMultipartMessage) } + state = .mutating + buffer.append(contentsOf: chunk) + state = .parsingInitialBoundary(buffer) + return .none + case .parsingPart(var buffer, let part): + guard let chunk else { return .emitError(.incompleteMultipartMessage) } + state = .mutating + buffer.append(contentsOf: chunk) + state = .parsingPart(buffer, part) + return .none + case .finished: + guard chunk == nil else { return .emitError(.receivedChunkWhenFinished) } + return .returnNil + case .mutating: preconditionFailure("Invalid state: \(state)") + } + } + } +} diff --git a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBytesToFramesSequence.swift b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBytesToFramesSequence.swift new file mode 100644 index 00000000..7229e45b --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBytesToFramesSequence.swift @@ -0,0 +1,46 @@ +//===----------------------------------------------------------------------===// +// +// 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_MultipartBytesToFramesSequence: Test_Runtime { + func test() async throws { + var chunk = chunkFromStringLines([ + "--__abcd__", #"Content-Disposition: form-data; name="name""#, "", "24", "--__abcd__", + #"Content-Disposition: form-data; name="info""#, "", "{}", "--__abcd__--", + ]) + let next: () async throws -> ArraySlice? = { + if let first = chunk.first { + let out: ArraySlice = [first] + chunk = chunk.dropFirst() + return out + } else { + return nil + } + } + let upstream = HTTPBody(AsyncThrowingStream(unfolding: next), length: .unknown, iterationBehavior: .single) + let sequence = MultipartBytesToFramesSequence(upstream: upstream, boundary: "__abcd__") + var frames: [MultipartFrame] = [] + for try await frame in sequence { frames.append(frame) } + XCTAssertEqual( + frames, + [ + .headerFields([.contentDisposition: #"form-data; name="name""#]), .bodyChunk(chunkFromString("2")), + .bodyChunk(chunkFromString("4")), .headerFields([.contentDisposition: #"form-data; name="info""#]), + .bodyChunk(chunkFromString("{")), .bodyChunk(chunkFromString("}")), + ] + ) + } +} diff --git a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartParser.swift b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartParser.swift new file mode 100644 index 00000000..5587868b --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartParser.swift @@ -0,0 +1,159 @@ +//===----------------------------------------------------------------------===// +// +// 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_MultipartParser: Test_Runtime { + func test() async throws { + var chunk = chunkFromStringLines([ + "--__abcd__", #"Content-Disposition: form-data; name="name""#, "", "24", "--__abcd__", + #"Content-Disposition: form-data; name="info""#, "", "{}", "--__abcd__--", + ]) + var parser = MultipartParser(boundary: "__abcd__") + let next: () async throws -> ArraySlice? = { + if let first = chunk.first { + let out: ArraySlice = [first] + chunk = chunk.dropFirst() + return out + } else { + return nil + } + } + var frames: [MultipartFrame] = [] + while let frame = try await parser.next(next) { frames.append(frame) } + XCTAssertEqual( + frames, + [ + .headerFields([.contentDisposition: #"form-data; name="name""#]), .bodyChunk(chunkFromString("2")), + .bodyChunk(chunkFromString("4")), .headerFields([.contentDisposition: #"form-data; name="info""#]), + .bodyChunk(chunkFromString("{")), .bodyChunk(chunkFromString("}")), + ] + ) + } +} + +private func newStateMachine() -> MultipartParser.StateMachine { .init(boundary: "__abcd__") } + +final class Test_MultipartParserStateMachine: Test_Runtime { + + func testInvalidInitialBoundary() throws { + var stateMachine = newStateMachine() + XCTAssertEqual(stateMachine.receivedChunk(chunkFromString("invalid")), .none) + XCTAssertEqual(stateMachine.readNextPart(), .emitError(.invalidInitialBoundary)) + } + + func testHeaderFields() throws { + var stateMachine = newStateMachine() + XCTAssertEqual(stateMachine.receivedChunk(chunkFromString("--__ab")), .none) + XCTAssertEqual(stateMachine.readNextPart(), .needsMore) + XCTAssertEqual(stateMachine.state, .parsingInitialBoundary(bufferFromString("--__ab"))) + XCTAssertEqual(stateMachine.receivedChunk(chunkFromString("cd__", addCRLFs: 1)), .none) + XCTAssertEqual(stateMachine.readNextPart(), .none) + XCTAssertEqual(stateMachine.state, .parsingPart([0x0d, 0x0a], .parsingHeaderFields(.init()))) + XCTAssertEqual(stateMachine.receivedChunk(chunkFromString(#"Content-Disposi"#)), .none) + XCTAssertEqual( + stateMachine.state, + .parsingPart([0x0d, 0x0a] + bufferFromString(#"Content-Disposi"#), .parsingHeaderFields(.init())) + ) + XCTAssertEqual(stateMachine.readNextPart(), .needsMore) + XCTAssertEqual( + stateMachine.receivedChunk(chunkFromString(#"tion: form-data; name="name""#, addCRLFs: 2)), + .none + ) + XCTAssertEqual( + stateMachine.state, + .parsingPart( + [0x0d, 0x0a] + bufferFromString(#"Content-Disposition: form-data; name="name""#) + [ + 0x0d, 0x0a, 0x0d, 0x0a, + ], + .parsingHeaderFields(.init()) + ) + ) + // Reads the first header field. + XCTAssertEqual(stateMachine.readNextPart(), .none) + // Reads the end of the header fields section. + XCTAssertEqual( + stateMachine.readNextPart(), + .emitHeaderFields([.contentDisposition: #"form-data; name="name""#]) + ) + XCTAssertEqual(stateMachine.state, .parsingPart([], .parsingBody)) + } + + func testPartBody() throws { + var stateMachine = newStateMachine() + let chunk = chunkFromStringLines(["--__abcd__", #"Content-Disposition: form-data; name="name""#, "", "24"]) + XCTAssertEqual(stateMachine.receivedChunk(chunk), .none) + XCTAssertEqual(stateMachine.state, .parsingInitialBoundary(Array(chunk))) + // Parse the initial boundary and first header field. + for _ in 0..<2 { XCTAssertEqual(stateMachine.readNextPart(), .none) } + // Parse the end of header fields. + XCTAssertEqual( + stateMachine.readNextPart(), + .emitHeaderFields([.contentDisposition: #"form-data; name="name""#]) + ) + XCTAssertEqual(stateMachine.state, .parsingPart(bufferFromString(#"24"#) + [0x0d, 0x0a], .parsingBody)) + XCTAssertEqual(stateMachine.receivedChunk(chunkFromString(".42")), .none) + XCTAssertEqual( + stateMachine.state, + .parsingPart(bufferFromString("24") + [0x0d, 0x0a] + bufferFromString(".42"), .parsingBody) + ) + XCTAssertEqual( + stateMachine.readNextPart(), + .emitBodyChunk(bufferFromString("24") + [0x0d, 0x0a] + bufferFromString(".42")) + ) + XCTAssertEqual(stateMachine.state, .parsingPart([], .parsingBody)) + XCTAssertEqual(stateMachine.receivedChunk([0x0d, 0x0a] + chunkFromString("--__ab")), .none) + XCTAssertEqual(stateMachine.state, .parsingPart([0x0d, 0x0a] + chunkFromString("--__ab"), .parsingBody)) + XCTAssertEqual(stateMachine.readNextPart(), .needsMore) + XCTAssertEqual(stateMachine.receivedChunk(chunkFromString("cd__--", addCRLFs: 1)), .none) + XCTAssertEqual( + stateMachine.state, + .parsingPart([0x0d, 0x0a] + chunkFromString("--__abcd__--", addCRLFs: 1), .parsingBody) + ) + // Parse the final boundary. + XCTAssertEqual(stateMachine.readNextPart(), .none) + // Parse the trailing two dashes. + XCTAssertEqual(stateMachine.readNextPart(), .returnNil) + } + + func testTwoParts() throws { + var stateMachine = newStateMachine() + let chunk = chunkFromStringLines([ + "--__abcd__", #"Content-Disposition: form-data; name="name""#, "", "24", "--__abcd__", + #"Content-Disposition: form-data; name="info""#, "", "{}", "--__abcd__--", + ]) + XCTAssertEqual(stateMachine.receivedChunk(chunk), .none) + // Parse the initial boundary and first header field. + for _ in 0..<2 { XCTAssertEqual(stateMachine.readNextPart(), .none) } + // Parse the end of header fields. + XCTAssertEqual( + stateMachine.readNextPart(), + .emitHeaderFields([.contentDisposition: #"form-data; name="name""#]) + ) + // Parse the first part's body. + XCTAssertEqual(stateMachine.readNextPart(), .emitBodyChunk(chunkFromString("24"))) + // Parse the boundary. + XCTAssertEqual(stateMachine.readNextPart(), .none) + // Parse the end of header fields. + XCTAssertEqual( + stateMachine.readNextPart(), + .emitHeaderFields([.contentDisposition: #"form-data; name="info""#]) + ) + // Parse the second part's body. + XCTAssertEqual(stateMachine.readNextPart(), .emitBodyChunk(chunkFromString("{}"))) + // Parse the trailing two dashes. + XCTAssertEqual(stateMachine.readNextPart(), .returnNil) + } +} diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index 29666cc1..e2fe87c0 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -109,6 +109,27 @@ class Test_Runtime: XCTestCase { } } +/// Each line gets a CRLF added. Extra CRLFs are added after the last line's CRLF. +func chunkFromStringLines(_ strings: [String], addExtraCRLFs: Int = 0) -> ArraySlice { + var slice: ArraySlice = [] + for string in strings { slice.append(contentsOf: chunkFromString(string, addCRLFs: 1)) } + slice.append(contentsOf: chunkFromString("", addCRLFs: addExtraCRLFs)) + return slice +} + +func chunkFromString(_ string: String, addCRLFs: Int = 0) -> ArraySlice { + var slice = ArraySlice(string.utf8) + for _ in 0.. [UInt8] { Array(string.utf8) } + +extension ArraySlice { + mutating func append(_ string: String) { append(contentsOf: chunkFromString(string)) } + mutating func appendCRLF() { append(contentsOf: [0x0d, 0x0a]) } +} + struct TestError: Error, Equatable {} struct MockMiddleware: ClientMiddleware, ServerMiddleware { From ce4fc058f4a2e81bd21aee53bde418a1c6c76470 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 16 Nov 2023 09:40:13 +0100 Subject: [PATCH 04/20] Add README badges (#70) ### Motivation Surface the Swift version and platform support status from Swift Package Index. ### Modifications Added badges, plus a quick link to the docc docs, to the top of the README. ### Result Easier to quickly see our support matrix, plus the quick link to docs. ### Test Plan Previewed locally. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index d54d870b..5fd6a69a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # Swift OpenAPI Generator Runtime +[![](https://img.shields.io/badge/docc-read_documentation-blue)](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation) +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fapple%2Fswift-openapi-runtime%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/apple/swift-openapi-runtime) +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fapple%2Fswift-openapi-runtime%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/apple/swift-openapi-runtime) + This library provides common abstractions and helper functions used by the client and server code generated by [Swift OpenAPI Generator][0]. ## Overview From 94c1b30e40a38b8cf6cbad2fdd41628c53bb6995 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 16 Nov 2023 15:27:35 +0100 Subject: [PATCH 05/20] [Multipart] Add the frame -> bytes serializer (#73) ### Motivation Second state machine, doing the inverse of #72, serializes frames into bytes. ### Modifications - A new state machine. - A new serializer wrapping the state machine. - An async sequence wrapping the serializer. ### Result We can now serialize multipart frames into bytes. ### Test Plan Unit tests for all 3 layers. --- .../Multipart/ByteUtilities.swift | 5 +- .../MultipartFramesToBytesSequence.swift | 71 +++++ .../Multipart/MultipartParser.swift | 2 +- .../Multipart/MultipartSerializer.swift | 260 ++++++++++++++++++ .../Test_MultipartBytesToFramesSequence.swift | 14 +- .../Test_MultipartFramesToBytesSequence.swift | 36 +++ .../Multipart/Test_MultipartSerializer.swift | 79 ++++++ Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 59 ++++ 8 files changed, 513 insertions(+), 13 deletions(-) create mode 100644 Sources/OpenAPIRuntime/Multipart/MultipartFramesToBytesSequence.swift create mode 100644 Sources/OpenAPIRuntime/Multipart/MultipartSerializer.swift create mode 100644 Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartFramesToBytesSequence.swift create mode 100644 Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartSerializer.swift diff --git a/Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift b/Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift index 05c47f1c..9ae1c6a5 100644 --- a/Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift +++ b/Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift @@ -36,9 +36,12 @@ enum ASCII { /// Two dash characters. static let dashes: [UInt8] = [dash, dash] - /// The `` character follow by the `` character. + /// The `` character followed by the `` character. static let crlf: [UInt8] = [cr, lf] + /// The colon character followed by the space character. + static let colonSpace: [UInt8] = [colon, space] + /// The characters that represent optional whitespace (OWS). static let optionalWhitespace: Set = [space, tab] diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartFramesToBytesSequence.swift b/Sources/OpenAPIRuntime/Multipart/MultipartFramesToBytesSequence.swift new file mode 100644 index 00000000..e1d55542 --- /dev/null +++ b/Sources/OpenAPIRuntime/Multipart/MultipartFramesToBytesSequence.swift @@ -0,0 +1,71 @@ +//===----------------------------------------------------------------------===// +// +// 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 HTTPTypes + +/// A sequence that serializes multipart frames into bytes. +struct MultipartFramesToBytesSequence: Sendable +where Upstream.Element == MultipartFrame { + + /// The source of multipart frames. + var upstream: Upstream + + /// The boundary string used to separate multipart parts. + var boundary: String +} + +extension MultipartFramesToBytesSequence: AsyncSequence { + + /// The type of element produced by this asynchronous sequence. + typealias Element = ArraySlice + + /// Creates the asynchronous iterator that produces elements of this + /// asynchronous sequence. + /// + /// - Returns: An instance of the `AsyncIterator` type used to produce + /// elements of the asynchronous sequence. + func makeAsyncIterator() -> Iterator { + Iterator(upstream: upstream.makeAsyncIterator(), boundary: boundary) + } + + /// An iterator that pulls frames from the upstream iterator and provides + /// serialized byte chunks. + struct Iterator: AsyncIteratorProtocol + where UpstreamIterator.Element == MultipartFrame { + + /// The iterator that provides the multipart frames. + private var upstream: UpstreamIterator + + /// The multipart frame serializer. + private var serializer: MultipartSerializer + + /// Creates a new iterator from the provided source of frames and a boundary string. + /// - Parameters: + /// - upstream: The iterator that provides the multipart frames. + /// - boundary: The boundary separating the multipart parts. + init(upstream: UpstreamIterator, boundary: String) { + self.upstream = upstream + self.serializer = .init(boundary: boundary) + } + + /// Asynchronously advances to the next element and returns it, or ends the + /// sequence if there is no next element. + /// + /// - Returns: The next element, if it exists, or `nil` to signal the end of + /// the sequence. + mutating func next() async throws -> ArraySlice? { + try await serializer.next { try await upstream.next() } + } + } +} diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartParser.swift b/Sources/OpenAPIRuntime/Multipart/MultipartParser.swift index 87267a6c..d98db13e 100644 --- a/Sources/OpenAPIRuntime/Multipart/MultipartParser.swift +++ b/Sources/OpenAPIRuntime/Multipart/MultipartParser.swift @@ -15,7 +15,7 @@ import Foundation import HTTPTypes -/// A parser of mutlipart frames from bytes. +/// A parser of multipart frames from bytes. struct MultipartParser { /// The underlying state machine. diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartSerializer.swift b/Sources/OpenAPIRuntime/Multipart/MultipartSerializer.swift new file mode 100644 index 00000000..8f744784 --- /dev/null +++ b/Sources/OpenAPIRuntime/Multipart/MultipartSerializer.swift @@ -0,0 +1,260 @@ +//===----------------------------------------------------------------------===// +// +// 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 +import HTTPTypes + +/// A serializer of multipart frames into bytes. +struct MultipartSerializer { + + /// The boundary that separates parts. + private let boundary: ArraySlice + + /// The underlying state machine. + private var stateMachine: StateMachine + + /// The buffer of bytes ready to be written out. + private var outBuffer: [UInt8] + + /// Creates a new serializer. + /// - Parameter boundary: The boundary that separates parts. + init(boundary: String) { + self.boundary = ArraySlice(boundary.utf8) + self.stateMachine = .init() + self.outBuffer = [] + } + /// Requests the next byte chunk. + /// - Parameter fetchFrame: A closure that is called when the serializer is ready to serialize the next frame. + /// - Returns: A byte chunk. + /// - Throws: When a serialization error is encountered. + mutating func next(_ fetchFrame: () async throws -> MultipartFrame?) async throws -> ArraySlice? { + + func flushedBytes() -> ArraySlice { + let outChunk = ArraySlice(outBuffer) + outBuffer.removeAll(keepingCapacity: true) + return outChunk + } + + while true { + switch stateMachine.next() { + case .returnNil: return nil + case .emitStart: + emitStart() + return flushedBytes() + case .needsMore: + let frame = try await fetchFrame() + switch stateMachine.receivedFrame(frame) { + case .returnNil: return nil + case .emitEvents(let events): + for event in events { + switch event { + case .headerFields(let headerFields): emitHeaders(headerFields) + case .bodyChunk(let chunk): emitBodyChunk(chunk) + case .endOfPart: emitEndOfPart() + case .start: emitStart() + case .end: emitEnd() + } + } + return flushedBytes() + case .emitError(let error): throw SerializerError(error: error) + } + } + } + } +} + +extension MultipartSerializer { + + /// An error thrown by the serializer. + struct SerializerError: Swift.Error, CustomStringConvertible, LocalizedError { + + /// The underlying error emitted by the state machine. + var error: StateMachine.ActionError + + var description: String { + switch error { + case .noHeaderFieldsAtStart: return "No header fields found at the start of the multipart body." + } + } + + var errorDescription: String? { description } + } +} + +extension MultipartSerializer { + + /// Writes the provided header fields into the buffer. + /// - Parameter headerFields: The header fields to serialize. + private mutating func emitHeaders(_ headerFields: HTTPFields) { + outBuffer.append(contentsOf: ASCII.crlf) + let sortedHeaders = headerFields.sorted { a, b in a.name.canonicalName < b.name.canonicalName } + for headerField in sortedHeaders { + outBuffer.append(contentsOf: headerField.name.canonicalName.utf8) + outBuffer.append(contentsOf: ASCII.colonSpace) + outBuffer.append(contentsOf: headerField.value.utf8) + outBuffer.append(contentsOf: ASCII.crlf) + } + outBuffer.append(contentsOf: ASCII.crlf) + } + + /// Writes the part body chunk into the buffer. + /// - Parameter bodyChunk: The body chunk to write. + private mutating func emitBodyChunk(_ bodyChunk: ArraySlice) { outBuffer.append(contentsOf: bodyChunk) } + + /// Writes an end of part boundary into the buffer. + private mutating func emitEndOfPart() { + outBuffer.append(contentsOf: ASCII.crlf) + outBuffer.append(contentsOf: ASCII.dashes) + outBuffer.append(contentsOf: boundary) + } + + /// Writes the start boundary into the buffer. + private mutating func emitStart() { + outBuffer.append(contentsOf: ASCII.dashes) + outBuffer.append(contentsOf: boundary) + } + + /// Writes the end double dash to the buffer. + private mutating func emitEnd() { + outBuffer.append(contentsOf: ASCII.dashes) + outBuffer.append(contentsOf: ASCII.crlf) + outBuffer.append(contentsOf: ASCII.crlf) + } +} + +extension MultipartSerializer { + + /// A state machine representing the multipart frame serializer. + struct StateMachine { + + /// The possible states of the state machine. + enum State: Hashable { + + /// Has not yet written any bytes. + case initial + + /// Emitted start, but no frames yet. + case emittedStart + + /// Finished, the terminal state. + case finished + + /// Last emitted a header fields frame. + case emittedHeaders + + /// Last emitted a part body chunk frame. + case emittedBodyChunk + } + + /// The current state of the state machine. + private(set) var state: State + + /// Creates a new state machine. + init() { self.state = .initial } + + /// An error returned by the state machine. + enum ActionError: Hashable { + + /// The first frame from upstream was not a header fields frame. + case noHeaderFieldsAtStart + } + + /// An action returned by the `next` method. + enum NextAction: Hashable { + + /// Return nil to the caller, no more bytes. + case returnNil + + /// Emit the initial boundary. + case emitStart + + /// Ready for the next frame. + case needsMore + } + + /// Read the next byte chunk serialized from upstream frames. + /// - Returns: An action to perform. + mutating func next() -> NextAction { + switch state { + case .initial: + state = .emittedStart + return .emitStart + case .finished: return .returnNil + case .emittedStart, .emittedHeaders, .emittedBodyChunk: return .needsMore + } + } + + /// An event to serialize to bytes. + enum Event: Hashable { + + /// The header fields of a part. + case headerFields(HTTPFields) + + /// A byte chunk of a part. + case bodyChunk(ArraySlice) + + /// A boundary between parts. + case endOfPart + + /// The initial boundary. + case start + + /// The final dashes. + case end + } + + /// An action returned by the `receivedFrame` method. + enum ReceivedFrameAction: Hashable { + + /// Return nil to the caller, no more bytes. + case returnNil + + /// Write the provided events as bytes. + case emitEvents([Event]) + + /// Throw the provided error. + case emitError(ActionError) + } + + /// Ingest the provided frame. + /// - Parameter frame: A new frame. If `nil`, then the source of frames is finished. + /// - Returns: An action to perform. + mutating func receivedFrame(_ frame: MultipartFrame?) -> ReceivedFrameAction { + switch state { + case .initial: preconditionFailure("Invalid state: \(state)") + case .finished: return .returnNil + case .emittedStart, .emittedHeaders, .emittedBodyChunk: break + } + switch (state, frame) { + case (.initial, _), (.finished, _): preconditionFailure("Already handled above.") + case (_, .none): + state = .finished + return .emitEvents([.endOfPart, .end]) + case (.emittedStart, .headerFields(let headerFields)): + state = .emittedHeaders + return .emitEvents([.headerFields(headerFields)]) + case (.emittedStart, .bodyChunk): + state = .finished + return .emitError(.noHeaderFieldsAtStart) + case (.emittedHeaders, .headerFields(let headerFields)), + (.emittedBodyChunk, .headerFields(let headerFields)): + state = .emittedHeaders + return .emitEvents([.endOfPart, .headerFields(headerFields)]) + case (.emittedHeaders, .bodyChunk(let bodyChunk)), (.emittedBodyChunk, .bodyChunk(let bodyChunk)): + state = .emittedBodyChunk + return .emitEvents([.bodyChunk(bodyChunk)]) + } + } + } +} diff --git a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBytesToFramesSequence.swift b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBytesToFramesSequence.swift index 7229e45b..88036301 100644 --- a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBytesToFramesSequence.swift +++ b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBytesToFramesSequence.swift @@ -17,20 +17,12 @@ import Foundation final class Test_MultipartBytesToFramesSequence: Test_Runtime { func test() async throws { - var chunk = chunkFromStringLines([ + let chunk = chunkFromStringLines([ "--__abcd__", #"Content-Disposition: form-data; name="name""#, "", "24", "--__abcd__", #"Content-Disposition: form-data; name="info""#, "", "{}", "--__abcd__--", ]) - let next: () async throws -> ArraySlice? = { - if let first = chunk.first { - let out: ArraySlice = [first] - chunk = chunk.dropFirst() - return out - } else { - return nil - } - } - let upstream = HTTPBody(AsyncThrowingStream(unfolding: next), length: .unknown, iterationBehavior: .single) + var iterator = chunk.makeIterator() + let upstream = AsyncStream { iterator.next().map { ArraySlice([$0]) } } let sequence = MultipartBytesToFramesSequence(upstream: upstream, boundary: "__abcd__") var frames: [MultipartFrame] = [] for try await frame in sequence { frames.append(frame) } diff --git a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartFramesToBytesSequence.swift b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartFramesToBytesSequence.swift new file mode 100644 index 00000000..257c9614 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartFramesToBytesSequence.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_MultipartFramesToBytesSequence: Test_Runtime { + func test() async throws { + let frames: [MultipartFrame] = [ + .headerFields([.contentDisposition: #"form-data; name="name""#]), .bodyChunk(chunkFromString("2")), + .bodyChunk(chunkFromString("4")), .headerFields([.contentDisposition: #"form-data; name="info""#]), + .bodyChunk(chunkFromString("{")), .bodyChunk(chunkFromString("}")), + ] + var iterator = frames.makeIterator() + let upstream = AsyncStream { iterator.next() } + let sequence = MultipartFramesToBytesSequence(upstream: upstream, boundary: "__abcd__") + var bytes: ArraySlice = [] + for try await chunk in sequence { bytes.append(contentsOf: chunk) } + let expectedBytes = chunkFromStringLines([ + "--__abcd__", #"content-disposition: form-data; name="name""#, "", "24", "--__abcd__", + #"content-disposition: form-data; name="info""#, "", "{}", "--__abcd__--", "", + ]) + XCTAssertEqualData(bytes, expectedBytes) + } +} diff --git a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartSerializer.swift b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartSerializer.swift new file mode 100644 index 00000000..7dd96a64 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartSerializer.swift @@ -0,0 +1,79 @@ +//===----------------------------------------------------------------------===// +// +// 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_MultipartSerializer: Test_Runtime { + func test() async throws { + let frames: [MultipartFrame] = [ + .headerFields([.contentDisposition: #"form-data; name="name""#]), .bodyChunk(chunkFromString("2")), + .bodyChunk(chunkFromString("4")), .headerFields([.contentDisposition: #"form-data; name="info""#]), + .bodyChunk(chunkFromString("{")), .bodyChunk(chunkFromString("}")), + ] + var serializer = MultipartSerializer(boundary: "__abcd__") + var iterator = frames.makeIterator() + var bytes: [UInt8] = [] + while let chunk = try await serializer.next({ iterator.next() }) { bytes.append(contentsOf: chunk) } + let expectedBytes = chunkFromStringLines([ + "--__abcd__", #"content-disposition: form-data; name="name""#, "", "24", "--__abcd__", + #"content-disposition: form-data; name="info""#, "", "{}", "--__abcd__--", "", + ]) + XCTAssertEqualData(bytes, expectedBytes) + } +} + +private func newStateMachine() -> MultipartSerializer.StateMachine { .init() } + +final class Test_MultipartSerializerStateMachine: Test_Runtime { + + func testInvalidFirstFrame() throws { + var stateMachine = newStateMachine() + XCTAssertEqual(stateMachine.next(), .emitStart) + XCTAssertEqual(stateMachine.next(), .needsMore) + XCTAssertEqual(stateMachine.receivedFrame(.bodyChunk([])), .emitError(.noHeaderFieldsAtStart)) + } + + func testTwoParts() throws { + var stateMachine = newStateMachine() + XCTAssertEqual(stateMachine.state, .initial) + XCTAssertEqual(stateMachine.next(), .emitStart) + XCTAssertEqual(stateMachine.state, .emittedStart) + XCTAssertEqual( + stateMachine.receivedFrame(.headerFields([.contentDisposition: #"form-data; name="name""#])), + .emitEvents([.headerFields([.contentDisposition: #"form-data; name="name""#])]) + ) + XCTAssertEqual(stateMachine.state, .emittedHeaders) + XCTAssertEqual(stateMachine.next(), .needsMore) + XCTAssertEqual( + stateMachine.receivedFrame(.bodyChunk(chunkFromString("24"))), + .emitEvents([.bodyChunk(chunkFromString("24"))]) + ) + XCTAssertEqual(stateMachine.state, .emittedBodyChunk) + XCTAssertEqual(stateMachine.next(), .needsMore) + XCTAssertEqual( + stateMachine.receivedFrame(.headerFields([.contentDisposition: #"form-data; name="info""#])), + .emitEvents([.endOfPart, .headerFields([.contentDisposition: #"form-data; name="info""#])]) + ) + XCTAssertEqual(stateMachine.state, .emittedHeaders) + XCTAssertEqual(stateMachine.next(), .needsMore) + XCTAssertEqual( + stateMachine.receivedFrame(.bodyChunk(chunkFromString("{}"))), + .emitEvents([.bodyChunk(chunkFromString("{}"))]) + ) + XCTAssertEqual(stateMachine.state, .emittedBodyChunk) + XCTAssertEqual(stateMachine.next(), .needsMore) + XCTAssertEqual(stateMachine.receivedFrame(nil), .emitEvents([.endOfPart, .end])) + } +} diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index e2fe87c0..2e6d386e 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -280,3 +280,62 @@ public func XCTAssertEqualStringifiedData( 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) } + +fileprivate extension UInt8 { + var asHex: String { + let original: String + switch self { + case 0x0d: original = "CR" + case 0x0a: original = "LF" + default: original = "\(UnicodeScalar(self)) " + } + return String(format: "%02x \(original)", self) + } +} +/// Asserts that the data matches the expected value. +public func XCTAssertEqualData( + _ expression1: @autoclosure () throws -> C1?, + _ expression2: @autoclosure () throws -> C2, + _ message: @autoclosure () -> String = "Data doesn't match.", + file: StaticString = #filePath, + line: UInt = #line +) where C1.Element == UInt8, C2.Element == UInt8 { + do { + guard let actualBytes = try expression1() else { + XCTFail("First value is nil", file: file, line: line) + return + } + let expectedBytes = try expression2() + if ArraySlice(actualBytes) == ArraySlice(expectedBytes) { return } + let actualCount = actualBytes.count + let expectedCount = expectedBytes.count + let minCount = min(actualCount, expectedCount) + print("Printing both byte sequences, first is the actual value and second is the expected one.") + for (index, byte) in zip(actualBytes.prefix(minCount), expectedBytes.prefix(minCount)).enumerated() { + print("\(String(format: "%04d", index)): \(byte.0 != byte.1 ? "x" : " ") \(byte.0.asHex) | \(byte.1.asHex)") + } + let direction: String + let extraBytes: ArraySlice + if actualCount > expectedCount { + direction = "Actual bytes has extra bytes" + extraBytes = ArraySlice(actualBytes.dropFirst(minCount)) + } else if expectedCount > actualCount { + direction = "Actual bytes is missing expected bytes" + extraBytes = ArraySlice(expectedBytes.dropFirst(minCount)) + } else { + direction = "" + extraBytes = [] + } + if !extraBytes.isEmpty { + print("\(direction):") + for (index, byte) in extraBytes.enumerated() { + print("\(String(format: "%04d", minCount + index)): \(byte.asHex)") + } + } + XCTFail( + "Actual stringified data '\(String(decoding: actualBytes, as: UTF8.self))' doesn't equal to expected stringified data '\(String(decoding: expectedBytes, as: UTF8.self))'. Details: \(message())", + file: file, + line: line + ) + } catch { XCTFail(error.localizedDescription, file: file, line: line) } +} From 927f93009947d2bf0be32f14975e6a9107d4a725 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 17 Nov 2023 17:56:26 +0100 Subject: [PATCH 06/20] [Multipart] Add a frames -> raw parts parsing sequence (#74) --- .../MultipartBytesToFramesSequence.swift | 336 ++++++++++++++++ .../MultipartFramesToBytesSequence.swift | 245 +++++++++++ .../MultipartFramesToRawPartsSequence.swift | 380 ++++++++++++++++++ .../Multipart/MultipartParser.swift | 350 ---------------- .../Multipart/MultipartPublicTypes.swift | 35 ++ .../Multipart/MultipartSerializer.swift | 260 ------------ .../Test_MultipartBytesToFramesSequence.swift | 143 +++++++ .../Test_MultipartFramesToBytesSequence.swift | 63 +++ ...st_MultipartFramesToRawPartsSequence.swift | 134 ++++++ .../Multipart/Test_MultipartParser.swift | 159 -------- .../Multipart/Test_MultipartSerializer.swift | 79 ---- docker/docker-compose.2204.510.yaml | 4 +- 12 files changed, 1339 insertions(+), 849 deletions(-) create mode 100644 Sources/OpenAPIRuntime/Multipart/MultipartFramesToRawPartsSequence.swift delete mode 100644 Sources/OpenAPIRuntime/Multipart/MultipartParser.swift create mode 100644 Sources/OpenAPIRuntime/Multipart/MultipartPublicTypes.swift delete mode 100644 Sources/OpenAPIRuntime/Multipart/MultipartSerializer.swift create mode 100644 Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartFramesToRawPartsSequence.swift delete mode 100644 Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartParser.swift delete mode 100644 Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartSerializer.swift diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartBytesToFramesSequence.swift b/Sources/OpenAPIRuntime/Multipart/MultipartBytesToFramesSequence.swift index b95ce563..1e03fc75 100644 --- a/Sources/OpenAPIRuntime/Multipart/MultipartBytesToFramesSequence.swift +++ b/Sources/OpenAPIRuntime/Multipart/MultipartBytesToFramesSequence.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import HTTPTypes +import Foundation /// A sequence that parses multipart frames from bytes. struct MultipartBytesToFramesSequence: Sendable @@ -65,3 +66,338 @@ extension MultipartBytesToFramesSequence: AsyncSequence { mutating func next() async throws -> MultipartFrame? { try await parser.next { try await upstream.next() } } } } + +/// A parser of multipart frames from bytes. +struct MultipartParser { + + /// The underlying state machine. + private var stateMachine: StateMachine + + /// Creates a new parser. + /// - Parameter boundary: The boundary that separates parts. + init(boundary: String) { self.stateMachine = .init(boundary: boundary) } + + /// Parses the next frame. + /// - Parameter fetchChunk: A closure that is called when the parser + /// needs more bytes to parse the next frame. + /// - Returns: A parsed frame, or nil at the end of the message. + /// - Throws: When a parsing error is encountered. + mutating func next(_ fetchChunk: () async throws -> ArraySlice?) async throws -> MultipartFrame? { + while true { + switch stateMachine.readNextPart() { + case .none: continue + case .emitError(let actionError): throw ParserError(error: actionError) + case .returnNil: return nil + case .emitHeaderFields(let httpFields): return .headerFields(httpFields) + case .emitBodyChunk(let bodyChunk): return .bodyChunk(bodyChunk) + case .needsMore: + let chunk = try await fetchChunk() + switch stateMachine.receivedChunk(chunk) { + case .none: continue + case .returnNil: return nil + case .emitError(let actionError): throw ParserError(error: actionError) + } + } + } + } +} + +extension MultipartParser { + + /// An error thrown by the parser. + struct ParserError: Swift.Error, CustomStringConvertible, LocalizedError { + + /// The underlying error emitted by the state machine. + let error: MultipartParser.StateMachine.ActionError + + var description: String { + switch error { + case .invalidInitialBoundary: return "Invalid initial boundary." + case .invalidCRLFAtStartOfHeaderField: return "Invalid CRLF at the start of a header field." + case .missingColonAfterHeaderName: return "Missing colon after header field name." + case .invalidCharactersInHeaderFieldName: return "Invalid characters in a header field name." + case .incompleteMultipartMessage: return "Incomplete multipart message." + case .receivedChunkWhenFinished: return "Received a chunk after being finished." + } + } + + var errorDescription: String? { description } + } +} + +extension MultipartParser { + + /// A state machine representing the byte to multipart frame parser. + struct StateMachine { + + /// The possible states of the state machine. + enum State: Hashable { + + /// Has not yet fully parsed the initial boundary. + case parsingInitialBoundary([UInt8]) + + /// A substate when parsing a part. + enum PartState: Hashable { + + /// Accumulating part headers. + case parsingHeaderFields(HTTPFields) + + /// Forwarding body chunks. + case parsingBody + } + + /// Is parsing a part. + case parsingPart([UInt8], PartState) + + /// Finished, the terminal state. + case finished + + /// Helper state to avoid copy-on-write copies. + case mutating + } + + /// The current state of the state machine. + private(set) var state: State + + /// The bytes of the boundary. + private let boundary: ArraySlice + + /// The bytes of the boundary with the double dash prepended. + private let dashDashBoundary: ArraySlice + + /// The bytes of the boundary prepended by CRLF + double dash. + private let crlfDashDashBoundary: ArraySlice + + /// Creates a new state machine. + /// - Parameter boundary: The boundary used to separate parts. + init(boundary: String) { + self.state = .parsingInitialBoundary([]) + self.boundary = ArraySlice(boundary.utf8) + self.dashDashBoundary = ASCII.dashes + self.boundary + self.crlfDashDashBoundary = ASCII.crlf + dashDashBoundary + } + + /// An error returned by the state machine. + enum ActionError: Hashable { + + /// The initial boundary is malformed. + case invalidInitialBoundary + + /// The expected CRLF at the start of a header is missing. + case invalidCRLFAtStartOfHeaderField + + /// A header field name contains an invalid character. + case invalidCharactersInHeaderFieldName + + /// The header field name is not followed by a colon. + case missingColonAfterHeaderName + + /// More bytes were received after completion. + case receivedChunkWhenFinished + + /// Ran out of bytes without the message being complete. + case incompleteMultipartMessage + } + + /// An action returned by the `readNextPart` method. + enum ReadNextPartAction: Hashable { + + /// No action, call `readNextPart` again. + case none + + /// Throw the provided error. + case emitError(ActionError) + + /// Return nil to the caller, no more frames. + case returnNil + + /// Emit a frame with the provided header fields. + case emitHeaderFields(HTTPFields) + + /// Emit a frame with the provided part body chunk. + case emitBodyChunk(ArraySlice) + + /// Needs more bytes to parse the next frame. + case needsMore + } + + /// Read the next frame from the accumulated bytes. + /// - Returns: An action to perform. + mutating func readNextPart() -> ReadNextPartAction { + switch state { + case .mutating: preconditionFailure("Invalid state: \(state)") + case .finished: return .returnNil + case .parsingInitialBoundary(var buffer): + state = .mutating + // These first bytes must be the boundary already, otherwise this is a malformed multipart body. + switch buffer.firstIndexAfterPrefix(dashDashBoundary) { + case .index(let index): + buffer.removeSubrange(buffer.startIndex...Index + switch buffer.firstIndexAfterPrefix(ASCII.crlf) { + case .index(let index): indexAfterFirstCRLF = index + case .reachedEndOfSelf: + state = .parsingPart(buffer, .parsingHeaderFields(headerFields)) + return .needsMore + case .unexpectedPrefix: + state = .finished + return .emitError(.invalidCRLFAtStartOfHeaderField) + } + // If CRLF is here, this is the end of header fields section. + switch buffer[indexAfterFirstCRLF...].firstIndexAfterPrefix(ASCII.crlf) { + case .index(let index): + buffer.removeSubrange(buffer.startIndex...Index + // Check that what follows is a colon, otherwise this is a malformed header field line. + // Source: RFC 7230, section 3.2.4. + switch buffer[endHeaderNameIndex...].firstIndexAfterPrefix([ASCII.colon]) { + case .index(let index): startHeaderValueWithWhitespaceIndex = index + case .reachedEndOfSelf: + state = .parsingPart(buffer, .parsingHeaderFields(headerFields)) + return .needsMore + case .unexpectedPrefix: + state = .finished + return .emitError(.missingColonAfterHeaderName) + } + guard + let startHeaderValueIndex = buffer[startHeaderValueWithWhitespaceIndex...] + .firstIndex(where: { !ASCII.optionalWhitespace.contains($0) }) + else { + state = .parsingPart(buffer, .parsingHeaderFields(headerFields)) + return .needsMore + } + + // Find the CRLF first, then remove any trailing whitespace. + guard + let endHeaderValueWithWhitespaceRange = buffer[startHeaderValueIndex...] + .firstRange(of: ASCII.crlf) + else { + state = .parsingPart(buffer, .parsingHeaderFields(headerFields)) + return .needsMore + } + let headerFieldValueBytes = buffer[ + startHeaderValueIndex..?) -> ReceivedChunkAction { + switch state { + case .parsingInitialBoundary(var buffer): + guard let chunk else { return .emitError(.incompleteMultipartMessage) } + state = .mutating + buffer.append(contentsOf: chunk) + state = .parsingInitialBoundary(buffer) + return .none + case .parsingPart(var buffer, let part): + guard let chunk else { return .emitError(.incompleteMultipartMessage) } + state = .mutating + buffer.append(contentsOf: chunk) + state = .parsingPart(buffer, part) + return .none + case .finished: + guard chunk == nil else { return .emitError(.receivedChunkWhenFinished) } + return .returnNil + case .mutating: preconditionFailure("Invalid state: \(state)") + } + } + } +} diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartFramesToBytesSequence.swift b/Sources/OpenAPIRuntime/Multipart/MultipartFramesToBytesSequence.swift index e1d55542..441c85fd 100644 --- a/Sources/OpenAPIRuntime/Multipart/MultipartFramesToBytesSequence.swift +++ b/Sources/OpenAPIRuntime/Multipart/MultipartFramesToBytesSequence.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import HTTPTypes +import Foundation /// A sequence that serializes multipart frames into bytes. struct MultipartFramesToBytesSequence: Sendable @@ -69,3 +70,247 @@ extension MultipartFramesToBytesSequence: AsyncSequence { } } } + +/// A serializer of multipart frames into bytes. +struct MultipartSerializer { + + /// The boundary that separates parts. + private let boundary: ArraySlice + + /// The underlying state machine. + private var stateMachine: StateMachine + + /// The buffer of bytes ready to be written out. + private var outBuffer: [UInt8] + + /// Creates a new serializer. + /// - Parameter boundary: The boundary that separates parts. + init(boundary: String) { + self.boundary = ArraySlice(boundary.utf8) + self.stateMachine = .init() + self.outBuffer = [] + } + /// Requests the next byte chunk. + /// - Parameter fetchFrame: A closure that is called when the serializer is ready to serialize the next frame. + /// - Returns: A byte chunk. + /// - Throws: When a serialization error is encountered. + mutating func next(_ fetchFrame: () async throws -> MultipartFrame?) async throws -> ArraySlice? { + + func flushedBytes() -> ArraySlice { + let outChunk = ArraySlice(outBuffer) + outBuffer.removeAll(keepingCapacity: true) + return outChunk + } + + while true { + switch stateMachine.next() { + case .returnNil: return nil + case .emitStart: + emitStart() + return flushedBytes() + case .needsMore: + let frame = try await fetchFrame() + switch stateMachine.receivedFrame(frame) { + case .returnNil: return nil + case .emitEvents(let events): + for event in events { + switch event { + case .headerFields(let headerFields): emitHeaders(headerFields) + case .bodyChunk(let chunk): emitBodyChunk(chunk) + case .endOfPart: emitEndOfPart() + case .start: emitStart() + case .end: emitEnd() + } + } + return flushedBytes() + case .emitError(let error): throw SerializerError(error: error) + } + } + } + } +} + +extension MultipartSerializer { + + /// An error thrown by the serializer. + struct SerializerError: Swift.Error, CustomStringConvertible, LocalizedError { + + /// The underlying error emitted by the state machine. + var error: StateMachine.ActionError + + var description: String { + switch error { + case .noHeaderFieldsAtStart: return "No header fields found at the start of the multipart body." + } + } + + var errorDescription: String? { description } + } +} + +extension MultipartSerializer { + + /// Writes the provided header fields into the buffer. + /// - Parameter headerFields: The header fields to serialize. + private mutating func emitHeaders(_ headerFields: HTTPFields) { + outBuffer.append(contentsOf: ASCII.crlf) + let sortedHeaders = headerFields.sorted { a, b in a.name.canonicalName < b.name.canonicalName } + for headerField in sortedHeaders { + outBuffer.append(contentsOf: headerField.name.canonicalName.utf8) + outBuffer.append(contentsOf: ASCII.colonSpace) + outBuffer.append(contentsOf: headerField.value.utf8) + outBuffer.append(contentsOf: ASCII.crlf) + } + outBuffer.append(contentsOf: ASCII.crlf) + } + + /// Writes the part body chunk into the buffer. + /// - Parameter bodyChunk: The body chunk to write. + private mutating func emitBodyChunk(_ bodyChunk: ArraySlice) { outBuffer.append(contentsOf: bodyChunk) } + + /// Writes an end of part boundary into the buffer. + private mutating func emitEndOfPart() { + outBuffer.append(contentsOf: ASCII.crlf) + outBuffer.append(contentsOf: ASCII.dashes) + outBuffer.append(contentsOf: boundary) + } + + /// Writes the start boundary into the buffer. + private mutating func emitStart() { + outBuffer.append(contentsOf: ASCII.dashes) + outBuffer.append(contentsOf: boundary) + } + + /// Writes the end double dash to the buffer. + private mutating func emitEnd() { + outBuffer.append(contentsOf: ASCII.dashes) + outBuffer.append(contentsOf: ASCII.crlf) + outBuffer.append(contentsOf: ASCII.crlf) + } +} + +extension MultipartSerializer { + + /// A state machine representing the multipart frame serializer. + struct StateMachine { + + /// The possible states of the state machine. + enum State: Hashable { + + /// Has not yet written any bytes. + case initial + + /// Emitted start, but no frames yet. + case startedNothingEmittedYet + + /// Finished, the terminal state. + case finished + + /// Last emitted a header fields frame. + case emittedHeaders + + /// Last emitted a part body chunk frame. + case emittedBodyChunk + } + + /// The current state of the state machine. + private(set) var state: State + + /// Creates a new state machine. + init() { self.state = .initial } + + /// An error returned by the state machine. + enum ActionError: Hashable { + + /// The first frame from upstream was not a header fields frame. + case noHeaderFieldsAtStart + } + + /// An action returned by the `next` method. + enum NextAction: Hashable { + + /// Return nil to the caller, no more bytes. + case returnNil + + /// Emit the initial boundary. + case emitStart + + /// Ready for the next frame. + case needsMore + } + + /// Read the next byte chunk serialized from upstream frames. + /// - Returns: An action to perform. + mutating func next() -> NextAction { + switch state { + case .initial: + state = .startedNothingEmittedYet + return .emitStart + case .finished: return .returnNil + case .startedNothingEmittedYet, .emittedHeaders, .emittedBodyChunk: return .needsMore + } + } + + /// An event to serialize to bytes. + enum Event: Hashable { + + /// The header fields of a part. + case headerFields(HTTPFields) + + /// A byte chunk of a part. + case bodyChunk(ArraySlice) + + /// A boundary between parts. + case endOfPart + + /// The initial boundary. + case start + + /// The final dashes. + case end + } + + /// An action returned by the `receivedFrame` method. + enum ReceivedFrameAction: Hashable { + + /// Return nil to the caller, no more bytes. + case returnNil + + /// Write the provided events as bytes. + case emitEvents([Event]) + + /// Throw the provided error. + case emitError(ActionError) + } + + /// Ingest the provided frame. + /// - Parameter frame: A new frame. If `nil`, then the source of frames is finished. + /// - Returns: An action to perform. + mutating func receivedFrame(_ frame: MultipartFrame?) -> ReceivedFrameAction { + switch state { + case .initial: preconditionFailure("Invalid state: \(state)") + case .finished: return .returnNil + case .startedNothingEmittedYet, .emittedHeaders, .emittedBodyChunk: break + } + switch (state, frame) { + case (.initial, _), (.finished, _): preconditionFailure("Already handled above.") + case (_, .none): + state = .finished + return .emitEvents([.endOfPart, .end]) + case (.startedNothingEmittedYet, .headerFields(let headerFields)): + state = .emittedHeaders + return .emitEvents([.headerFields(headerFields)]) + case (.startedNothingEmittedYet, .bodyChunk): + state = .finished + return .emitError(.noHeaderFieldsAtStart) + case (.emittedHeaders, .headerFields(let headerFields)), + (.emittedBodyChunk, .headerFields(let headerFields)): + state = .emittedHeaders + return .emitEvents([.endOfPart, .headerFields(headerFields)]) + case (.emittedHeaders, .bodyChunk(let bodyChunk)), (.emittedBodyChunk, .bodyChunk(let bodyChunk)): + state = .emittedBodyChunk + return .emitEvents([.bodyChunk(bodyChunk)]) + } + } + } +} diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartFramesToRawPartsSequence.swift b/Sources/OpenAPIRuntime/Multipart/MultipartFramesToRawPartsSequence.swift new file mode 100644 index 00000000..3345c088 --- /dev/null +++ b/Sources/OpenAPIRuntime/Multipart/MultipartFramesToRawPartsSequence.swift @@ -0,0 +1,380 @@ +//===----------------------------------------------------------------------===// +// +// 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 HTTPTypes +import Foundation + +/// A sequence that parses raw multipart parts from multipart frames. +struct MultipartFramesToRawPartsSequence: Sendable +where Upstream.Element == MultipartFrame { + + /// The source of multipart frames. + var upstream: Upstream +} + +extension MultipartFramesToRawPartsSequence: AsyncSequence { + + /// The type of element produced by this asynchronous sequence. + typealias Element = MultipartRawPart + + /// Creates the asynchronous iterator that produces elements of this + /// asynchronous sequence. + /// + /// - Returns: An instance of the `AsyncIterator` type used to produce + /// elements of the asynchronous sequence. + func makeAsyncIterator() -> Iterator { Iterator(makeUpstreamIterator: { upstream.makeAsyncIterator() }) } + + /// An iterator that pulls frames from the upstream iterator and provides + /// raw multipart parts. + struct Iterator: AsyncIteratorProtocol { + + /// The underlying shared iterator. + var shared: SharedIterator + + /// The closure invoked to fetch the next byte chunk of the part's body. + var bodyClosure: @Sendable () async throws -> ArraySlice? + + /// Creates a new iterator. + /// - Parameter makeUpstreamIterator: A closure that creates the upstream source of frames. + init(makeUpstreamIterator: @Sendable () -> Upstream.AsyncIterator) { + let shared = SharedIterator(makeUpstreamIterator: makeUpstreamIterator) + self.shared = shared + self.bodyClosure = { try await shared.nextFromBodySubsequence() } + } + + /// Asynchronously advances to the next element and returns it, or ends the + /// sequence if there is no next element. + /// + /// - Returns: The next element, if it exists, or `nil` to signal the end of + /// the sequence. + mutating func next() async throws -> Element? { + try await shared.nextFromPartSequence(bodyClosure: bodyClosure) + } + } +} + +extension HTTPBody { + + /// Creates a new body from the provided header fields and body closure. + /// - Parameters: + /// - headerFields: The header fields to inspect for a `content-length` header. + /// - bodyClosure: A closure invoked to fetch the next byte chunk of the body. + fileprivate convenience init( + headerFields: HTTPFields, + bodyClosure: @escaping @Sendable () async throws -> ArraySlice? + ) { + let stream = AsyncThrowingStream(unfolding: bodyClosure) + let length: HTTPBody.Length + if let contentLengthString = headerFields[.contentLength], let contentLength = Int(contentLengthString) { + length = .known(contentLength) + } else { + length = .unknown + } + self.init(stream, length: length) + } +} + +extension MultipartFramesToRawPartsSequence { + + /// A state machine representing the frame to raw part parser. + struct StateMachine { + + /// The possible states of the state machine. + enum State: Hashable { + + /// Has not started parsing any parts yet. + case initial + + /// Waiting to send header fields to start a new part. + /// + /// Associated value is optional headers. + /// If they're non-nil, they arrived already, so just send them right away. + /// If they're nil, you need to fetch the next frame to get them. + case waitingToSendHeaders(HTTPFields?) + + /// In the process of streaming the byte chunks of a part body. + case streamingBody + + /// Finished, the terminal state. + case finished + } + + /// The current state of the state machine. + private(set) var state: State + + /// Creates a new state machine. + init() { self.state = .initial } + + /// An error returned by the state machine. + enum ActionError: Hashable { + + /// The outer, raw part sequence called next before the current part's body was fully consumed. + /// + /// This is a usage error by the consumer of the sequence. + case partSequenceNextCalledBeforeBodyWasConsumed + + /// The first frame received was a body chunk instead of header fields, which is invalid. + /// + /// This indicates an issue in the source of frames. + case receivedBodyChunkInInitial + + /// Received a body chunk when waiting for header fields, which is invalid. + /// + /// This indicates an issue in the source of frames. + case receivedBodyChunkWhenWaitingForHeaders + + /// Received another frame before having had a chance to send out header fields, this is an error caused + /// by the driver of the state machine. + case receivedFrameWhenAlreadyHasUnsentHeaders + } + + /// An action returned by the `nextFromPartSequence` method. + enum NextFromPartSequenceAction: Hashable { + + /// Return nil to the caller, no more parts. + case returnNil + + /// Fetch the next frame. + case fetchFrame + + /// Throw the provided error. + case emitError(ActionError) + + /// Emit a part with the provided header fields. + case emitPart(HTTPFields) + } + + /// Read the next part from the upstream frames. + /// - Returns: An action to perform. + mutating func nextFromPartSequence() -> NextFromPartSequenceAction { + switch state { + case .initial: + state = .waitingToSendHeaders(nil) + return .fetchFrame + case .waitingToSendHeaders(.some(let headers)): + state = .streamingBody + return .emitPart(headers) + case .waitingToSendHeaders(.none), .streamingBody: + state = .finished + return .emitError(.partSequenceNextCalledBeforeBodyWasConsumed) + case .finished: return .returnNil + } + } + + /// An action returned by the `partReceivedFrame` method. + enum PartReceivedFrameAction: Hashable { + + /// Return nil to the caller, no more parts. + case returnNil + + /// Throw the provided error. + case emitError(ActionError) + + /// Emit a part with the provided header fields. + case emitPart(HTTPFields) + } + + /// Ingest the provided frame, requested by the part sequence. + /// - Parameter frame: A new frame. If `nil`, then the source of frames is finished. + /// - Returns: An action to perform. + mutating func partReceivedFrame(_ frame: MultipartFrame?) -> PartReceivedFrameAction { + switch state { + case .initial: preconditionFailure("Haven't asked for a part chunk, how did we receive one?") + case .waitingToSendHeaders(.some): + state = .finished + return .emitError(.receivedFrameWhenAlreadyHasUnsentHeaders) + case .waitingToSendHeaders(.none): + if let frame { + switch frame { + case .headerFields(let headers): + state = .streamingBody + return .emitPart(headers) + case .bodyChunk: + state = .finished + return .emitError(.receivedBodyChunkWhenWaitingForHeaders) + } + } else { + state = .finished + return .returnNil + } + case .streamingBody: + state = .finished + return .emitError(.partSequenceNextCalledBeforeBodyWasConsumed) + case .finished: return .returnNil + } + } + + /// An action returned by the `nextFromBodySubsequence` method. + enum NextFromBodySubsequenceAction: Hashable { + + /// Return nil to the caller, no more byte chunks. + case returnNil + + /// Fetch the next frame. + case fetchFrame + + /// Throw the provided error. + case emitError(ActionError) + } + + /// Read the next byte chunk requested by the current part's body sequence. + /// - Returns: An action to perform. + mutating func nextFromBodySubsequence() -> NextFromBodySubsequenceAction { + switch state { + case .initial: + state = .finished + return .emitError(.receivedBodyChunkInInitial) + case .waitingToSendHeaders: + state = .finished + return .emitError(.receivedBodyChunkWhenWaitingForHeaders) + case .streamingBody: return .fetchFrame + case .finished: return .returnNil + } + } + + /// An action returned by the `bodyReceivedFrame` method. + enum BodyReceivedFrameAction: Hashable { + + /// Return nil to the caller, no more byte chunks. + case returnNil + + /// Return the provided byte chunk. + case returnChunk(ArraySlice) + + /// Throw the provided error. + case emitError(ActionError) + } + + /// Ingest the provided frame, requested by the body sequence. + /// - Parameter frame: A new frame. If `nil`, then the source of frames is finished. + /// - Returns: An action to perform. + mutating func bodyReceivedFrame(_ frame: MultipartFrame?) -> BodyReceivedFrameAction { + switch state { + case .initial: preconditionFailure("Haven't asked for a frame, how did we receive one?") + case .waitingToSendHeaders: + state = .finished + return .emitError(.receivedBodyChunkWhenWaitingForHeaders) + case .streamingBody: + if let frame { + switch frame { + case .headerFields(let headers): + state = .waitingToSendHeaders(headers) + return .returnNil + case .bodyChunk(let bodyChunk): return .returnChunk(bodyChunk) + } + } else { + state = .finished + return .returnNil + } + case .finished: return .returnNil + } + } + } +} + +extension MultipartFramesToRawPartsSequence { + + /// A type-safe iterator shared by the outer part sequence iterator and an inner body sequence iterator. + /// + /// It enforces that when a new part is emitted by the outer sequence, that the new part's body is then fully + /// consumed before the outer sequence is asked for the next part. + /// + /// This is required as the source of bytes is a single stream, so without the current part's body being consumed, + /// we can't move on to the next part. + actor SharedIterator { + + /// The upstream source of frames. + private var upstream: Upstream.AsyncIterator + + /// The underlying state machine. + private var stateMachine: StateMachine + + /// Creates a new iterator. + /// - Parameter makeUpstreamIterator: A closure that creates the upstream source of frames. + init(makeUpstreamIterator: @Sendable () -> Upstream.AsyncIterator) { + let upstream = makeUpstreamIterator() + self.upstream = upstream + self.stateMachine = .init() + } + + /// An error thrown by the shared iterator. + struct IteratorError: Swift.Error, CustomStringConvertible, LocalizedError { + + /// The underlying error emitted by the state machine. + let error: StateMachine.ActionError + + var description: String { + switch error { + case .partSequenceNextCalledBeforeBodyWasConsumed: + return + "The outer part sequence was asked for the next element before the current part's inner body sequence was fully consumed." + case .receivedBodyChunkInInitial: + return + "Received a body chunk from the upstream sequence as the first element, instead of header fields." + case .receivedBodyChunkWhenWaitingForHeaders: + return "Received a body chunk from the upstream sequence when expecting header fields." + case .receivedFrameWhenAlreadyHasUnsentHeaders: + return "Received another frame before the current frame with header fields was written out." + } + } + + var errorDescription: String? { description } + } + + /// Request the next element from the outer part sequence. + /// - Parameter bodyClosure: The closure invoked to fetch the next byte chunk of the part's body. + /// - Returns: The next element, or `nil` if finished. + /// - Throws: When a parsing error is encountered. + func nextFromPartSequence(bodyClosure: @escaping @Sendable () async throws -> ArraySlice?) async throws + -> Element? + { + switch stateMachine.nextFromPartSequence() { + case .returnNil: return nil + case .fetchFrame: + var upstream = upstream + let frame = try await upstream.next() + self.upstream = upstream + switch stateMachine.partReceivedFrame(frame) { + case .returnNil: return nil + case .emitError(let error): throw IteratorError(error: error) + case .emitPart(let headers): + let body = HTTPBody(headerFields: headers, bodyClosure: bodyClosure) + return .init(headerFields: headers, body: body) + } + case .emitError(let error): throw IteratorError(error: error) + case .emitPart(let headers): + let body = HTTPBody(headerFields: headers, bodyClosure: bodyClosure) + return .init(headerFields: headers, body: body) + } + } + + /// Request the next element from the inner body bytes sequence. + /// - Returns: The next element, or `nil` if finished. + func nextFromBodySubsequence() async throws -> ArraySlice? { + switch stateMachine.nextFromBodySubsequence() { + case .returnNil: return nil + case .fetchFrame: + var upstream = upstream + let frame = try await upstream.next() + self.upstream = upstream + switch stateMachine.bodyReceivedFrame(frame) { + case .returnNil: return nil + case .returnChunk(let bodyChunk): return bodyChunk + case .emitError(let error): throw IteratorError(error: error) + } + case .emitError(let error): throw IteratorError(error: error) + } + } + } +} diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartParser.swift b/Sources/OpenAPIRuntime/Multipart/MultipartParser.swift deleted file mode 100644 index d98db13e..00000000 --- a/Sources/OpenAPIRuntime/Multipart/MultipartParser.swift +++ /dev/null @@ -1,350 +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 -import HTTPTypes - -/// A parser of multipart frames from bytes. -struct MultipartParser { - - /// The underlying state machine. - private var stateMachine: StateMachine - - /// Creates a new parser. - /// - Parameter boundary: The boundary that separates parts. - init(boundary: String) { self.stateMachine = .init(boundary: boundary) } - - /// Parses the next frame. - /// - Parameter fetchChunk: A closure that is called when the parser - /// needs more bytes to parse the next frame. - /// - Returns: A parsed frame, or nil at the end of the message. - /// - Throws: When a parsing error is encountered. - mutating func next(_ fetchChunk: () async throws -> ArraySlice?) async throws -> MultipartFrame? { - while true { - switch stateMachine.readNextPart() { - case .none: continue - case .emitError(let actionError): throw ParserError(error: actionError) - case .returnNil: return nil - case .emitHeaderFields(let httpFields): return .headerFields(httpFields) - case .emitBodyChunk(let bodyChunk): return .bodyChunk(bodyChunk) - case .needsMore: - let chunk = try await fetchChunk() - switch stateMachine.receivedChunk(chunk) { - case .none: continue - case .returnNil: return nil - case .emitError(let actionError): throw ParserError(error: actionError) - } - } - } - } -} -extension MultipartParser { - - /// An error thrown by the parser. - struct ParserError: Swift.Error, CustomStringConvertible, LocalizedError { - - /// The underlying error emitted by the state machine. - let error: MultipartParser.StateMachine.ActionError - - var description: String { - switch error { - case .invalidInitialBoundary: return "Invalid initial boundary." - case .invalidCRLFAtStartOfHeaderField: return "Invalid CRLF at the start of a header field." - case .missingColonAfterHeaderName: return "Missing colon after header field name." - case .invalidCharactersInHeaderFieldName: return "Invalid characters in a header field name." - case .incompleteMultipartMessage: return "Incomplete multipart message." - case .receivedChunkWhenFinished: return "Received a chunk after being finished." - } - } - - var errorDescription: String? { description } - } -} - -extension MultipartParser { - - /// A state machine representing the byte to multipart frame parser. - struct StateMachine { - - /// The possible states of the state machine. - enum State: Hashable { - - /// Has not yet fully parsed the initial boundary. - case parsingInitialBoundary([UInt8]) - - /// A substate when parsing a part. - enum PartState: Hashable { - - /// Accumulating part headers. - case parsingHeaderFields(HTTPFields) - - /// Forwarding body chunks. - case parsingBody - } - - /// Is parsing a part. - case parsingPart([UInt8], PartState) - - /// Finished, the terminal state. - case finished - - /// Helper state to avoid copy-on-write copies. - case mutating - } - - /// The current state of the state machine. - private(set) var state: State - - /// The bytes of the boundary. - private let boundary: ArraySlice - - /// The bytes of the boundary with the double dash prepended. - private let dashDashBoundary: ArraySlice - - /// The bytes of the boundary prepended by CRLF + double dash. - private let crlfDashDashBoundary: ArraySlice - - /// Creates a new state machine. - /// - Parameter boundary: The boundary used to separate parts. - init(boundary: String) { - self.state = .parsingInitialBoundary([]) - self.boundary = ArraySlice(boundary.utf8) - self.dashDashBoundary = ASCII.dashes + self.boundary - self.crlfDashDashBoundary = ASCII.crlf + dashDashBoundary - } - - /// An error returned by the state machine. - enum ActionError: Hashable { - - /// The initial boundary is malformed. - case invalidInitialBoundary - - /// The expected CRLF at the start of a header is missing. - case invalidCRLFAtStartOfHeaderField - - /// A header field name contains an invalid character. - case invalidCharactersInHeaderFieldName - - /// The header field name is not followed by a colon. - case missingColonAfterHeaderName - - /// More bytes were received after completion. - case receivedChunkWhenFinished - - /// Ran out of bytes without the message being complete. - case incompleteMultipartMessage - } - - /// An action returned by the `readNextPart` method. - enum ReadNextPartAction: Hashable { - - /// No action, call `readNextPart` again. - case none - - /// Throw the provided error. - case emitError(ActionError) - - /// Return nil to the caller, no more frames. - case returnNil - - /// Emit a frame with the provided header fields. - case emitHeaderFields(HTTPFields) - - /// Emit a frame with the provided part body chunk. - case emitBodyChunk(ArraySlice) - - /// Needs more bytes to parse the next frame. - case needsMore - } - - /// Read the next part from the accumulated bytes. - /// - Returns: An action to perform. - mutating func readNextPart() -> ReadNextPartAction { - switch state { - case .mutating: preconditionFailure("Invalid state: \(state)") - case .finished: return .returnNil - case .parsingInitialBoundary(var buffer): - state = .mutating - // These first bytes must be the boundary already, otherwise this is a malformed multipart body. - switch buffer.firstIndexAfterPrefix(dashDashBoundary) { - case .index(let index): - buffer.removeSubrange(buffer.startIndex...Index - switch buffer.firstIndexAfterPrefix(ASCII.crlf) { - case .index(let index): indexAfterFirstCRLF = index - case .reachedEndOfSelf: - state = .parsingPart(buffer, .parsingHeaderFields(headerFields)) - return .needsMore - case .unexpectedPrefix: - state = .finished - return .emitError(.invalidCRLFAtStartOfHeaderField) - } - // If CRLF is here, this is the end of header fields section. - switch buffer[indexAfterFirstCRLF...].firstIndexAfterPrefix(ASCII.crlf) { - case .index(let index): - buffer.removeSubrange(buffer.startIndex...Index - // Check that what follows is a colon, otherwise this is a malformed header field line. - // Source: RFC 7230, section 3.2.4. - switch buffer[endHeaderNameIndex...].firstIndexAfterPrefix([ASCII.colon]) { - case .index(let index): startHeaderValueWithWhitespaceIndex = index - case .reachedEndOfSelf: - state = .parsingPart(buffer, .parsingHeaderFields(headerFields)) - return .needsMore - case .unexpectedPrefix: - state = .finished - return .emitError(.missingColonAfterHeaderName) - } - guard - let startHeaderValueIndex = buffer[startHeaderValueWithWhitespaceIndex...] - .firstIndex(where: { !ASCII.optionalWhitespace.contains($0) }) - else { - state = .parsingPart(buffer, .parsingHeaderFields(headerFields)) - return .needsMore - } - - // Find the CRLF first, then remove any trailing whitespace. - guard - let endHeaderValueWithWhitespaceRange = buffer[startHeaderValueIndex...] - .firstRange(of: ASCII.crlf) - else { - state = .parsingPart(buffer, .parsingHeaderFields(headerFields)) - return .needsMore - } - let headerFieldValueBytes = buffer[ - startHeaderValueIndex..?) -> ReceivedChunkAction { - switch state { - case .parsingInitialBoundary(var buffer): - guard let chunk else { return .emitError(.incompleteMultipartMessage) } - state = .mutating - buffer.append(contentsOf: chunk) - state = .parsingInitialBoundary(buffer) - return .none - case .parsingPart(var buffer, let part): - guard let chunk else { return .emitError(.incompleteMultipartMessage) } - state = .mutating - buffer.append(contentsOf: chunk) - state = .parsingPart(buffer, part) - return .none - case .finished: - guard chunk == nil else { return .emitError(.receivedChunkWhenFinished) } - return .returnNil - case .mutating: preconditionFailure("Invalid state: \(state)") - } - } - } -} diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartPublicTypes.swift b/Sources/OpenAPIRuntime/Multipart/MultipartPublicTypes.swift new file mode 100644 index 00000000..213dcfb6 --- /dev/null +++ b/Sources/OpenAPIRuntime/Multipart/MultipartPublicTypes.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// 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 +import HTTPTypes + +/// A raw multipart part containing the header fields and the body stream. +public struct MultipartRawPart: Sendable, Hashable { + + /// The header fields contained in this part, such as `content-disposition`. + public var headerFields: HTTPFields + + /// The body stream of this part. + public var body: HTTPBody + + /// Creates a new part. + /// - Parameters: + /// - headerFields: The header fields contained in this part, such as `content-disposition`. + /// - body: The body stream of this part. + public init(headerFields: HTTPFields, body: HTTPBody) { + self.headerFields = headerFields + self.body = body + } +} diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartSerializer.swift b/Sources/OpenAPIRuntime/Multipart/MultipartSerializer.swift deleted file mode 100644 index 8f744784..00000000 --- a/Sources/OpenAPIRuntime/Multipart/MultipartSerializer.swift +++ /dev/null @@ -1,260 +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 -import HTTPTypes - -/// A serializer of multipart frames into bytes. -struct MultipartSerializer { - - /// The boundary that separates parts. - private let boundary: ArraySlice - - /// The underlying state machine. - private var stateMachine: StateMachine - - /// The buffer of bytes ready to be written out. - private var outBuffer: [UInt8] - - /// Creates a new serializer. - /// - Parameter boundary: The boundary that separates parts. - init(boundary: String) { - self.boundary = ArraySlice(boundary.utf8) - self.stateMachine = .init() - self.outBuffer = [] - } - /// Requests the next byte chunk. - /// - Parameter fetchFrame: A closure that is called when the serializer is ready to serialize the next frame. - /// - Returns: A byte chunk. - /// - Throws: When a serialization error is encountered. - mutating func next(_ fetchFrame: () async throws -> MultipartFrame?) async throws -> ArraySlice? { - - func flushedBytes() -> ArraySlice { - let outChunk = ArraySlice(outBuffer) - outBuffer.removeAll(keepingCapacity: true) - return outChunk - } - - while true { - switch stateMachine.next() { - case .returnNil: return nil - case .emitStart: - emitStart() - return flushedBytes() - case .needsMore: - let frame = try await fetchFrame() - switch stateMachine.receivedFrame(frame) { - case .returnNil: return nil - case .emitEvents(let events): - for event in events { - switch event { - case .headerFields(let headerFields): emitHeaders(headerFields) - case .bodyChunk(let chunk): emitBodyChunk(chunk) - case .endOfPart: emitEndOfPart() - case .start: emitStart() - case .end: emitEnd() - } - } - return flushedBytes() - case .emitError(let error): throw SerializerError(error: error) - } - } - } - } -} - -extension MultipartSerializer { - - /// An error thrown by the serializer. - struct SerializerError: Swift.Error, CustomStringConvertible, LocalizedError { - - /// The underlying error emitted by the state machine. - var error: StateMachine.ActionError - - var description: String { - switch error { - case .noHeaderFieldsAtStart: return "No header fields found at the start of the multipart body." - } - } - - var errorDescription: String? { description } - } -} - -extension MultipartSerializer { - - /// Writes the provided header fields into the buffer. - /// - Parameter headerFields: The header fields to serialize. - private mutating func emitHeaders(_ headerFields: HTTPFields) { - outBuffer.append(contentsOf: ASCII.crlf) - let sortedHeaders = headerFields.sorted { a, b in a.name.canonicalName < b.name.canonicalName } - for headerField in sortedHeaders { - outBuffer.append(contentsOf: headerField.name.canonicalName.utf8) - outBuffer.append(contentsOf: ASCII.colonSpace) - outBuffer.append(contentsOf: headerField.value.utf8) - outBuffer.append(contentsOf: ASCII.crlf) - } - outBuffer.append(contentsOf: ASCII.crlf) - } - - /// Writes the part body chunk into the buffer. - /// - Parameter bodyChunk: The body chunk to write. - private mutating func emitBodyChunk(_ bodyChunk: ArraySlice) { outBuffer.append(contentsOf: bodyChunk) } - - /// Writes an end of part boundary into the buffer. - private mutating func emitEndOfPart() { - outBuffer.append(contentsOf: ASCII.crlf) - outBuffer.append(contentsOf: ASCII.dashes) - outBuffer.append(contentsOf: boundary) - } - - /// Writes the start boundary into the buffer. - private mutating func emitStart() { - outBuffer.append(contentsOf: ASCII.dashes) - outBuffer.append(contentsOf: boundary) - } - - /// Writes the end double dash to the buffer. - private mutating func emitEnd() { - outBuffer.append(contentsOf: ASCII.dashes) - outBuffer.append(contentsOf: ASCII.crlf) - outBuffer.append(contentsOf: ASCII.crlf) - } -} - -extension MultipartSerializer { - - /// A state machine representing the multipart frame serializer. - struct StateMachine { - - /// The possible states of the state machine. - enum State: Hashable { - - /// Has not yet written any bytes. - case initial - - /// Emitted start, but no frames yet. - case emittedStart - - /// Finished, the terminal state. - case finished - - /// Last emitted a header fields frame. - case emittedHeaders - - /// Last emitted a part body chunk frame. - case emittedBodyChunk - } - - /// The current state of the state machine. - private(set) var state: State - - /// Creates a new state machine. - init() { self.state = .initial } - - /// An error returned by the state machine. - enum ActionError: Hashable { - - /// The first frame from upstream was not a header fields frame. - case noHeaderFieldsAtStart - } - - /// An action returned by the `next` method. - enum NextAction: Hashable { - - /// Return nil to the caller, no more bytes. - case returnNil - - /// Emit the initial boundary. - case emitStart - - /// Ready for the next frame. - case needsMore - } - - /// Read the next byte chunk serialized from upstream frames. - /// - Returns: An action to perform. - mutating func next() -> NextAction { - switch state { - case .initial: - state = .emittedStart - return .emitStart - case .finished: return .returnNil - case .emittedStart, .emittedHeaders, .emittedBodyChunk: return .needsMore - } - } - - /// An event to serialize to bytes. - enum Event: Hashable { - - /// The header fields of a part. - case headerFields(HTTPFields) - - /// A byte chunk of a part. - case bodyChunk(ArraySlice) - - /// A boundary between parts. - case endOfPart - - /// The initial boundary. - case start - - /// The final dashes. - case end - } - - /// An action returned by the `receivedFrame` method. - enum ReceivedFrameAction: Hashable { - - /// Return nil to the caller, no more bytes. - case returnNil - - /// Write the provided events as bytes. - case emitEvents([Event]) - - /// Throw the provided error. - case emitError(ActionError) - } - - /// Ingest the provided frame. - /// - Parameter frame: A new frame. If `nil`, then the source of frames is finished. - /// - Returns: An action to perform. - mutating func receivedFrame(_ frame: MultipartFrame?) -> ReceivedFrameAction { - switch state { - case .initial: preconditionFailure("Invalid state: \(state)") - case .finished: return .returnNil - case .emittedStart, .emittedHeaders, .emittedBodyChunk: break - } - switch (state, frame) { - case (.initial, _), (.finished, _): preconditionFailure("Already handled above.") - case (_, .none): - state = .finished - return .emitEvents([.endOfPart, .end]) - case (.emittedStart, .headerFields(let headerFields)): - state = .emittedHeaders - return .emitEvents([.headerFields(headerFields)]) - case (.emittedStart, .bodyChunk): - state = .finished - return .emitError(.noHeaderFieldsAtStart) - case (.emittedHeaders, .headerFields(let headerFields)), - (.emittedBodyChunk, .headerFields(let headerFields)): - state = .emittedHeaders - return .emitEvents([.endOfPart, .headerFields(headerFields)]) - case (.emittedHeaders, .bodyChunk(let bodyChunk)), (.emittedBodyChunk, .bodyChunk(let bodyChunk)): - state = .emittedBodyChunk - return .emitEvents([.bodyChunk(bodyChunk)]) - } - } - } -} diff --git a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBytesToFramesSequence.swift b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBytesToFramesSequence.swift index 88036301..acdee3f4 100644 --- a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBytesToFramesSequence.swift +++ b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBytesToFramesSequence.swift @@ -36,3 +36,146 @@ final class Test_MultipartBytesToFramesSequence: Test_Runtime { ) } } + +final class Test_MultipartParser: Test_Runtime { + func test() async throws { + var chunk = chunkFromStringLines([ + "--__abcd__", #"Content-Disposition: form-data; name="name""#, "", "24", "--__abcd__", + #"Content-Disposition: form-data; name="info""#, "", "{}", "--__abcd__--", + ]) + var parser = MultipartParser(boundary: "__abcd__") + let next: () async throws -> ArraySlice? = { + if let first = chunk.first { + let out: ArraySlice = [first] + chunk = chunk.dropFirst() + return out + } else { + return nil + } + } + var frames: [MultipartFrame] = [] + while let frame = try await parser.next(next) { frames.append(frame) } + XCTAssertEqual( + frames, + [ + .headerFields([.contentDisposition: #"form-data; name="name""#]), .bodyChunk(chunkFromString("2")), + .bodyChunk(chunkFromString("4")), .headerFields([.contentDisposition: #"form-data; name="info""#]), + .bodyChunk(chunkFromString("{")), .bodyChunk(chunkFromString("}")), + ] + ) + } +} + +private func newStateMachine() -> MultipartParser.StateMachine { .init(boundary: "__abcd__") } + +final class Test_MultipartParserStateMachine: Test_Runtime { + + func testInvalidInitialBoundary() throws { + var stateMachine = newStateMachine() + XCTAssertEqual(stateMachine.receivedChunk(chunkFromString("invalid")), .none) + XCTAssertEqual(stateMachine.readNextPart(), .emitError(.invalidInitialBoundary)) + } + + func testHeaderFields() throws { + var stateMachine = newStateMachine() + XCTAssertEqual(stateMachine.receivedChunk(chunkFromString("--__ab")), .none) + XCTAssertEqual(stateMachine.readNextPart(), .needsMore) + XCTAssertEqual(stateMachine.state, .parsingInitialBoundary(bufferFromString("--__ab"))) + XCTAssertEqual(stateMachine.receivedChunk(chunkFromString("cd__", addCRLFs: 1)), .none) + XCTAssertEqual(stateMachine.readNextPart(), .none) + XCTAssertEqual(stateMachine.state, .parsingPart([0x0d, 0x0a], .parsingHeaderFields(.init()))) + XCTAssertEqual(stateMachine.receivedChunk(chunkFromString(#"Content-Disposi"#)), .none) + XCTAssertEqual( + stateMachine.state, + .parsingPart([0x0d, 0x0a] + bufferFromString(#"Content-Disposi"#), .parsingHeaderFields(.init())) + ) + XCTAssertEqual(stateMachine.readNextPart(), .needsMore) + XCTAssertEqual( + stateMachine.receivedChunk(chunkFromString(#"tion: form-data; name="name""#, addCRLFs: 2)), + .none + ) + XCTAssertEqual( + stateMachine.state, + .parsingPart( + [0x0d, 0x0a] + bufferFromString(#"Content-Disposition: form-data; name="name""#) + [ + 0x0d, 0x0a, 0x0d, 0x0a, + ], + .parsingHeaderFields(.init()) + ) + ) + // Reads the first header field. + XCTAssertEqual(stateMachine.readNextPart(), .none) + // Reads the end of the header fields section. + XCTAssertEqual( + stateMachine.readNextPart(), + .emitHeaderFields([.contentDisposition: #"form-data; name="name""#]) + ) + XCTAssertEqual(stateMachine.state, .parsingPart([], .parsingBody)) + } + + func testPartBody() throws { + var stateMachine = newStateMachine() + let chunk = chunkFromStringLines(["--__abcd__", #"Content-Disposition: form-data; name="name""#, "", "24"]) + XCTAssertEqual(stateMachine.receivedChunk(chunk), .none) + XCTAssertEqual(stateMachine.state, .parsingInitialBoundary(Array(chunk))) + // Parse the initial boundary and first header field. + for _ in 0..<2 { XCTAssertEqual(stateMachine.readNextPart(), .none) } + // Parse the end of header fields. + XCTAssertEqual( + stateMachine.readNextPart(), + .emitHeaderFields([.contentDisposition: #"form-data; name="name""#]) + ) + XCTAssertEqual(stateMachine.state, .parsingPart(bufferFromString(#"24"#) + [0x0d, 0x0a], .parsingBody)) + XCTAssertEqual(stateMachine.receivedChunk(chunkFromString(".42")), .none) + XCTAssertEqual( + stateMachine.state, + .parsingPart(bufferFromString("24") + [0x0d, 0x0a] + bufferFromString(".42"), .parsingBody) + ) + XCTAssertEqual( + stateMachine.readNextPart(), + .emitBodyChunk(bufferFromString("24") + [0x0d, 0x0a] + bufferFromString(".42")) + ) + XCTAssertEqual(stateMachine.state, .parsingPart([], .parsingBody)) + XCTAssertEqual(stateMachine.receivedChunk([0x0d, 0x0a] + chunkFromString("--__ab")), .none) + XCTAssertEqual(stateMachine.state, .parsingPart([0x0d, 0x0a] + chunkFromString("--__ab"), .parsingBody)) + XCTAssertEqual(stateMachine.readNextPart(), .needsMore) + XCTAssertEqual(stateMachine.receivedChunk(chunkFromString("cd__--", addCRLFs: 1)), .none) + XCTAssertEqual( + stateMachine.state, + .parsingPart([0x0d, 0x0a] + chunkFromString("--__abcd__--", addCRLFs: 1), .parsingBody) + ) + // Parse the final boundary. + XCTAssertEqual(stateMachine.readNextPart(), .none) + // Parse the trailing two dashes. + XCTAssertEqual(stateMachine.readNextPart(), .returnNil) + } + + func testTwoParts() throws { + var stateMachine = newStateMachine() + let chunk = chunkFromStringLines([ + "--__abcd__", #"Content-Disposition: form-data; name="name""#, "", "24", "--__abcd__", + #"Content-Disposition: form-data; name="info""#, "", "{}", "--__abcd__--", + ]) + XCTAssertEqual(stateMachine.receivedChunk(chunk), .none) + // Parse the initial boundary and first header field. + for _ in 0..<2 { XCTAssertEqual(stateMachine.readNextPart(), .none) } + // Parse the end of header fields. + XCTAssertEqual( + stateMachine.readNextPart(), + .emitHeaderFields([.contentDisposition: #"form-data; name="name""#]) + ) + // Parse the first part's body. + XCTAssertEqual(stateMachine.readNextPart(), .emitBodyChunk(chunkFromString("24"))) + // Parse the boundary. + XCTAssertEqual(stateMachine.readNextPart(), .none) + // Parse the end of header fields. + XCTAssertEqual( + stateMachine.readNextPart(), + .emitHeaderFields([.contentDisposition: #"form-data; name="info""#]) + ) + // Parse the second part's body. + XCTAssertEqual(stateMachine.readNextPart(), .emitBodyChunk(chunkFromString("{}"))) + // Parse the trailing two dashes. + XCTAssertEqual(stateMachine.readNextPart(), .returnNil) + } +} diff --git a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartFramesToBytesSequence.swift b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartFramesToBytesSequence.swift index 257c9614..de487ed6 100644 --- a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartFramesToBytesSequence.swift +++ b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartFramesToBytesSequence.swift @@ -34,3 +34,66 @@ final class Test_MultipartFramesToBytesSequence: Test_Runtime { XCTAssertEqualData(bytes, expectedBytes) } } + +final class Test_MultipartSerializer: Test_Runtime { + func test() async throws { + let frames: [MultipartFrame] = [ + .headerFields([.contentDisposition: #"form-data; name="name""#]), .bodyChunk(chunkFromString("2")), + .bodyChunk(chunkFromString("4")), .headerFields([.contentDisposition: #"form-data; name="info""#]), + .bodyChunk(chunkFromString("{")), .bodyChunk(chunkFromString("}")), + ] + var serializer = MultipartSerializer(boundary: "__abcd__") + var iterator = frames.makeIterator() + var bytes: [UInt8] = [] + while let chunk = try await serializer.next({ iterator.next() }) { bytes.append(contentsOf: chunk) } + let expectedBytes = chunkFromStringLines([ + "--__abcd__", #"content-disposition: form-data; name="name""#, "", "24", "--__abcd__", + #"content-disposition: form-data; name="info""#, "", "{}", "--__abcd__--", "", + ]) + XCTAssertEqualData(bytes, expectedBytes) + } +} + +private func newStateMachine() -> MultipartSerializer.StateMachine { .init() } + +final class Test_MultipartSerializerStateMachine: Test_Runtime { + + func testInvalidFirstFrame() throws { + var stateMachine = newStateMachine() + XCTAssertEqual(stateMachine.next(), .emitStart) + XCTAssertEqual(stateMachine.next(), .needsMore) + XCTAssertEqual(stateMachine.receivedFrame(.bodyChunk([])), .emitError(.noHeaderFieldsAtStart)) + } + + func testTwoParts() throws { + var stateMachine = newStateMachine() + XCTAssertEqual(stateMachine.state, .initial) + XCTAssertEqual(stateMachine.next(), .emitStart) + XCTAssertEqual(stateMachine.state, .startedNothingEmittedYet) + XCTAssertEqual( + stateMachine.receivedFrame(.headerFields([.contentDisposition: #"form-data; name="name""#])), + .emitEvents([.headerFields([.contentDisposition: #"form-data; name="name""#])]) + ) + XCTAssertEqual(stateMachine.state, .emittedHeaders) + XCTAssertEqual(stateMachine.next(), .needsMore) + XCTAssertEqual( + stateMachine.receivedFrame(.bodyChunk(chunkFromString("24"))), + .emitEvents([.bodyChunk(chunkFromString("24"))]) + ) + XCTAssertEqual(stateMachine.state, .emittedBodyChunk) + XCTAssertEqual(stateMachine.next(), .needsMore) + XCTAssertEqual( + stateMachine.receivedFrame(.headerFields([.contentDisposition: #"form-data; name="info""#])), + .emitEvents([.endOfPart, .headerFields([.contentDisposition: #"form-data; name="info""#])]) + ) + XCTAssertEqual(stateMachine.state, .emittedHeaders) + XCTAssertEqual(stateMachine.next(), .needsMore) + XCTAssertEqual( + stateMachine.receivedFrame(.bodyChunk(chunkFromString("{}"))), + .emitEvents([.bodyChunk(chunkFromString("{}"))]) + ) + XCTAssertEqual(stateMachine.state, .emittedBodyChunk) + XCTAssertEqual(stateMachine.next(), .needsMore) + XCTAssertEqual(stateMachine.receivedFrame(nil), .emitEvents([.endOfPart, .end])) + } +} diff --git a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartFramesToRawPartsSequence.swift b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartFramesToRawPartsSequence.swift new file mode 100644 index 00000000..4a75b727 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartFramesToRawPartsSequence.swift @@ -0,0 +1,134 @@ +//===----------------------------------------------------------------------===// +// +// 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_MultipartFramesToRawPartsSequence: Test_Runtime { + func test() async throws { + let frames: [MultipartFrame] = [ + .headerFields([.contentDisposition: #"form-data; name="name""#]), .bodyChunk(chunkFromString("2")), + .bodyChunk(chunkFromString("4")), .headerFields([.contentDisposition: #"form-data; name="info""#]), + .bodyChunk(chunkFromString("{")), .bodyChunk(chunkFromString("}")), + ] + var upstreamIterator = frames.makeIterator() + let upstream = AsyncStream { upstreamIterator.next() } + let sequence = MultipartFramesToRawPartsSequence(upstream: upstream) + var iterator = sequence.makeAsyncIterator() + guard let part1 = try await iterator.next() else { + XCTFail("Missing part") + return + } + XCTAssertEqual(part1.headerFields, [.contentDisposition: #"form-data; name="name""#]) + try await XCTAssertEqualStringifiedData(part1.body, "24") + guard let part2 = try await iterator.next() else { + XCTFail("Missing part") + return + } + XCTAssertEqual(part2.headerFields, [.contentDisposition: #"form-data; name="info""#]) + try await XCTAssertEqualStringifiedData(part2.body, "{}") + + let part3 = try await iterator.next() + XCTAssertNil(part3) + } +} + +final class Test_MultipartFramesToRawPartsSequenceIterator: Test_Runtime { + func test() async throws { + let frames: [MultipartFrame] = [ + .headerFields([.contentDisposition: #"form-data; name="name""#]), .bodyChunk(chunkFromString("2")), + .bodyChunk(chunkFromString("4")), .headerFields([.contentDisposition: #"form-data; name="info""#]), + .bodyChunk(chunkFromString("{")), .bodyChunk(chunkFromString("}")), + ] + var upstreamSyncIterator = frames.makeIterator() + let upstream = AsyncStream { upstreamSyncIterator.next() } + let sharedIterator = MultipartFramesToRawPartsSequence> + .SharedIterator(makeUpstreamIterator: { upstream.makeAsyncIterator() }) + let bodyClosure: @Sendable () async throws -> ArraySlice? = { + try await sharedIterator.nextFromBodySubsequence() + } + guard let part1 = try await sharedIterator.nextFromPartSequence(bodyClosure: bodyClosure) else { + XCTFail("Missing part") + return + } + XCTAssertEqual(part1.headerFields, [.contentDisposition: #"form-data; name="name""#]) + try await XCTAssertEqualStringifiedData(part1.body, "24") + guard let part2 = try await sharedIterator.nextFromPartSequence(bodyClosure: bodyClosure) else { + XCTFail("Missing part") + return + } + XCTAssertEqual(part2.headerFields, [.contentDisposition: #"form-data; name="info""#]) + try await XCTAssertEqualStringifiedData(part2.body, "{}") + + let part3 = try await sharedIterator.nextFromPartSequence(bodyClosure: bodyClosure) + XCTAssertNil(part3) + } +} + +private func newStateMachine() -> MultipartFramesToRawPartsSequence>.StateMachine { + .init() +} + +final class Test_MultipartFramesToRawPartsSequenceIteratorStateMachine: Test_Runtime { + + func testInvalidFirstFrame() throws { + var stateMachine = newStateMachine() + XCTAssertEqual(stateMachine.state, .initial) + XCTAssertEqual(stateMachine.nextFromPartSequence(), .fetchFrame) + XCTAssertEqual(stateMachine.state, .waitingToSendHeaders(nil)) + XCTAssertEqual( + stateMachine.partReceivedFrame(.bodyChunk([])), + .emitError(.receivedBodyChunkWhenWaitingForHeaders) + ) + } + + func testTwoParts() throws { + var stateMachine = newStateMachine() + XCTAssertEqual(stateMachine.state, .initial) + XCTAssertEqual(stateMachine.nextFromPartSequence(), .fetchFrame) + XCTAssertEqual(stateMachine.state, .waitingToSendHeaders(nil)) + XCTAssertEqual( + stateMachine.partReceivedFrame(.headerFields([.contentDisposition: #"form-data; name="name""#])), + .emitPart([.contentDisposition: #"form-data; name="name""#]) + ) + XCTAssertEqual(stateMachine.state, .streamingBody) + XCTAssertEqual(stateMachine.nextFromBodySubsequence(), .fetchFrame) + XCTAssertEqual(stateMachine.state, .streamingBody) + XCTAssertEqual( + stateMachine.bodyReceivedFrame(.bodyChunk(chunkFromString("24"))), + .returnChunk(chunkFromString("24")) + ) + XCTAssertEqual(stateMachine.state, .streamingBody) + XCTAssertEqual(stateMachine.nextFromBodySubsequence(), .fetchFrame) + XCTAssertEqual( + stateMachine.bodyReceivedFrame(.headerFields([.contentDisposition: #"form-data; name="info""#])), + .returnNil + ) + XCTAssertEqual(stateMachine.state, .waitingToSendHeaders([.contentDisposition: #"form-data; name="info""#])) + XCTAssertEqual( + stateMachine.nextFromPartSequence(), + .emitPart([.contentDisposition: #"form-data; name="info""#]) + ) + XCTAssertEqual(stateMachine.state, .streamingBody) + XCTAssertEqual(stateMachine.nextFromBodySubsequence(), .fetchFrame) + XCTAssertEqual( + stateMachine.bodyReceivedFrame(.bodyChunk(chunkFromString("{}"))), + .returnChunk(chunkFromString("{}")) + ) + XCTAssertEqual(stateMachine.nextFromBodySubsequence(), .fetchFrame) + XCTAssertEqual(stateMachine.bodyReceivedFrame(nil), .returnNil) + XCTAssertEqual(stateMachine.state, .finished) + XCTAssertEqual(stateMachine.nextFromPartSequence(), .returnNil) + } +} diff --git a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartParser.swift b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartParser.swift deleted file mode 100644 index 5587868b..00000000 --- a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartParser.swift +++ /dev/null @@ -1,159 +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 -@_spi(Generated) @testable import OpenAPIRuntime -import Foundation - -final class Test_MultipartParser: Test_Runtime { - func test() async throws { - var chunk = chunkFromStringLines([ - "--__abcd__", #"Content-Disposition: form-data; name="name""#, "", "24", "--__abcd__", - #"Content-Disposition: form-data; name="info""#, "", "{}", "--__abcd__--", - ]) - var parser = MultipartParser(boundary: "__abcd__") - let next: () async throws -> ArraySlice? = { - if let first = chunk.first { - let out: ArraySlice = [first] - chunk = chunk.dropFirst() - return out - } else { - return nil - } - } - var frames: [MultipartFrame] = [] - while let frame = try await parser.next(next) { frames.append(frame) } - XCTAssertEqual( - frames, - [ - .headerFields([.contentDisposition: #"form-data; name="name""#]), .bodyChunk(chunkFromString("2")), - .bodyChunk(chunkFromString("4")), .headerFields([.contentDisposition: #"form-data; name="info""#]), - .bodyChunk(chunkFromString("{")), .bodyChunk(chunkFromString("}")), - ] - ) - } -} - -private func newStateMachine() -> MultipartParser.StateMachine { .init(boundary: "__abcd__") } - -final class Test_MultipartParserStateMachine: Test_Runtime { - - func testInvalidInitialBoundary() throws { - var stateMachine = newStateMachine() - XCTAssertEqual(stateMachine.receivedChunk(chunkFromString("invalid")), .none) - XCTAssertEqual(stateMachine.readNextPart(), .emitError(.invalidInitialBoundary)) - } - - func testHeaderFields() throws { - var stateMachine = newStateMachine() - XCTAssertEqual(stateMachine.receivedChunk(chunkFromString("--__ab")), .none) - XCTAssertEqual(stateMachine.readNextPart(), .needsMore) - XCTAssertEqual(stateMachine.state, .parsingInitialBoundary(bufferFromString("--__ab"))) - XCTAssertEqual(stateMachine.receivedChunk(chunkFromString("cd__", addCRLFs: 1)), .none) - XCTAssertEqual(stateMachine.readNextPart(), .none) - XCTAssertEqual(stateMachine.state, .parsingPart([0x0d, 0x0a], .parsingHeaderFields(.init()))) - XCTAssertEqual(stateMachine.receivedChunk(chunkFromString(#"Content-Disposi"#)), .none) - XCTAssertEqual( - stateMachine.state, - .parsingPart([0x0d, 0x0a] + bufferFromString(#"Content-Disposi"#), .parsingHeaderFields(.init())) - ) - XCTAssertEqual(stateMachine.readNextPart(), .needsMore) - XCTAssertEqual( - stateMachine.receivedChunk(chunkFromString(#"tion: form-data; name="name""#, addCRLFs: 2)), - .none - ) - XCTAssertEqual( - stateMachine.state, - .parsingPart( - [0x0d, 0x0a] + bufferFromString(#"Content-Disposition: form-data; name="name""#) + [ - 0x0d, 0x0a, 0x0d, 0x0a, - ], - .parsingHeaderFields(.init()) - ) - ) - // Reads the first header field. - XCTAssertEqual(stateMachine.readNextPart(), .none) - // Reads the end of the header fields section. - XCTAssertEqual( - stateMachine.readNextPart(), - .emitHeaderFields([.contentDisposition: #"form-data; name="name""#]) - ) - XCTAssertEqual(stateMachine.state, .parsingPart([], .parsingBody)) - } - - func testPartBody() throws { - var stateMachine = newStateMachine() - let chunk = chunkFromStringLines(["--__abcd__", #"Content-Disposition: form-data; name="name""#, "", "24"]) - XCTAssertEqual(stateMachine.receivedChunk(chunk), .none) - XCTAssertEqual(stateMachine.state, .parsingInitialBoundary(Array(chunk))) - // Parse the initial boundary and first header field. - for _ in 0..<2 { XCTAssertEqual(stateMachine.readNextPart(), .none) } - // Parse the end of header fields. - XCTAssertEqual( - stateMachine.readNextPart(), - .emitHeaderFields([.contentDisposition: #"form-data; name="name""#]) - ) - XCTAssertEqual(stateMachine.state, .parsingPart(bufferFromString(#"24"#) + [0x0d, 0x0a], .parsingBody)) - XCTAssertEqual(stateMachine.receivedChunk(chunkFromString(".42")), .none) - XCTAssertEqual( - stateMachine.state, - .parsingPart(bufferFromString("24") + [0x0d, 0x0a] + bufferFromString(".42"), .parsingBody) - ) - XCTAssertEqual( - stateMachine.readNextPart(), - .emitBodyChunk(bufferFromString("24") + [0x0d, 0x0a] + bufferFromString(".42")) - ) - XCTAssertEqual(stateMachine.state, .parsingPart([], .parsingBody)) - XCTAssertEqual(stateMachine.receivedChunk([0x0d, 0x0a] + chunkFromString("--__ab")), .none) - XCTAssertEqual(stateMachine.state, .parsingPart([0x0d, 0x0a] + chunkFromString("--__ab"), .parsingBody)) - XCTAssertEqual(stateMachine.readNextPart(), .needsMore) - XCTAssertEqual(stateMachine.receivedChunk(chunkFromString("cd__--", addCRLFs: 1)), .none) - XCTAssertEqual( - stateMachine.state, - .parsingPart([0x0d, 0x0a] + chunkFromString("--__abcd__--", addCRLFs: 1), .parsingBody) - ) - // Parse the final boundary. - XCTAssertEqual(stateMachine.readNextPart(), .none) - // Parse the trailing two dashes. - XCTAssertEqual(stateMachine.readNextPart(), .returnNil) - } - - func testTwoParts() throws { - var stateMachine = newStateMachine() - let chunk = chunkFromStringLines([ - "--__abcd__", #"Content-Disposition: form-data; name="name""#, "", "24", "--__abcd__", - #"Content-Disposition: form-data; name="info""#, "", "{}", "--__abcd__--", - ]) - XCTAssertEqual(stateMachine.receivedChunk(chunk), .none) - // Parse the initial boundary and first header field. - for _ in 0..<2 { XCTAssertEqual(stateMachine.readNextPart(), .none) } - // Parse the end of header fields. - XCTAssertEqual( - stateMachine.readNextPart(), - .emitHeaderFields([.contentDisposition: #"form-data; name="name""#]) - ) - // Parse the first part's body. - XCTAssertEqual(stateMachine.readNextPart(), .emitBodyChunk(chunkFromString("24"))) - // Parse the boundary. - XCTAssertEqual(stateMachine.readNextPart(), .none) - // Parse the end of header fields. - XCTAssertEqual( - stateMachine.readNextPart(), - .emitHeaderFields([.contentDisposition: #"form-data; name="info""#]) - ) - // Parse the second part's body. - XCTAssertEqual(stateMachine.readNextPart(), .emitBodyChunk(chunkFromString("{}"))) - // Parse the trailing two dashes. - XCTAssertEqual(stateMachine.readNextPart(), .returnNil) - } -} diff --git a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartSerializer.swift b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartSerializer.swift deleted file mode 100644 index 7dd96a64..00000000 --- a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartSerializer.swift +++ /dev/null @@ -1,79 +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 -@_spi(Generated) @testable import OpenAPIRuntime -import Foundation - -final class Test_MultipartSerializer: Test_Runtime { - func test() async throws { - let frames: [MultipartFrame] = [ - .headerFields([.contentDisposition: #"form-data; name="name""#]), .bodyChunk(chunkFromString("2")), - .bodyChunk(chunkFromString("4")), .headerFields([.contentDisposition: #"form-data; name="info""#]), - .bodyChunk(chunkFromString("{")), .bodyChunk(chunkFromString("}")), - ] - var serializer = MultipartSerializer(boundary: "__abcd__") - var iterator = frames.makeIterator() - var bytes: [UInt8] = [] - while let chunk = try await serializer.next({ iterator.next() }) { bytes.append(contentsOf: chunk) } - let expectedBytes = chunkFromStringLines([ - "--__abcd__", #"content-disposition: form-data; name="name""#, "", "24", "--__abcd__", - #"content-disposition: form-data; name="info""#, "", "{}", "--__abcd__--", "", - ]) - XCTAssertEqualData(bytes, expectedBytes) - } -} - -private func newStateMachine() -> MultipartSerializer.StateMachine { .init() } - -final class Test_MultipartSerializerStateMachine: Test_Runtime { - - func testInvalidFirstFrame() throws { - var stateMachine = newStateMachine() - XCTAssertEqual(stateMachine.next(), .emitStart) - XCTAssertEqual(stateMachine.next(), .needsMore) - XCTAssertEqual(stateMachine.receivedFrame(.bodyChunk([])), .emitError(.noHeaderFieldsAtStart)) - } - - func testTwoParts() throws { - var stateMachine = newStateMachine() - XCTAssertEqual(stateMachine.state, .initial) - XCTAssertEqual(stateMachine.next(), .emitStart) - XCTAssertEqual(stateMachine.state, .emittedStart) - XCTAssertEqual( - stateMachine.receivedFrame(.headerFields([.contentDisposition: #"form-data; name="name""#])), - .emitEvents([.headerFields([.contentDisposition: #"form-data; name="name""#])]) - ) - XCTAssertEqual(stateMachine.state, .emittedHeaders) - XCTAssertEqual(stateMachine.next(), .needsMore) - XCTAssertEqual( - stateMachine.receivedFrame(.bodyChunk(chunkFromString("24"))), - .emitEvents([.bodyChunk(chunkFromString("24"))]) - ) - XCTAssertEqual(stateMachine.state, .emittedBodyChunk) - XCTAssertEqual(stateMachine.next(), .needsMore) - XCTAssertEqual( - stateMachine.receivedFrame(.headerFields([.contentDisposition: #"form-data; name="info""#])), - .emitEvents([.endOfPart, .headerFields([.contentDisposition: #"form-data; name="info""#])]) - ) - XCTAssertEqual(stateMachine.state, .emittedHeaders) - XCTAssertEqual(stateMachine.next(), .needsMore) - XCTAssertEqual( - stateMachine.receivedFrame(.bodyChunk(chunkFromString("{}"))), - .emitEvents([.bodyChunk(chunkFromString("{}"))]) - ) - XCTAssertEqual(stateMachine.state, .emittedBodyChunk) - XCTAssertEqual(stateMachine.next(), .needsMore) - XCTAssertEqual(stateMachine.receivedFrame(nil), .emitEvents([.endOfPart, .end])) - } -} diff --git a/docker/docker-compose.2204.510.yaml b/docker/docker-compose.2204.510.yaml index d031df5a..02e5d46e 100644 --- a/docker/docker-compose.2204.510.yaml +++ b/docker/docker-compose.2204.510.yaml @@ -12,7 +12,9 @@ services: environment: - WARN_AS_ERROR_ARG=-Xswiftc -warnings-as-errors - IMPORT_CHECK_ARG=--explicit-target-dependency-import-check error - - STRICT_CONCURRENCY_ARG=-Xswiftc -strict-concurrency=complete + # Disabled strict concurrency checking as currently it's not possible to iterate an async sequence + # from inside an actor without warnings. + # - STRICT_CONCURRENCY_ARG=-Xswiftc -strict-concurrency=complete shell: image: *image From 304808aaebda67cdc08cefc7b95bc34f0bff36a8 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 17 Nov 2023 18:15:28 +0100 Subject: [PATCH 07/20] [Multipart] Add a raw parts to frames serializer sequence. (#75) --- .../MultipartRawPartsToFramesSequence.swift | 219 ++++++++++++++++++ ...st_MultipartRawPartsToFramesSequence.swift | 144 ++++++++++++ 2 files changed, 363 insertions(+) create mode 100644 Sources/OpenAPIRuntime/Multipart/MultipartRawPartsToFramesSequence.swift create mode 100644 Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartRawPartsToFramesSequence.swift diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartRawPartsToFramesSequence.swift b/Sources/OpenAPIRuntime/Multipart/MultipartRawPartsToFramesSequence.swift new file mode 100644 index 00000000..343eb0f2 --- /dev/null +++ b/Sources/OpenAPIRuntime/Multipart/MultipartRawPartsToFramesSequence.swift @@ -0,0 +1,219 @@ +//===----------------------------------------------------------------------===// +// +// 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 HTTPTypes +import Foundation + +/// A sequence that serializes raw multipart parts into multipart frames. +struct MultipartRawPartsToFramesSequence: Sendable +where Upstream.Element == MultipartRawPart { + + /// The source of raw parts. + var upstream: Upstream +} + +extension MultipartRawPartsToFramesSequence: AsyncSequence { + + /// The type of element produced by this asynchronous sequence. + typealias Element = MultipartFrame + + /// Creates the asynchronous iterator that produces elements of this + /// asynchronous sequence. + /// + /// - Returns: An instance of the `AsyncIterator` type used to produce + /// elements of the asynchronous sequence. + func makeAsyncIterator() -> Iterator { Iterator(upstream: upstream.makeAsyncIterator()) } + + /// An iterator that pulls raw parts from the upstream iterator and provides + /// multipart frames. + struct Iterator: AsyncIteratorProtocol { + + /// The iterator that provides the raw parts. + var upstream: Upstream.AsyncIterator + + /// The underlying parts to frames serializer. + var serializer: Serializer + + /// Creates a new iterator. + /// - Parameter upstream: The iterator that provides the raw parts. + init(upstream: Upstream.AsyncIterator) { + self.upstream = upstream + self.serializer = .init(upstream: upstream) + } + + /// Asynchronously advances to the next element and returns it, or ends the + /// sequence if there is no next element. + /// + /// - Returns: The next element, if it exists, or `nil` to signal the end of + /// the sequence. + mutating func next() async throws -> Element? { try await serializer.next() } + } +} + +extension MultipartRawPartsToFramesSequence { + + /// A state machine representing the raw part to frame serializer. + struct StateMachine { + + /// The possible states of the state machine. + enum State { + + /// Has not emitted any frames yet. + case initial + + /// Waiting for the next part. + case waitingForPart + + /// Returning body chunks from the current part's body. + case streamingBody(HTTPBody.AsyncIterator) + + /// Finished, the terminal state. + case finished + } + + /// The current state of the state machine. + private(set) var state: State + + /// Creates a new state machine. + init() { self.state = .initial } + + /// An action returned by the `next` method. + enum NextAction { + + /// Return nil to the caller, no more parts. + case returnNil + + /// Fetch the next part. + case fetchPart + + /// Fetch the next body chunk from the provided iterator. + case fetchBodyChunk(HTTPBody.AsyncIterator) + } + + /// Read the next part from the upstream frames. + /// - Returns: An action to perform. + mutating func next() -> NextAction { + switch state { + case .initial: + state = .waitingForPart + return .fetchPart + case .streamingBody(let iterator): return .fetchBodyChunk(iterator) + case .finished: return .returnNil + case .waitingForPart: preconditionFailure("Invalid state: \(state)") + } + } + + /// An action returned by the `receivedPart` method. + enum ReceivedPartAction: Hashable { + + /// Return nil to the caller, no more frames. + case returnNil + + /// Return the provided header fields. + case emitHeaderFields(HTTPFields) + } + + /// Ingest the provided part. + /// - Parameter part: A new part. If `nil`, then the source of parts is finished. + /// - Returns: An action to perform. + mutating func receivedPart(_ part: MultipartRawPart?) -> ReceivedPartAction { + switch state { + case .waitingForPart: + if let part { + state = .streamingBody(part.body.makeAsyncIterator()) + return .emitHeaderFields(part.headerFields) + } else { + state = .finished + return .returnNil + } + case .finished: return .returnNil + case .initial, .streamingBody: preconditionFailure("Invalid state: \(state)") + } + } + + /// An action returned by the `receivedBodyChunk` method. + enum ReceivedBodyChunkAction: Hashable { + + /// Return nil to the caller, no more frames. + case returnNil + + /// Fetch the next part. + case fetchPart + + /// Return the provided body chunk. + case emitBodyChunk(ArraySlice) + } + + /// Ingest the provided part. + /// - Parameter bodyChunk: A new body chunk. If `nil`, then the current part's body is finished. + /// - Returns: An action to perform. + mutating func receivedBodyChunk(_ bodyChunk: ArraySlice?) -> ReceivedBodyChunkAction { + switch state { + case .streamingBody: + if let bodyChunk { + return .emitBodyChunk(bodyChunk) + } else { + state = .waitingForPart + return .fetchPart + } + case .finished: return .returnNil + case .initial, .waitingForPart: preconditionFailure("Invalid state: \(state)") + } + } + } +} + +extension MultipartRawPartsToFramesSequence { + + /// A serializer of multipart raw parts into multipart frames. + struct Serializer { + + /// The upstream source of raw parts. + private var upstream: Upstream.AsyncIterator + + /// The underlying state machine. + private var stateMachine: StateMachine + + /// Creates a new iterator. + /// - Parameter upstream: The upstream source of raw parts. + init(upstream: Upstream.AsyncIterator) { + self.upstream = upstream + self.stateMachine = .init() + } + + /// Requests the next frame. + /// - Returns: A frame. + /// - Throws: When a serialization error is encountered. + mutating func next() async throws -> MultipartFrame? { + func handleFetchPart() async throws -> MultipartFrame? { + let part = try await upstream.next() + switch stateMachine.receivedPart(part) { + case .returnNil: return nil + case .emitHeaderFields(let headerFields): return .headerFields(headerFields) + } + } + switch stateMachine.next() { + case .returnNil: return nil + case .fetchPart: return try await handleFetchPart() + case .fetchBodyChunk(var iterator): + let bodyChunk = try await iterator.next() + switch stateMachine.receivedBodyChunk(bodyChunk) { + case .returnNil: return nil + case .fetchPart: return try await handleFetchPart() + case .emitBodyChunk(let bodyChunk): return .bodyChunk(bodyChunk) + } + } + } + } +} diff --git a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartRawPartsToFramesSequence.swift b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartRawPartsToFramesSequence.swift new file mode 100644 index 00000000..5017e532 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartRawPartsToFramesSequence.swift @@ -0,0 +1,144 @@ +//===----------------------------------------------------------------------===// +// +// 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_MultipartRawPartsToFramesSequence: Test_Runtime { + func test() async throws { + var secondPartChunks = "{}".utf8.makeIterator() + let secondPartBody = HTTPBody( + AsyncStream(unfolding: { secondPartChunks.next().map { ArraySlice([$0]) } }), + length: .unknown + ) + let parts: [MultipartRawPart] = [ + .init(headerFields: [.contentDisposition: #"form-data; name="name""#], body: "24"), + .init(headerFields: [.contentDisposition: #"form-data; name="info""#], body: secondPartBody), + ] + var upstreamIterator = parts.makeIterator() + let upstream = AsyncStream { upstreamIterator.next() } + let sequence = MultipartRawPartsToFramesSequence(upstream: upstream) + + var frames: [MultipartFrame] = [] + for try await frame in sequence { frames.append(frame) } + let expectedFrames: [MultipartFrame] = [ + .headerFields([.contentDisposition: #"form-data; name="name""#]), .bodyChunk(chunkFromString("24")), + .headerFields([.contentDisposition: #"form-data; name="info""#]), .bodyChunk(chunkFromString("{")), + .bodyChunk(chunkFromString("}")), + ] + XCTAssertEqual(frames, expectedFrames) + } +} + +final class Test_MultipartRawPartsToFramesSequenceSerializer: Test_Runtime { + func test() async throws { + var secondPartChunks = "{}".utf8.makeIterator() + let secondPartBody = HTTPBody( + AsyncStream(unfolding: { secondPartChunks.next().map { ArraySlice([$0]) } }), + length: .unknown + ) + let parts: [MultipartRawPart] = [ + .init(headerFields: [.contentDisposition: #"form-data; name="name""#], body: "24"), + .init(headerFields: [.contentDisposition: #"form-data; name="info""#], body: secondPartBody), + ] + var upstreamIterator = parts.makeIterator() + let upstream = AsyncStream { upstreamIterator.next() } + var serializer = MultipartRawPartsToFramesSequence> + .Serializer(upstream: upstream.makeAsyncIterator()) + var frames: [MultipartFrame] = [] + while let frame = try await serializer.next() { frames.append(frame) } + let expectedFrames: [MultipartFrame] = [ + .headerFields([.contentDisposition: #"form-data; name="name""#]), .bodyChunk(chunkFromString("24")), + .headerFields([.contentDisposition: #"form-data; name="info""#]), .bodyChunk(chunkFromString("{")), + .bodyChunk(chunkFromString("}")), + ] + XCTAssertEqual(frames, expectedFrames) + } +} + +private func newStateMachine() -> MultipartRawPartsToFramesSequence>.StateMachine { + .init() +} + +final class Test_MultipartRawPartsToFramesSequenceStateMachine: Test_Runtime { + + func testTwoParts() throws { + var stateMachine = newStateMachine() + XCTAssertTrue(stateMachine.state.isInitial) + XCTAssertTrue(stateMachine.next().isFetchPart) + XCTAssertTrue(stateMachine.state.isWaitingForPart) + XCTAssertEqual( + stateMachine.receivedPart( + .init(headerFields: [.contentDisposition: #"form-data; name="name""#], body: "24") + ), + .emitHeaderFields([.contentDisposition: #"form-data; name="name""#]) + ) + XCTAssertTrue(stateMachine.state.isStreamingBody) + XCTAssertTrue(stateMachine.next().isFetchBodyChunk) + XCTAssertEqual(stateMachine.receivedBodyChunk(chunkFromString("24")), .emitBodyChunk(chunkFromString("24"))) + XCTAssertTrue(stateMachine.state.isStreamingBody) + XCTAssertTrue(stateMachine.next().isFetchBodyChunk) + XCTAssertEqual(stateMachine.receivedBodyChunk(nil), .fetchPart) + XCTAssertEqual( + stateMachine.receivedPart( + .init(headerFields: [.contentDisposition: #"form-data; name="info""#], body: "{}") + ), + .emitHeaderFields([.contentDisposition: #"form-data; name="info""#]) + ) + XCTAssertTrue(stateMachine.state.isStreamingBody) + XCTAssertTrue(stateMachine.next().isFetchBodyChunk) + XCTAssertEqual(stateMachine.receivedBodyChunk(chunkFromString("{")), .emitBodyChunk(chunkFromString("{"))) + XCTAssertTrue(stateMachine.state.isStreamingBody) + XCTAssertTrue(stateMachine.next().isFetchBodyChunk) + XCTAssertEqual(stateMachine.receivedBodyChunk(chunkFromString("}")), .emitBodyChunk(chunkFromString("}"))) + XCTAssertTrue(stateMachine.state.isStreamingBody) + XCTAssertTrue(stateMachine.next().isFetchBodyChunk) + XCTAssertEqual(stateMachine.receivedBodyChunk(nil), .fetchPart) + XCTAssertEqual(stateMachine.receivedPart(nil), .returnNil) + } +} + +extension MultipartRawPartsToFramesSequence.StateMachine.State { + var isInitial: Bool { + guard case .initial = self else { return false } + return true + } + var isWaitingForPart: Bool { + guard case .waitingForPart = self else { return false } + return true + } + var isStreamingBody: Bool { + guard case .streamingBody = self else { return false } + return true + } + var isFinished: Bool { + guard case .finished = self else { return false } + return true + } +} + +extension MultipartRawPartsToFramesSequence.StateMachine.NextAction { + var isReturnNil: Bool { + guard case .returnNil = self else { return false } + return true + } + var isFetchPart: Bool { + guard case .fetchPart = self else { return false } + return true + } + var isFetchBodyChunk: Bool { + guard case .fetchBodyChunk = self else { return false } + return true + } +} From d50b48957ccb388fb89db98a56c2337276298e79 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 20 Nov 2023 12:42:08 +0100 Subject: [PATCH 08/20] [Multipart] Validation sequence (#76) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Multipart] Validation sequence ### Motivation The OpenAPI document provides information about which parts are required, optional, arrays, and single values, so we need to enforce those semantics for the adopter, just like we enforce (using JSONDecoder) that a received JSON payload follows the documented structure. ### Modifications Since the mutlipart body is not a struct, but an async sequence of parts, it's a little more complicated. We introduce a `MultipartValidationSequence` with a state machine that keeps track of the requirements and which of them have already been fulfilled over time. And it throws an error if any of the requirements are violated. For missing required parts, the error is thrown when `nil` is received from the upstream sequence, indicating that there will be no more parts coming. To implement this, an internal type `ContentDisposition` was also introduced for working with that header's values, and helper accessors on `MultipartRawPart` as well. ### Result Adopters don't have to validate these semantics manually, if they successfully iterate over the parts without an error being thrown, they can be confident that the received (or sent) parts match the requirements from the OpenAPI document. ### Test Plan Unit tests for the sequence, the validator, and the state machine were added. Also added unit tests for the `ContentDisposition` type. Reviewed by: simonjbeaumont Builds: ✔︎ pull request validation (5.10) - Build finished. ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (api breakage) - Build finished. ✔︎ pull request validation (docc test) - Build finished. ✔︎ pull request validation (integration test) - Build finished. ✔︎ pull request validation (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. https://github.com/apple/swift-openapi-runtime/pull/76 --- .../Base/ContentDisposition.swift | 128 ++++++++ .../MultipartPublicTypesExtensions.swift | 78 +++++ .../Multipart/MultipartValidation.swift | 282 +++++++++++++++++ .../Base/Test_ContentDisposition.swift | 85 ++++++ .../Test_MultipartValidationSequence.swift | 283 ++++++++++++++++++ 5 files changed, 856 insertions(+) create mode 100644 Sources/OpenAPIRuntime/Base/ContentDisposition.swift create mode 100644 Sources/OpenAPIRuntime/Multipart/MultipartPublicTypesExtensions.swift create mode 100644 Sources/OpenAPIRuntime/Multipart/MultipartValidation.swift create mode 100644 Tests/OpenAPIRuntimeTests/Base/Test_ContentDisposition.swift create mode 100644 Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartValidationSequence.swift diff --git a/Sources/OpenAPIRuntime/Base/ContentDisposition.swift b/Sources/OpenAPIRuntime/Base/ContentDisposition.swift new file mode 100644 index 00000000..c0b25074 --- /dev/null +++ b/Sources/OpenAPIRuntime/Base/ContentDisposition.swift @@ -0,0 +1,128 @@ +//===----------------------------------------------------------------------===// +// +// 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 parsed representation of the `content-disposition` header described by RFC 6266 containing only +/// the features relevant to OpenAPI multipart bodies. +struct ContentDisposition: Hashable { + + /// A `disposition-type` parameter value. + enum DispositionType: Hashable { + + /// A form data value. + case formData + + /// Any other value. + case other(String) + + /// Creates a new disposition type value. + /// - Parameter rawValue: A string representation of the value. + init(rawValue: String) { + switch rawValue.lowercased() { + case "form-data": self = .formData + default: self = .other(rawValue) + } + } + + /// A string representation of the value. + var rawValue: String { + switch self { + case .formData: return "form-data" + case .other(let string): return string + } + } + } + + /// The disposition type value. + var dispositionType: DispositionType + + /// A content disposition parameter name. + enum ParameterName: Hashable { + + /// The name parameter. + case name + + /// The filename parameter. + case filename + + /// Any other parameter. + case other(String) + + /// Creates a new parameter name. + /// - Parameter rawValue: A string representation of the name. + init(rawValue: String) { + switch rawValue.lowercased() { + case "name": self = .name + case "filename": self = .filename + default: self = .other(rawValue) + } + } + + /// A string representation of the name. + var rawValue: String { + switch self { + case .name: return "name" + case .filename: return "filename" + case .other(let string): return string + } + } + } + + /// The parameters of the content disposition value. + var parameters: [ParameterName: String] = [:] + + /// The name parameter value. + var name: String? { + get { parameters[.name] } + set { parameters[.name] = newValue } + } + + /// The filename parameter value. + var filename: String? { + get { parameters[.filename] } + set { parameters[.filename] = newValue } + } +} + +extension ContentDisposition: RawRepresentable { + + /// Creates a new instance with the specified raw value. + /// + /// https://datatracker.ietf.org/doc/html/rfc6266#section-4.1 + /// - Parameter rawValue: The raw value to use for the new instance. + init?(rawValue: String) { + var components = rawValue.split(separator: ";").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + guard !components.isEmpty else { return nil } + self.dispositionType = DispositionType(rawValue: components.removeFirst()) + let parameterTuples: [(ParameterName, String)] = components.compactMap { component in + let parameterComponents = component.split(separator: "=", maxSplits: 1) + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + guard parameterComponents.count == 2 else { return nil } + let valueWithoutQuotes = parameterComponents[1].trimmingCharacters(in: ["\""]) + return (.init(rawValue: parameterComponents[0]), valueWithoutQuotes) + } + self.parameters = Dictionary(parameterTuples, uniquingKeysWith: { a, b in a }) + } + + /// The corresponding value of the raw type. + var rawValue: String { + var string = "" + string.append(dispositionType.rawValue) + if !parameters.isEmpty { + for (key, value) in parameters.sorted(by: { $0.key.rawValue < $1.key.rawValue }) { + string.append("; \(key.rawValue)=\"\(value)\"") + } + } + return string + } +} diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartPublicTypesExtensions.swift b/Sources/OpenAPIRuntime/Multipart/MultipartPublicTypesExtensions.swift new file mode 100644 index 00000000..ac9d9d5f --- /dev/null +++ b/Sources/OpenAPIRuntime/Multipart/MultipartPublicTypesExtensions.swift @@ -0,0 +1,78 @@ +//===----------------------------------------------------------------------===// +// +// 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 +import HTTPTypes + +// MARK: - Extensions + +extension MultipartRawPart { + + /// Creates a new raw part by injecting the provided name and filename into + /// the `content-disposition` header field. + /// - Parameters: + /// - name: The name of the part. + /// - filename: The file name of the part. + /// - headerFields: The header fields of the part. + /// - body: The body stream of the part. + public init(name: String?, filename: String? = nil, headerFields: HTTPFields, body: HTTPBody) { + var parameters: [ContentDisposition.ParameterName: String] = [:] + if let name { parameters[.name] = name } + if let filename { parameters[.filename] = filename } + let contentDisposition = ContentDisposition(dispositionType: .formData, parameters: parameters) + var headerFields = headerFields + headerFields[.contentDisposition] = contentDisposition.rawValue + self.init(headerFields: headerFields, body: body) + } + + /// Returns the parameter value for the provided name. + /// - Parameter name: The parameter name. + /// - Returns: The parameter value. Nil if not found in the content disposition header field. + private func getParameter(_ name: ContentDisposition.ParameterName) -> String? { + guard let contentDispositionString = headerFields[.contentDisposition], + let contentDisposition = ContentDisposition(rawValue: contentDispositionString) + else { return nil } + return contentDisposition.parameters[name] + } + + /// Sets the parameter name to the provided value. + /// - Parameters: + /// - name: The parameter name. + /// - value: The value of the parameter. + private mutating func setParameter(_ name: ContentDisposition.ParameterName, _ value: String?) { + guard let contentDispositionString = headerFields[.contentDisposition], + var contentDisposition = ContentDisposition(rawValue: contentDispositionString) + else { + if let value { + headerFields[.contentDisposition] = + ContentDisposition(dispositionType: .formData, parameters: [name: value]).rawValue + } + return + } + contentDisposition.parameters[name] = value + headerFields[.contentDisposition] = contentDisposition.rawValue + } + + /// The name of the part stored in the `content-disposition` header field. + public var name: String? { + get { getParameter(.name) } + set { setParameter(.name, newValue) } + } + + /// The file name of the part stored in the `content-disposition` header field. + public var filename: String? { + get { getParameter(.filename) } + set { setParameter(.filename, newValue) } + } +} diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartValidation.swift b/Sources/OpenAPIRuntime/Multipart/MultipartValidation.swift new file mode 100644 index 00000000..dbac2bc8 --- /dev/null +++ b/Sources/OpenAPIRuntime/Multipart/MultipartValidation.swift @@ -0,0 +1,282 @@ +//===----------------------------------------------------------------------===// +// +// 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 HTTPTypes +import Foundation + +/// A container for multipart body requirements. +struct MultipartBodyRequirements: Sendable, Hashable { + + /// A Boolean value indicating whether unknown part names are allowed. + var allowsUnknownParts: Bool + + /// A set of known part names that must appear exactly once. + var requiredExactlyOncePartNames: Set + + /// A set of known part names that must appear at least once. + var requiredAtLeastOncePartNames: Set + + /// A set of known part names that can appear at most once. + var atMostOncePartNames: Set + + /// A set of known part names that can appear any number of times. + var zeroOrMoreTimesPartNames: Set +} + +/// A sequence that validates that the raw parts passing through the sequence match the provided semantics. +struct MultipartValidationSequence: Sendable +where Upstream.Element == MultipartRawPart { + + /// The source of raw parts. + var upstream: Upstream + + /// The requirements to enforce. + var requirements: MultipartBodyRequirements +} + +extension MultipartValidationSequence: AsyncSequence { + + /// The type of element produced by this asynchronous sequence. + typealias Element = MultipartRawPart + + /// Creates the asynchronous iterator that produces elements of this + /// asynchronous sequence. + /// + /// - Returns: An instance of the `AsyncIterator` type used to produce + /// elements of the asynchronous sequence. + func makeAsyncIterator() -> Iterator { + Iterator(upstream: upstream.makeAsyncIterator(), requirements: requirements) + } + + /// An iterator that pulls raw parts from the upstream iterator and validates their semantics. + struct Iterator: AsyncIteratorProtocol { + + /// The iterator that provides the raw parts. + var upstream: Upstream.AsyncIterator + + /// The underlying requirements validator. + var validator: Validator + + /// Creates a new iterator. + /// - Parameters: + /// - upstream: The iterator that provides the raw parts. + /// - requirements: The requirements to enforce. + init(upstream: Upstream.AsyncIterator, requirements: MultipartBodyRequirements) { + self.upstream = upstream + self.validator = .init(requirements: requirements) + } + + /// Asynchronously advances to the next element and returns it, or ends the + /// sequence if there is no next element. + /// + /// - Returns: The next element, if it exists, or `nil` to signal the end of + /// the sequence. + mutating func next() async throws -> Element? { try await validator.next(upstream.next()) } + } +} + +extension MultipartValidationSequence { + + /// A state machine representing the validator. + struct StateMachine { + + /// The state of the state machine. + struct State: Hashable { + + /// A Boolean value indicating whether unknown part names are allowed. + let allowsUnknownParts: Bool + + /// A set of known part names that must appear exactly once. + let exactlyOncePartNames: Set + + /// A set of known part names that must appear at least once. + let atLeastOncePartNames: Set + + /// A set of known part names that can appear at most once. + let atMostOncePartNames: Set + + /// A set of known part names that can appear any number of times. + let zeroOrMoreTimesPartNames: Set + + /// The remaining part names that must appear exactly once. + var remainingExactlyOncePartNames: Set + + /// The remaining part names that must appear at least once. + var remainingAtLeastOncePartNames: Set + + /// The remaining part names that can appear at most once. + var remainingAtMostOncePartNames: Set + } + + /// The current state of the state machine. + private(set) var state: State + + /// Creates a new state machine. + /// - Parameters: + /// - allowsUnknownParts: A Boolean value indicating whether unknown part names are allowed. + /// - requiredExactlyOncePartNames: A set of known part names that must appear exactly once. + /// - requiredAtLeastOncePartNames: A set of known part names that must appear at least once. + /// - atMostOncePartNames: A set of known part names that can appear at most once. + /// - zeroOrMoreTimesPartNames: A set of known part names that can appear any number of times. + init( + allowsUnknownParts: Bool, + requiredExactlyOncePartNames: Set, + requiredAtLeastOncePartNames: Set, + atMostOncePartNames: Set, + zeroOrMoreTimesPartNames: Set + ) { + self.state = .init( + allowsUnknownParts: allowsUnknownParts, + exactlyOncePartNames: requiredExactlyOncePartNames, + atLeastOncePartNames: requiredAtLeastOncePartNames, + atMostOncePartNames: atMostOncePartNames, + zeroOrMoreTimesPartNames: zeroOrMoreTimesPartNames, + remainingExactlyOncePartNames: requiredExactlyOncePartNames, + remainingAtLeastOncePartNames: requiredAtLeastOncePartNames, + remainingAtMostOncePartNames: atMostOncePartNames + ) + } + + /// An error returned by the state machine. + enum ActionError: Hashable { + + /// The sequence finished without encountering at least one required part. + case missingRequiredParts(expectedExactlyOnce: Set, expectedAtLeastOnce: Set) + + /// The validator encountered a part without a name, but `allowsUnknownParts` is set to `false`. + case receivedUnnamedPart + + /// The validator encountered a part with an unknown name, but `allowsUnknownParts` is set to `false`. + case receivedUnknownPart(String) + + /// The validator encountered a repeated part of the provided name, even though the part + /// is only allowed to appear at most once. + case receivedMultipleValuesForSingleValuePart(String) + } + + /// An action returned by the `next` method. + enum NextAction: Hashable { + + /// Return nil to the caller, no more parts. + case returnNil + + /// Fetch the next part. + case emitError(ActionError) + + /// Return the part to the caller. + case emitPart(MultipartRawPart) + } + + /// Read the next part from the upstream and validate it. + /// - Returns: An action to perform. + mutating func next(_ part: MultipartRawPart?) -> NextAction { + guard let part else { + guard state.remainingExactlyOncePartNames.isEmpty && state.remainingAtLeastOncePartNames.isEmpty else { + return .emitError( + .missingRequiredParts( + expectedExactlyOnce: state.remainingExactlyOncePartNames, + expectedAtLeastOnce: state.remainingAtLeastOncePartNames + ) + ) + } + return .returnNil + } + guard let name = part.name else { + guard state.allowsUnknownParts else { return .emitError(.receivedUnnamedPart) } + return .emitPart(part) + } + if state.remainingExactlyOncePartNames.contains(name) { + state.remainingExactlyOncePartNames.remove(name) + return .emitPart(part) + } + if state.remainingAtLeastOncePartNames.contains(name) { + state.remainingAtLeastOncePartNames.remove(name) + return .emitPart(part) + } + if state.remainingAtMostOncePartNames.contains(name) { + state.remainingAtMostOncePartNames.remove(name) + return .emitPart(part) + } + if state.exactlyOncePartNames.contains(name) || state.atMostOncePartNames.contains(name) { + return .emitError(.receivedMultipleValuesForSingleValuePart(name)) + } + if state.atLeastOncePartNames.contains(name) { return .emitPart(part) } + if state.zeroOrMoreTimesPartNames.contains(name) { return .emitPart(part) } + guard state.allowsUnknownParts else { return .emitError(.receivedUnknownPart(name)) } + return .emitPart(part) + } + } +} + +extension MultipartValidationSequence { + + /// A validator of multipart raw parts. + struct Validator { + + /// The underlying state machine. + private var stateMachine: StateMachine + /// Creates a new validator. + /// - Parameter requirements: The requirements to validate. + init(requirements: MultipartBodyRequirements) { + self.stateMachine = .init( + allowsUnknownParts: requirements.allowsUnknownParts, + requiredExactlyOncePartNames: requirements.requiredExactlyOncePartNames, + requiredAtLeastOncePartNames: requirements.requiredAtLeastOncePartNames, + atMostOncePartNames: requirements.atMostOncePartNames, + zeroOrMoreTimesPartNames: requirements.zeroOrMoreTimesPartNames + ) + } + + /// Ingests the next part. + /// - Parameter part: A part provided by the upstream sequence. Nil if the sequence is finished. + /// - Returns: The validated part. Nil if the incoming part was nil. + /// - Throws: When a validation error is encountered. + mutating func next(_ part: MultipartRawPart?) async throws -> MultipartRawPart? { + switch stateMachine.next(part) { + case .returnNil: return nil + case .emitPart(let outPart): return outPart + case .emitError(let error): throw ValidatorError(error: error) + } + } + } +} + +extension MultipartValidationSequence { + + /// An error thrown by the validator. + struct ValidatorError: Swift.Error, LocalizedError, CustomStringConvertible { + + /// The underlying error emitted by the state machine. + var error: StateMachine.ActionError + + var description: String { + switch error { + case .missingRequiredParts(let expectedExactlyOnce, let expectedAtLeastOnce): + let allSorted = expectedExactlyOnce.union(expectedAtLeastOnce).sorted() + return "Missing required parts: \(allSorted.joined(separator: ", "))." + case .receivedUnnamedPart: + return + "Received an unnamed part, which is disallowed in the OpenAPI document using \"additionalProperties: false\"." + case .receivedUnknownPart(let name): + return + "Received an unknown part '\(name)', which is disallowed in the OpenAPI document using \"additionalProperties: false\"." + case .receivedMultipleValuesForSingleValuePart(let name): + return + "Received more than one value of the part '\(name)', but according to the OpenAPI document this part can only appear at most once." + } + } + + var errorDescription: String? { description } + } +} diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_ContentDisposition.swift b/Tests/OpenAPIRuntimeTests/Base/Test_ContentDisposition.swift new file mode 100644 index 00000000..b820929d --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Base/Test_ContentDisposition.swift @@ -0,0 +1,85 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +final class Test_ContentDisposition: Test_Runtime { + + func testParsing() { + func _test( + input: String, + parsed: ContentDisposition?, + output: String?, + file: StaticString = #file, + line: UInt = #line + ) { + let value = ContentDisposition(rawValue: input) + XCTAssertEqual(value, parsed, file: file, line: line) + XCTAssertEqual(value?.rawValue, output, file: file, line: line) + } + + // Common + _test(input: "form-data", parsed: ContentDisposition(dispositionType: .formData), output: "form-data") + // With an unquoted name parameter. + _test( + input: "form-data; name=Foo", + parsed: ContentDisposition(dispositionType: .formData, parameters: [.name: "Foo"]), + output: "form-data; name=\"Foo\"" + ) + + // With a quoted name parameter. + _test( + input: "form-data; name=\"Foo\"", + parsed: ContentDisposition(dispositionType: .formData, parameters: [.name: "Foo"]), + output: "form-data; name=\"Foo\"" + ) + + // With quoted name and filename parameters. + _test( + input: "form-data; name=\"Foo\"; filename=\"foo.txt\"", + parsed: ContentDisposition(dispositionType: .formData, parameters: [.name: "Foo", .filename: "foo.txt"]), + output: "form-data; filename=\"foo.txt\"; name=\"Foo\"" + ) + + // With an unknown parameter. + _test( + input: "form-data; bar=\"Foo\"", + parsed: ContentDisposition(dispositionType: .formData, parameters: [.other("bar"): "Foo"]), + output: "form-data; bar=\"Foo\"" + ) + + // Other + _test( + input: "attachment", + parsed: ContentDisposition(dispositionType: .other("attachment")), + output: "attachment" + ) + + // Empty + _test(input: "", parsed: nil, output: nil) + } + func testAccessors() { + var value = ContentDisposition(dispositionType: .formData, parameters: [.name: "Foo"]) + XCTAssertEqual(value.name, "Foo") + XCTAssertNil(value.filename) + value.name = nil + XCTAssertNil(value.name) + XCTAssertNil(value.filename) + value.name = "Foo2" + value.filename = "foo.txt" + XCTAssertEqual(value.name, "Foo2") + XCTAssertEqual(value.filename, "foo.txt") + XCTAssertEqual(value.rawValue, "form-data; filename=\"foo.txt\"; name=\"Foo2\"") + } +} diff --git a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartValidationSequence.swift b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartValidationSequence.swift new file mode 100644 index 00000000..3951e864 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartValidationSequence.swift @@ -0,0 +1,283 @@ +//===----------------------------------------------------------------------===// +// +// 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_MultipartValidationSequence: Test_Runtime { + func test() async throws { + let firstBody: HTTPBody = "24" + let secondBody: HTTPBody = "{}" + let parts: [MultipartRawPart] = [ + .init(headerFields: [.contentDisposition: #"form-data; name="name""#], body: firstBody), + .init(headerFields: [.contentDisposition: #"form-data; name="info""#], body: secondBody), + ] + var upstreamIterator = parts.makeIterator() + let upstream = AsyncStream { upstreamIterator.next() } + let sequence = MultipartValidationSequence( + upstream: upstream, + requirements: .init( + allowsUnknownParts: true, + requiredExactlyOncePartNames: ["name"], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: ["info"], + zeroOrMoreTimesPartNames: [] + ) + ) + var outParts: [MultipartRawPart] = [] + for try await part in sequence { outParts.append(part) } + let expectedParts: [MultipartRawPart] = [ + .init(headerFields: [.contentDisposition: #"form-data; name="name""#], body: firstBody), + .init(headerFields: [.contentDisposition: #"form-data; name="info""#], body: secondBody), + ] + XCTAssertEqual(outParts, expectedParts) + } +} + +final class Test_MultipartValidationSequenceValidator: Test_Runtime { + func test() async throws { + let firstBody: HTTPBody = "24" + let secondBody: HTTPBody = "{}" + let parts: [MultipartRawPart] = [ + .init(headerFields: [.contentDisposition: #"form-data; name="name""#], body: firstBody), + .init(headerFields: [.contentDisposition: #"form-data; name="info""#], body: secondBody), + ] + var validator = MultipartValidationSequence> + .Validator( + requirements: .init( + allowsUnknownParts: true, + requiredExactlyOncePartNames: ["name"], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: ["info"], + zeroOrMoreTimesPartNames: [] + ) + ) + let outParts: [MultipartRawPart?] = try await [validator.next(parts[0]), validator.next(parts[1])] + let expectedParts: [MultipartRawPart] = [ + .init(headerFields: [.contentDisposition: #"form-data; name="name""#], body: firstBody), + .init(headerFields: [.contentDisposition: #"form-data; name="info""#], body: secondBody), + ] + XCTAssertEqual(outParts, expectedParts) + } +} + +private func newStateMachine( + allowsUnknownParts: Bool, + requiredExactlyOncePartNames: Set, + requiredAtLeastOncePartNames: Set, + atMostOncePartNames: Set, + zeroOrMoreTimesPartNames: Set +) -> MultipartValidationSequence>.StateMachine { + .init( + allowsUnknownParts: allowsUnknownParts, + requiredExactlyOncePartNames: requiredExactlyOncePartNames, + requiredAtLeastOncePartNames: requiredAtLeastOncePartNames, + atMostOncePartNames: atMostOncePartNames, + zeroOrMoreTimesPartNames: zeroOrMoreTimesPartNames + ) +} + +final class Test_MultipartValidationSequenceStateMachine: Test_Runtime { + + func testTwoParts() throws { + let parts: [MultipartRawPart] = [ + .init(headerFields: [.contentDisposition: #"form-data; name="name""#], body: "24"), + .init(headerFields: [.contentDisposition: #"form-data; name="info""#], body: "{}"), + ] + var stateMachine = newStateMachine( + allowsUnknownParts: true, + requiredExactlyOncePartNames: ["name"], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: ["info"], + zeroOrMoreTimesPartNames: [] + ) + XCTAssertEqual( + stateMachine.state, + .init( + allowsUnknownParts: true, + exactlyOncePartNames: ["name"], + atLeastOncePartNames: [], + atMostOncePartNames: ["info"], + zeroOrMoreTimesPartNames: [], + remainingExactlyOncePartNames: ["name"], + remainingAtLeastOncePartNames: [], + remainingAtMostOncePartNames: ["info"] + ) + ) + XCTAssertEqual(stateMachine.next(parts[0]), .emitPart(parts[0])) + XCTAssertEqual( + stateMachine.state, + .init( + allowsUnknownParts: true, + exactlyOncePartNames: ["name"], + atLeastOncePartNames: [], + atMostOncePartNames: ["info"], + zeroOrMoreTimesPartNames: [], + remainingExactlyOncePartNames: [], + remainingAtLeastOncePartNames: [], + remainingAtMostOncePartNames: ["info"] + ) + ) + XCTAssertEqual(stateMachine.next(parts[1]), .emitPart(parts[1])) + XCTAssertEqual( + stateMachine.state, + .init( + allowsUnknownParts: true, + exactlyOncePartNames: ["name"], + atLeastOncePartNames: [], + atMostOncePartNames: ["info"], + zeroOrMoreTimesPartNames: [], + remainingExactlyOncePartNames: [], + remainingAtLeastOncePartNames: [], + remainingAtMostOncePartNames: [] + ) + ) + XCTAssertEqual(stateMachine.next(nil), .returnNil) + } + func testUnknownWithName() throws { + let parts: [MultipartRawPart] = [ + .init(headerFields: [.contentDisposition: #"form-data; name="name""#], body: "24") + ] + var stateMachine = newStateMachine( + allowsUnknownParts: false, + requiredExactlyOncePartNames: [], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [] + ) + XCTAssertEqual(stateMachine.next(parts[0]), .emitError(.receivedUnknownPart("name"))) + } + + func testUnnamed_disallowed() throws { + let parts: [MultipartRawPart] = [.init(headerFields: [.contentDisposition: #"form-data"#], body: "24")] + var stateMachine = newStateMachine( + allowsUnknownParts: false, + requiredExactlyOncePartNames: [], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [] + ) + XCTAssertEqual(stateMachine.next(parts[0]), .emitError(.receivedUnnamedPart)) + } + func testUnnamed_allowed() throws { + let parts: [MultipartRawPart] = [.init(headerFields: [.contentDisposition: #"form-data"#], body: "24")] + var stateMachine = newStateMachine( + allowsUnknownParts: true, + requiredExactlyOncePartNames: [], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [] + ) + XCTAssertEqual(stateMachine.next(parts[0]), .emitPart(parts[0])) + } + func testUnknown_disallowed_zeroOrMore() throws { + let parts: [MultipartRawPart] = [ + .init(headerFields: [.contentDisposition: #"form-data; name="name""#], body: "24") + ] + var stateMachine = newStateMachine( + allowsUnknownParts: false, + requiredExactlyOncePartNames: [], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: ["name"] + ) + XCTAssertEqual(stateMachine.next(parts[0]), .emitPart(parts[0])) + XCTAssertEqual(stateMachine.next(parts[0]), .emitPart(parts[0])) + } + func testUnknown_allowed() throws { + let parts: [MultipartRawPart] = [ + .init(headerFields: [.contentDisposition: #"form-data; name="name""#], body: "24") + ] + var stateMachine = newStateMachine( + allowsUnknownParts: true, + requiredExactlyOncePartNames: [], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [] + ) + XCTAssertEqual(stateMachine.next(parts[0]), .emitPart(parts[0])) + } + + func testMissingRequiredExactlyOnce() throws { + var stateMachine = newStateMachine( + allowsUnknownParts: false, + requiredExactlyOncePartNames: ["name"], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [] + ) + XCTAssertEqual( + stateMachine.next(nil), + .emitError(.missingRequiredParts(expectedExactlyOnce: ["name"], expectedAtLeastOnce: [])) + ) + } + + func testMissingRequiredAtLeastOnce_once() throws { + var stateMachine = newStateMachine( + allowsUnknownParts: false, + requiredExactlyOncePartNames: [], + requiredAtLeastOncePartNames: ["info"], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [] + ) + XCTAssertEqual( + stateMachine.next(nil), + .emitError(.missingRequiredParts(expectedExactlyOnce: [], expectedAtLeastOnce: ["info"])) + ) + } + func testMissingRequiredAtLeastOnce_multipleTimes() throws { + let parts: [MultipartRawPart] = [ + .init(headerFields: [.contentDisposition: #"form-data; name="name""#], body: "24") + ] + var stateMachine = newStateMachine( + allowsUnknownParts: false, + requiredExactlyOncePartNames: [], + requiredAtLeastOncePartNames: ["name"], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [] + ) + XCTAssertEqual(stateMachine.next(parts[0]), .emitPart(parts[0])) + XCTAssertEqual(stateMachine.next(parts[0]), .emitPart(parts[0])) + } + + func testMissingRequiredExactlyOnce_multipleTimes() throws { + let parts: [MultipartRawPart] = [ + .init(headerFields: [.contentDisposition: #"form-data; name="name""#], body: "24") + ] + var stateMachine = newStateMachine( + allowsUnknownParts: false, + requiredExactlyOncePartNames: ["name"], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [] + ) + XCTAssertEqual(stateMachine.next(parts[0]), .emitPart(parts[0])) + XCTAssertEqual(stateMachine.next(parts[0]), .emitError(.receivedMultipleValuesForSingleValuePart("name"))) + } + + func testMissingRequiredAtMostOnce() throws { + let parts: [MultipartRawPart] = [ + .init(headerFields: [.contentDisposition: #"form-data; name="name""#], body: "24") + ] + var stateMachine = newStateMachine( + allowsUnknownParts: false, + requiredExactlyOncePartNames: [], + requiredAtLeastOncePartNames: [], + atMostOncePartNames: ["name"], + zeroOrMoreTimesPartNames: [] + ) + XCTAssertEqual(stateMachine.next(parts[0]), .emitPart(parts[0])) + XCTAssertEqual(stateMachine.next(parts[0]), .emitError(.receivedMultipleValuesForSingleValuePart("name"))) + } +} From 5060bb9f1fe02d7d23c35a592d6a00c04ed2e9d3 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 24 Nov 2023 11:47:18 +0100 Subject: [PATCH 09/20] [Multipart] Add public types (#77) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Multipart] Add public types ### Motivation Add the public types approved in SOAR-0009. ### Modifications Added the public types and make other changes outlined in SOAR-0009. ### Result Most of the required runtime types are now in place. ### Test Plan Added unit tests for the new code, where it made sense. ⚠️ The pipeline `pull request validation (api breakage)` is failing with: ``` 1 breaking change detected in OpenAPIRuntime: 💔 API breakage: constructor Configuration.init(dateTranscoder:) has removed default argument from parameter 0 ** ERROR: ❌ Breaking API changes detected. ``` but that seems to be a false positive in the tool, as there is now a newer initializer that you can use as `.init()`, `.init(dateTranscoder:)`, `.init(multipartBoundaryGenerator:)`, or `init(dateTranscoder:multipartBoundaryGenerator:)`, so no existing code could be broken by this change. Reviewed by: simonjbeaumont Builds: ✔︎ pull request validation (5.10) - Build finished. ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (docc test) - Build finished. ✔︎ pull request validation (integration test) - Build finished. ✔︎ pull request validation (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. ✖︎ pull request validation (api breakage) - Build finished. https://github.com/apple/swift-openapi-runtime/pull/77 --- .../OpenAPIRuntime/Base/CopyOnWriteBox.swift | 4 +- .../Conversion/Configuration.swift | 15 +- .../Deprecated/Deprecated.swift | 21 ++ .../Interface/AsyncSequenceCommon.swift | 120 +++++++ .../OpenAPIRuntime/Interface/HTTPBody.swift | 120 +------ .../MultipartBoundaryGenerator.swift | 75 ++++ .../Multipart/MultipartPublicTypes.swift | 329 ++++++++++++++++++ .../Test_MultipartBoundaryGenerator.swift | 36 ++ 8 files changed, 610 insertions(+), 110 deletions(-) create mode 100644 Sources/OpenAPIRuntime/Interface/AsyncSequenceCommon.swift create mode 100644 Sources/OpenAPIRuntime/Multipart/MultipartBoundaryGenerator.swift create mode 100644 Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBoundaryGenerator.swift 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__")) + } +} From bb2d2b3cb2f3062f7b0d0ce607aa89f6911755d2 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 24 Nov 2023 12:01:21 +0100 Subject: [PATCH 10/20] [Multipart] Add converter SPI methods (#78) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Multipart] Add converter SPI methods ### Motivation Last planned runtime PR for multipart, this adds the remaining SPI methods that allow the generated code to serialize/deserialize multipart bodies, both for client and server. ### Modifications Added SPI methods on `Converter` for multipart. ### Result Client and server generated code can now serialize/deserialize multipart bodies. ### Test Plan Added unit tests for the SPI methods. Reviewed by: simonjbeaumont Builds: ✔︎ pull request validation (5.10) - Build finished. ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (api breakage) - Build finished. ✔︎ pull request validation (docc test) - Build finished. ✔︎ pull request validation (integration test) - Build finished. ✔︎ pull request validation (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. https://github.com/apple/swift-openapi-runtime/pull/78 --- .../Conversion/Converter+Client.swift | 98 ++++++++++++++++++- .../Conversion/Converter+Common.swift | 23 +++++ .../Conversion/Converter+Server.swift | 97 +++++++++++++++++- .../Conversion/CurrencyExtensions.swift | 55 +++++++++-- .../OpenAPIRuntime/Errors/RuntimeError.swift | 10 ++ .../Multipart/OpenAPIMIMEType+Multipart.swift | 30 ++++++ .../Conversion/Test_Converter+Client.swift | 40 ++++++++ .../Conversion/Test_Converter+Common.swift | 31 ++++++ .../Conversion/Test_Converter+Server.swift | 41 ++++++++ Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 74 +++++++++++++- 10 files changed, 488 insertions(+), 11 deletions(-) create mode 100644 Sources/OpenAPIRuntime/Multipart/OpenAPIMIMEType+Multipart.swift diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index 4b723cac..ea575002 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -140,7 +140,7 @@ extension Converter { /// - Throws: An error if setting the request body as binary fails. public func setOptionalRequestBodyAsBinary(_ value: HTTPBody?, headerFields: inout HTTPFields, contentType: String) throws -> HTTPBody? - { try setOptionalRequestBody(value, headerFields: &headerFields, contentType: contentType, convert: { $0 }) } + { setOptionalRequestBody(value, headerFields: &headerFields, contentType: contentType, convert: { $0 }) } /// Sets a required request body as binary in the specified header fields and returns an `HTTPBody`. /// @@ -154,7 +154,7 @@ extension Converter { /// - Throws: An error if setting the request body as binary fails. public func setRequiredRequestBodyAsBinary(_ value: HTTPBody, headerFields: inout HTTPFields, contentType: String) throws -> HTTPBody - { try setRequiredRequestBody(value, headerFields: &headerFields, contentType: contentType, convert: { $0 }) } + { setRequiredRequestBody(value, headerFields: &headerFields, contentType: contentType, convert: { $0 }) } /// Sets an optional request body as URL-encoded form data in the specified header fields and returns an `HTTPBody`. /// @@ -202,6 +202,56 @@ extension Converter { ) } + /// Sets a required request body as multipart and returns the streaming body. + /// + /// - Parameters: + /// - value: The multipart body to be set as the request body. + /// - headerFields: The header fields in which to set the content type. + /// - contentType: The content type to be set in the header fields. + /// - allowsUnknownParts: A Boolean value indicating whether parts with unknown names + /// should be pass through. If `false`, encountering an unknown part throws an error + /// whent the returned body sequence iterates it. + /// - requiredExactlyOncePartNames: The list of part names that are required exactly once. + /// - requiredAtLeastOncePartNames: The list of part names that are required at least once. + /// - atMostOncePartNames: The list of part names that can appear at most once. + /// - zeroOrMoreTimesPartNames: The list of names that can appear any number of times. + /// - encode: A closure that transforms the type-safe part into a raw part. + /// - Returns: A streaming body representing the multipart-encoded request body. + /// - Throws: Currently never, but might in the future. + public func setRequiredRequestBodyAsMultipart( + _ value: MultipartBody, + headerFields: inout HTTPFields, + contentType: String, + allowsUnknownParts: Bool, + requiredExactlyOncePartNames: Set, + requiredAtLeastOncePartNames: Set, + atMostOncePartNames: Set, + zeroOrMoreTimesPartNames: Set, + encoding encode: @escaping @Sendable (Part) throws -> MultipartRawPart + ) throws -> HTTPBody { + let boundary = configuration.multipartBoundaryGenerator.makeBoundary() + let contentTypeWithBoundary = contentType + "; boundary=\(boundary)" + return setRequiredRequestBody( + value, + headerFields: &headerFields, + contentType: contentTypeWithBoundary, + convert: { value in + convertMultipartToBytes( + value, + requirements: .init( + allowsUnknownParts: allowsUnknownParts, + requiredExactlyOncePartNames: requiredExactlyOncePartNames, + requiredAtLeastOncePartNames: requiredAtLeastOncePartNames, + atMostOncePartNames: atMostOncePartNames, + zeroOrMoreTimesPartNames: zeroOrMoreTimesPartNames + ), + boundary: boundary, + encode: encode + ) + } + ) + } + /// Retrieves the response body as JSON and transforms it into a specified type. /// /// - Parameters: @@ -244,4 +294,48 @@ extension Converter { guard let data else { throw RuntimeError.missingRequiredResponseBody } return try getResponseBody(type, from: data, transforming: transform, convert: { $0 }) } + /// Returns an async sequence of multipart parts parsed from the provided body stream. + /// + /// - Parameters: + /// - type: The type representing the type-safe multipart body. + /// - data: The HTTP body data to transform. + /// - transform: A closure that transforms the multipart body into the output type. + /// - boundary: The multipart boundary string. + /// - allowsUnknownParts: A Boolean value indicating whether parts with unknown names + /// should be pass through. If `false`, encountering an unknown part throws an error + /// whent the returned body sequence iterates it. + /// - requiredExactlyOncePartNames: The list of part names that are required exactly once. + /// - requiredAtLeastOncePartNames: The list of part names that are required at least once. + /// - atMostOncePartNames: The list of part names that can appear at most once. + /// - zeroOrMoreTimesPartNames: The list of names that can appear any number of times. + /// - decoder: A closure that parses a raw part into a type-safe part. + /// - Returns: A value of the output type. + /// - Throws: If the transform closure throws. + public func getResponseBodyAsMultipart( + _ type: MultipartBody.Type, + from data: HTTPBody?, + transforming transform: @escaping @Sendable (MultipartBody) throws -> C, + boundary: String, + allowsUnknownParts: Bool, + requiredExactlyOncePartNames: Set, + requiredAtLeastOncePartNames: Set, + atMostOncePartNames: Set, + zeroOrMoreTimesPartNames: Set, + decoding decoder: @escaping @Sendable (MultipartRawPart) async throws -> Part + ) throws -> C { + guard let data else { throw RuntimeError.missingRequiredResponseBody } + let multipart = convertBytesToMultipart( + data, + boundary: boundary, + requirements: .init( + allowsUnknownParts: allowsUnknownParts, + requiredExactlyOncePartNames: requiredExactlyOncePartNames, + requiredAtLeastOncePartNames: requiredAtLeastOncePartNames, + atMostOncePartNames: atMostOncePartNames, + zeroOrMoreTimesPartNames: zeroOrMoreTimesPartNames + ), + transform: decoder + ) + return try transform(multipart) + } } diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift index a7f8e979..dc908e75 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift @@ -65,6 +65,29 @@ extension Converter { return bestContentType } + /// Verifies the MIME type from the content-type header, if present. + /// - Parameters: + /// - headerFields: The header fields to inspect for the content type header. + /// - match: The content type to verify. + /// - Throws: If the content type is incompatible or malformed. + public func verifyContentTypeIfPresent(in headerFields: HTTPFields, matches match: String) throws { + guard let rawValue = headerFields[.contentType] else { return } + _ = try bestContentType(received: .init(rawValue), options: [match]) + } + + /// Returns the name and file name parameter values from the `content-disposition` header field, if found. + /// - Parameter headerFields: The header fields to inspect for a `content-disposition` header field. + /// - Returns: A tuple of the name and file name string values. + /// - Throws: Currently doesn't, but might in the future. + public func extractContentDispositionNameAndFilename(in headerFields: HTTPFields) throws -> ( + name: String?, filename: String? + ) { + guard let rawValue = headerFields[.contentDisposition], + let contentDisposition = ContentDisposition(rawValue: rawValue) + else { return (nil, nil) } + return (contentDisposition.name, contentDisposition.filename) + } + // MARK: - Converter helper methods /// Sets a header field with an optional value, encoding it as a URI component if not nil. diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index c354d4aa..e8f36306 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -284,6 +284,51 @@ extension Converter { ) } + /// Returns an async sequence of multipart parts parsed from the provided body stream. + /// + /// - Parameters: + /// - type: The type representing the type-safe multipart body. + /// - data: The HTTP body data to transform. + /// - transform: A closure that transforms the multipart body into the output type. + /// - boundary: The multipart boundary string. + /// - allowsUnknownParts: A Boolean value indicating whether parts with unknown names + /// should be pass through. If `false`, encountering an unknown part throws an error + /// whent the returned body sequence iterates it. + /// - requiredExactlyOncePartNames: The list of part names that are required exactly once. + /// - requiredAtLeastOncePartNames: The list of part names that are required at least once. + /// - atMostOncePartNames: The list of part names that can appear at most once. + /// - zeroOrMoreTimesPartNames: The list of names that can appear any number of times. + /// - decoder: A closure that parses a raw part into a type-safe part. + /// - Returns: A value of the output type. + /// - Throws: If the transform closure throws. + public func getRequiredRequestBodyAsMultipart( + _ type: MultipartBody.Type, + from data: HTTPBody?, + transforming transform: @escaping @Sendable (MultipartBody) throws -> C, + boundary: String, + allowsUnknownParts: Bool, + requiredExactlyOncePartNames: Set, + requiredAtLeastOncePartNames: Set, + atMostOncePartNames: Set, + zeroOrMoreTimesPartNames: Set, + decoding decoder: @escaping @Sendable (MultipartRawPart) async throws -> Part + ) throws -> C { + guard let data else { throw RuntimeError.missingRequiredRequestBody } + let multipart = convertBytesToMultipart( + data, + boundary: boundary, + requirements: .init( + allowsUnknownParts: allowsUnknownParts, + requiredExactlyOncePartNames: requiredExactlyOncePartNames, + requiredAtLeastOncePartNames: requiredAtLeastOncePartNames, + atMostOncePartNames: atMostOncePartNames, + zeroOrMoreTimesPartNames: zeroOrMoreTimesPartNames + ), + transform: decoder + ) + return try transform(multipart) + } + /// Sets the response body as JSON data, serializing the provided value. /// /// - Parameters: @@ -313,5 +358,55 @@ extension Converter { /// - Throws: An error if there are issues setting the response body or updating the header fields. public func setResponseBodyAsBinary(_ value: HTTPBody, headerFields: inout HTTPFields, contentType: String) throws -> HTTPBody - { try setResponseBody(value, headerFields: &headerFields, contentType: contentType, convert: { $0 }) } + { setResponseBody(value, headerFields: &headerFields, contentType: contentType, convert: { $0 }) } + + /// Sets a response body as multipart and returns the streaming body. + /// + /// - Parameters: + /// - value: The multipart body to be set as the response body. + /// - headerFields: The header fields in which to set the content type. + /// - contentType: The content type to be set in the header fields. + /// - allowsUnknownParts: A Boolean value indicating whether parts with unknown names + /// should be pass through. If `false`, encountering an unknown part throws an error + /// whent the returned body sequence iterates it. + /// - requiredExactlyOncePartNames: The list of part names that are required exactly once. + /// - requiredAtLeastOncePartNames: The list of part names that are required at least once. + /// - atMostOncePartNames: The list of part names that can appear at most once. + /// - zeroOrMoreTimesPartNames: The list of names that can appear any number of times. + /// - encode: A closure that transforms the type-safe part into a raw part. + /// - Returns: A streaming body representing the multipart-encoded response body. + /// - Throws: Currently never, but might in the future. + public func setResponseBodyAsMultipart( + _ value: MultipartBody, + headerFields: inout HTTPFields, + contentType: String, + allowsUnknownParts: Bool, + requiredExactlyOncePartNames: Set, + requiredAtLeastOncePartNames: Set, + atMostOncePartNames: Set, + zeroOrMoreTimesPartNames: Set, + encoding encode: @escaping @Sendable (Part) throws -> MultipartRawPart + ) throws -> HTTPBody { + let boundary = configuration.multipartBoundaryGenerator.makeBoundary() + let contentTypeWithBoundary = contentType + "; boundary=\(boundary)" + return setResponseBody( + value, + headerFields: &headerFields, + contentType: contentTypeWithBoundary, + convert: { value in + convertMultipartToBytes( + value, + requirements: .init( + allowsUnknownParts: allowsUnknownParts, + requiredExactlyOncePartNames: requiredExactlyOncePartNames, + requiredAtLeastOncePartNames: requiredAtLeastOncePartNames, + atMostOncePartNames: atMostOncePartNames, + zeroOrMoreTimesPartNames: zeroOrMoreTimesPartNames + ), + boundary: boundary, + encode: encode + ) + } + ) + } } diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index 55765921..df6caf04 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -179,6 +179,52 @@ extension Converter { return HTTPBody(encodedString) } + /// Returns a serialized multipart body stream. + /// - Parameters: + /// - multipart: The multipart body. + /// - requirements: The multipart requirements to enforce. When violated, an error is thrown in the sequence. + /// - boundary: The multipart boundary string. + /// - encode: A closure that converts a typed part into a raw part. + /// - Returns: The serialized body stream. + func convertMultipartToBytes( + _ multipart: MultipartBody, + requirements: MultipartBodyRequirements, + boundary: String, + encode: @escaping @Sendable (Part) throws -> MultipartRawPart + ) -> HTTPBody { + let untyped = multipart.map { part in + var untypedPart = try encode(part) + if case .known(let byteCount) = untypedPart.body.length { + untypedPart.headerFields[.contentLength] = String(byteCount) + } + return untypedPart + } + let validated = MultipartValidationSequence(upstream: untyped, requirements: requirements) + let frames = MultipartRawPartsToFramesSequence(upstream: validated) + let bytes = MultipartFramesToBytesSequence(upstream: frames, boundary: boundary) + return HTTPBody(bytes, length: .unknown, iterationBehavior: multipart.iterationBehavior) + } + + /// Returns a parsed multipart body. + /// - Parameters: + /// - bytes: The multipart body byte stream. + /// - boundary: The multipart boundary string. + /// - requirements: The multipart requirements to enforce. When violated, an error is thrown in the sequence. + /// - transform: A closure that converts a raw part into a typed part. + /// - Returns: The typed multipart body stream. + func convertBytesToMultipart( + _ bytes: HTTPBody, + boundary: String, + requirements: MultipartBodyRequirements, + transform: @escaping @Sendable (MultipartRawPart) async throws -> Part + ) -> MultipartBody { + let frames = MultipartBytesToFramesSequence(upstream: bytes, boundary: boundary) + let raw = MultipartFramesToRawPartsSequence(upstream: frames) + let validated = MultipartValidationSequence(upstream: raw, requirements: requirements) + let typed = validated.map(transform) + return .init(typed, iterationBehavior: bytes.iterationBehavior) + } + /// Returns a JSON string for the provided encodable value. /// - Parameter value: The value to encode. /// - Returns: A JSON string. @@ -383,13 +429,12 @@ extension Converter { /// - contentType: The content type value. /// - convert: The closure that encodes the value into a raw body. /// - Returns: The body. - /// - Throws: An error if an issue occurs while encoding the request body or setting the content type. func setRequiredRequestBody( _ value: T, headerFields: inout HTTPFields, contentType: String, convert: (T) throws -> HTTPBody - ) throws -> HTTPBody { + ) rethrows -> HTTPBody { headerFields[.contentType] = contentType return try convert(value) } @@ -402,13 +447,12 @@ extension Converter { /// - contentType: The content type value. /// - convert: The closure that encodes the value into a raw body. /// - Returns: The body, if value was not nil. - /// - Throws: An error if an issue occurs while encoding the request body or setting the content type. func setOptionalRequestBody( _ value: T?, headerFields: inout HTTPFields, contentType: String, convert: (T) throws -> HTTPBody - ) throws -> HTTPBody? { + ) rethrows -> HTTPBody? { guard let value else { return nil } return try setRequiredRequestBody( value, @@ -547,13 +591,12 @@ extension Converter { /// - contentType: The content type value. /// - convert: The closure that encodes the value into a raw body. /// - Returns: The body, if value was not nil. - /// - Throws: An error if an issue occurs while encoding the request body. func setResponseBody( _ value: T, headerFields: inout HTTPFields, contentType: String, convert: (T) throws -> HTTPBody - ) throws -> HTTPBody { + ) rethrows -> HTTPBody { headerFields[.contentType] = contentType return try convert(value) } diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index ffb39ab7..150b804c 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -39,6 +39,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret case unexpectedContentTypeHeader(String) case unexpectedAcceptHeader(String) case malformedAcceptHeader(String) + case missingOrMalformedContentDispositionName // Path case missingRequiredPathParameter(String) @@ -51,6 +52,10 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret case missingRequiredRequestBody case missingRequiredResponseBody + // Multipart + case missingRequiredMultipartFormDataContentType + case missingMultipartBoundaryContentTypeParameter + // Transport/Handler case transportFailed(any Error) case middlewareFailed(middlewareType: Any.Type, any Error) @@ -90,11 +95,16 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret case .unexpectedContentTypeHeader(let contentType): return "Unexpected Content-Type header: \(contentType)" case .unexpectedAcceptHeader(let accept): return "Unexpected Accept header: \(accept)" case .malformedAcceptHeader(let accept): return "Malformed Accept header: \(accept)" + case .missingOrMalformedContentDispositionName: + return "Missing or malformed Content-Disposition header or it's missing a name." 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: return "Missing required request body" case .missingRequiredResponseBody: return "Missing required response body" + case .missingRequiredMultipartFormDataContentType: return "Expected a 'multipart/form-data' content type." + case .missingMultipartBoundaryContentTypeParameter: + return "Missing 'boundary' parameter in the 'multipart/form-data' content type." case .transportFailed: return "Transport threw an error." case .middlewareFailed(middlewareType: let type, _): return "Middleware of type '\(type)' threw an error." case .handlerFailed: return "User handler threw an error." diff --git a/Sources/OpenAPIRuntime/Multipart/OpenAPIMIMEType+Multipart.swift b/Sources/OpenAPIRuntime/Multipart/OpenAPIMIMEType+Multipart.swift new file mode 100644 index 00000000..4d8b2f25 --- /dev/null +++ b/Sources/OpenAPIRuntime/Multipart/OpenAPIMIMEType+Multipart.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// +@_spi(Generated) extension Optional where Wrapped == OpenAPIMIMEType { + + /// Unwraps the boundary parameter from the parsed MIME type. + /// - Returns: The boundary value. + /// - Throws: If self is nil, or if the MIME type isn't a `multipart/form-data` + /// with a boundary parameter. + public func requiredBoundary() throws -> String { + guard let self else { throw RuntimeError.missingRequiredMultipartFormDataContentType } + guard case .concrete(type: "multipart", subtype: "form-data") = self.kind else { + throw RuntimeError.missingRequiredMultipartFormDataContentType + } + guard let boundary = self.parameters["boundary"] else { + throw RuntimeError.missingMultipartBoundaryContentTypeParameter + } + return boundary + } +} diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift index 135bdf46..57c11580 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -175,6 +175,28 @@ final class Test_ClientConverterExtensions: Test_Runtime { XCTAssertEqual(headerFields, [.contentType: "application/octet-stream"]) } + // | client | set | request body | multipart | required | setRequiredRequestBodyAsMultipart | + func test_setRequiredRequestBodyAsMultipart() async throws { + let multipartBody: MultipartBody = .init(MultipartTestPart.all) + var headerFields: HTTPFields = [:] + let body = try converter.setRequiredRequestBodyAsMultipart( + multipartBody, + headerFields: &headerFields, + contentType: "multipart/form-data", + allowsUnknownParts: true, + requiredExactlyOncePartNames: ["hello"], + requiredAtLeastOncePartNames: ["world"], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [], + encoding: { part in part.rawPart } + ) + try await XCTAssertEqualData(body, testMultipartStringBytes) + XCTAssertEqual( + headerFields, + [.contentType: "multipart/form-data; boundary=__X_SWIFT_OPENAPI_GENERATOR_BOUNDARY__"] + ) + } + // | client | get | response body | JSON | required | getResponseBodyAsJSON | func test_getResponseBodyAsJSON_codable() async throws { let value: TestPet = try await converter.getResponseBodyAsJSON( @@ -194,6 +216,24 @@ final class Test_ClientConverterExtensions: Test_Runtime { ) try await XCTAssertEqualStringifiedData(value, testString) } + // | client | get | response body | multipart | required | getResponseBodyAsMultipart | + func test_getResponseBodyAsMultipart() async throws { + let value = try converter.getResponseBodyAsMultipart( + MultipartBody.self, + from: .init(testMultipartStringBytes), + transforming: { $0 }, + boundary: "__X_SWIFT_OPENAPI_GENERATOR_BOUNDARY__", + allowsUnknownParts: true, + requiredExactlyOncePartNames: ["hello"], + requiredAtLeastOncePartNames: ["world"], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [], + decoding: { part in try await .init(part) } + ) + var parts: [MultipartTestPart] = [] + for try await part in value { parts.append(part) } + XCTAssertEqual(parts, MultipartTestPart.all) + } } /// Asserts that the string representation of binary data is equal to an expected string. diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift index da68208f..bca29837 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift @@ -107,6 +107,37 @@ final class Test_CommonConverterExtensions: Test_Runtime { ) } + func testVerifyContentTypeIfPresent() throws { + func testCase(received: String?, match: String, file: StaticString = #file, line: UInt = #line) throws { + let headerFields: HTTPFields + if let received { headerFields = [.contentType: received] } else { headerFields = [:] } + try converter.verifyContentTypeIfPresent(in: headerFields, matches: match) + } + try testCase(received: nil, match: "application/json") + try testCase(received: "application/json", match: "application/json") + try testCase(received: "application/json", match: "application/*") + try testCase(received: "application/json", match: "*/*") + } + + func testExtractContentDispositionNameAndFilename() throws { + func testCase(value: String?, name: String?, filename: String?, file: StaticString = #file, line: UInt = #line) + throws + { + let headerFields: HTTPFields + if let value { headerFields = [.contentDisposition: value] } else { headerFields = [:] } + let (actualName, actualFilename) = try converter.extractContentDispositionNameAndFilename(in: headerFields) + XCTAssertEqual(actualName, name, file: file, line: line) + XCTAssertEqual(actualFilename, filename, file: file, line: line) + } + try testCase(value: nil, name: nil, filename: nil) + try testCase(value: "form-data", name: nil, filename: nil) + try testCase(value: "form-data; filename=\"foo.txt\"", name: nil, filename: "foo.txt") + try testCase(value: "form-data; name=\"Foo and Bar\"", name: "Foo and Bar", filename: nil) + try testCase(value: "form-data; filename=foo.txt", name: nil, filename: "foo.txt") + try testCase(value: "form-data; name=Foo", name: "Foo", filename: nil) + try testCase(value: "form-data; filename=\"foo.txt\"; name=\"Foo\"", name: "Foo", filename: "foo.txt") + } + // MARK: Converter helper methods // | common | set | header field | URI | both | setHeaderFieldAsURI | diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift index 91525af4..d70a58d7 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift @@ -288,6 +288,25 @@ final class Test_ServerConverterExtensions: Test_Runtime { try await XCTAssertEqualStringifiedData(body, testString) } + // | server | get | request body | multipart | required | getRequiredRequestBodyAsMultipart | + func test_getRequiredRequestBodyAsMultipart() async throws { + let value = try converter.getRequiredRequestBodyAsMultipart( + MultipartBody.self, + from: .init(testMultipartStringBytes), + transforming: { $0 }, + boundary: "__X_SWIFT_OPENAPI_GENERATOR_BOUNDARY__", + allowsUnknownParts: true, + requiredExactlyOncePartNames: ["hello"], + requiredAtLeastOncePartNames: ["world"], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [], + decoding: { part in try await .init(part) } + ) + var parts: [MultipartTestPart] = [] + for try await part in value { parts.append(part) } + XCTAssertEqual(parts, MultipartTestPart.all) + } + // | server | set | response body | JSON | required | setResponseBodyAsJSON | func test_setResponseBodyAsJSON_codable() async throws { var headers: HTTPFields = [:] @@ -311,4 +330,26 @@ final class Test_ServerConverterExtensions: Test_Runtime { try await XCTAssertEqualStringifiedData(data, testString) XCTAssertEqual(headers, [.contentType: "application/octet-stream"]) } + + // | server | set | response body | multipart | required | setResponseBodyAsMultipart | + func test_setResponseBodyAsMultipart() async throws { + let multipartBody: MultipartBody = .init(MultipartTestPart.all) + var headerFields: HTTPFields = [:] + let body = try converter.setResponseBodyAsMultipart( + multipartBody, + headerFields: &headerFields, + contentType: "multipart/form-data", + allowsUnknownParts: true, + requiredExactlyOncePartNames: ["hello"], + requiredAtLeastOncePartNames: ["world"], + atMostOncePartNames: [], + zeroOrMoreTimesPartNames: [], + encoding: { part in part.rawPart } + ) + try await XCTAssertEqualData(body, testMultipartStringBytes) + XCTAssertEqual( + headerFields, + [.contentType: "multipart/form-data; boundary=__X_SWIFT_OPENAPI_GENERATOR_BOUNDARY__"] + ) + } } diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index 2e6d386e..0d7d108e 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// import XCTest -@_spi(Generated) import OpenAPIRuntime +@_spi(Generated) @testable import OpenAPIRuntime import HTTPTypes class Test_Runtime: XCTestCase { @@ -26,7 +26,7 @@ class Test_Runtime: XCTestCase { var serverURL: URL { get throws { try URL(validatingOpenAPIServerURL: "/api") } } - var configuration: Configuration { .init() } + var configuration: Configuration { .init(multipartBoundaryGenerator: .constant) } var converter: Converter { .init(configuration: configuration) } @@ -52,6 +52,34 @@ class Test_Runtime: XCTestCase { var testStringData: Data { Data(testString.utf8) } + var testMultipartString: String { "hello" } + + var testMultipartStringBytes: ArraySlice { + var bytes: [UInt8] = [] + bytes.append(contentsOf: "--__X_SWIFT_OPENAPI_GENERATOR_BOUNDARY__".utf8) + bytes.append(contentsOf: ASCII.crlf) + bytes.append(contentsOf: #"content-disposition: form-data; filename="foo.txt"; name="hello""#.utf8) + bytes.append(contentsOf: ASCII.crlf) + bytes.append(contentsOf: #"content-length: 5"#.utf8) + bytes.append(contentsOf: ASCII.crlf) + bytes.append(contentsOf: ASCII.crlf) + bytes.append(contentsOf: "hello".utf8) + bytes.append(contentsOf: ASCII.crlf) + bytes.append(contentsOf: "--__X_SWIFT_OPENAPI_GENERATOR_BOUNDARY__".utf8) + bytes.append(contentsOf: ASCII.crlf) + bytes.append(contentsOf: #"content-disposition: form-data; filename="bar.txt"; name="world""#.utf8) + bytes.append(contentsOf: ASCII.crlf) + bytes.append(contentsOf: #"content-length: 5"#.utf8) + bytes.append(contentsOf: ASCII.crlf) + bytes.append(contentsOf: ASCII.crlf) + bytes.append(contentsOf: "world".utf8) + bytes.append(contentsOf: ASCII.crlf) + bytes.append(contentsOf: "--__X_SWIFT_OPENAPI_GENERATOR_BOUNDARY__--".utf8) + bytes.append(contentsOf: ASCII.crlf) + bytes.append(contentsOf: ASCII.crlf) + return ArraySlice(bytes) + } + var testQuotedString: String { "\"hello\"" } var testQuotedStringData: Data { Data(testQuotedString.utf8) } @@ -196,6 +224,31 @@ enum TestHabitat: String, Codable, Equatable { case air } +enum MultipartTestPart: Hashable { + case hello(payload: String, filename: String?) + case world(payload: String, filename: String?) + var rawPart: MultipartRawPart { + switch self { + case .hello(let payload, let filename): + return .init(name: "hello", filename: filename, headerFields: [:], body: .init(payload)) + case .world(let payload, let filename): + return .init(name: "world", filename: filename, headerFields: [:], body: .init(payload)) + } + } + init(_ rawPart: MultipartRawPart) async throws { + switch rawPart.name { + case "hello": + self = .hello(payload: try await String(collecting: rawPart.body, upTo: .max), filename: rawPart.filename) + case "world": + self = .world(payload: try await String(collecting: rawPart.body, upTo: .max), filename: rawPart.filename) + default: preconditionFailure("Unexpected part: \(rawPart.name ?? "")") + } + } + static var all: [MultipartTestPart] { + [.hello(payload: "hello", filename: "foo.txt"), .world(payload: "world", filename: "bar.txt")] + } +} + /// Injects an authentication header to every request. struct AuthenticationMiddleware: ClientMiddleware { @@ -292,6 +345,7 @@ fileprivate extension UInt8 { return String(format: "%02x \(original)", self) } } + /// Asserts that the data matches the expected value. public func XCTAssertEqualData( _ expression1: @autoclosure () throws -> C1?, @@ -339,3 +393,19 @@ public func XCTAssertEqualData( ) } catch { XCTFail(error.localizedDescription, file: file, line: line) } } + +/// Asserts that the data matches the expected value. +public func XCTAssertEqualData( + _ expression1: @autoclosure () throws -> HTTPBody?, + _ expression2: @autoclosure () throws -> C, + _ message: @autoclosure () -> String = "Data doesn't match.", + file: StaticString = #filePath, + line: UInt = #line +) async throws where C.Element == UInt8 { + guard let actualBytesBody = try expression1() else { + XCTFail("First value is nil", file: file, line: line) + return + } + let actualBytes = try await [UInt8](collecting: actualBytesBody, upTo: .max) + XCTAssertEqualData(actualBytes, try expression2(), file: file, line: line) +} From 391703120df51fb22869ad29b91b4e9913d8458e Mon Sep 17 00:00:00 2001 From: Si Beaumont Date: Mon, 27 Nov 2023 12:35:54 +0000 Subject: [PATCH 11/20] Change type of HTTPBody.Length.known from Int to Int64 (#79) ### Motivation The associated value of enum case `HTTPBody.Length.known` was `Int`, and, on watchOS, `Int = Int32`, which is not ideal for a content length. ### Modifications - (API breaking) Change type of HTTPBody.Length.known from Int to Int64. ### Result Can now express larger values for content type on 32-bit platforms. ### Test Plan Unit tests pass. ### Related Issues - Fixes https://github.com/apple/swift-openapi-generator/issues/354. ### Note I have marked this PR as `semver/major` as it constitutes the first of the PRs we make in the run up to 1.0.0-alpha.1 and the release notes generator should therefore catch us from inadvertently cutting another pre-1.0 patch release. --- Sources/OpenAPIRuntime/Interface/HTTPBody.swift | 10 +++++----- .../Multipart/MultipartFramesToRawPartsSequence.swift | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift index eb163459..648c504a 100644 --- a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -125,17 +125,17 @@ public final class HTTPBody: @unchecked Sendable { /// the input sequence can be iterated. public let iterationBehavior: OpenAPIRuntime.IterationBehavior - /// Describes the total length of the body, if known. + /// Describes the total length of the body, in bytes, if known. public enum Length: Sendable, Equatable { /// Total length not known yet. case unknown /// Total length is known. - case known(Int) + case known(Int64) } - /// The total length of the body, if known. + /// The total length of the body, in bytes, if known. public let length: Length /// The underlying type-erased async sequence. @@ -257,7 +257,7 @@ extension HTTPBody { /// Creates a new body with the provided byte chunk. /// - Parameter bytes: A byte chunk. @inlinable public convenience init(_ bytes: ByteChunk) { - self.init([bytes], length: .known(bytes.count), iterationBehavior: .multiple) + self.init([bytes], length: .known(Int64(bytes.count)), iterationBehavior: .multiple) } /// Creates a new body with the provided byte sequence. @@ -283,7 +283,7 @@ extension HTTPBody { /// Creates a new body with the provided byte collection. /// - Parameter bytes: A byte chunk. @inlinable public convenience init(_ bytes: some Collection & Sendable) { - self.init(bytes, length: .known(bytes.count)) + self.init(bytes, length: .known(Int64(bytes.count))) } /// Creates a new body with the provided async throwing stream. diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartFramesToRawPartsSequence.swift b/Sources/OpenAPIRuntime/Multipart/MultipartFramesToRawPartsSequence.swift index 3345c088..c5823ec2 100644 --- a/Sources/OpenAPIRuntime/Multipart/MultipartFramesToRawPartsSequence.swift +++ b/Sources/OpenAPIRuntime/Multipart/MultipartFramesToRawPartsSequence.swift @@ -77,7 +77,7 @@ extension HTTPBody { let stream = AsyncThrowingStream(unfolding: bodyClosure) let length: HTTPBody.Length if let contentLengthString = headerFields[.contentLength], let contentLength = Int(contentLengthString) { - length = .known(contentLength) + length = .known(Int64(contentLength)) } else { length = .unknown } From e5816cb665f1a54a25e5c15195f31bad83c55603 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 27 Nov 2023 16:16:30 +0100 Subject: [PATCH 12/20] Move to Swift 5.9 as the minimum version (#80) ### Motivation Part of addressing https://github.com/apple/swift-openapi-generator/issues/75 and https://github.com/apple/swift-openapi-generator/issues/119. ### Modifications Bumped Swift tools version to 5.9 and made the `ExistentialAny` build setting unconditional. ### Result Building the package requires 5.9 now. ### Test Plan Ran tests, all passed when using a Swift 5.9 toolchain. --- Package.swift | 10 +++------- docker/Dockerfile | 2 +- docker/docker-compose.2204.58.yaml | 19 ------------------- docker/docker-compose.yaml | 2 +- 4 files changed, 5 insertions(+), 28 deletions(-) delete mode 100644 docker/docker-compose.2204.58.yaml diff --git a/Package.swift b/Package.swift index 960e3311..f8045977 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.8 +// swift-tools-version: 5.9 //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project @@ -15,15 +15,11 @@ import PackageDescription // General Swift-settings for all targets. -var swiftSettings: [SwiftSetting] = [] - -#if swift(>=5.9) -swiftSettings.append( +let swiftSettings: [SwiftSetting] = [ // https://github.com/apple/swift-evolution/blob/main/proposals/0335-existential-any.md // Require `any` for existential types. .enableUpcomingFeature("ExistentialAny") -) -#endif +] let package = Package( name: "swift-openapi-runtime", diff --git a/docker/Dockerfile b/docker/Dockerfile index 76bb652d..5040ad9c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -ARG swift_version=5.8 +ARG swift_version=5.9 ARG ubuntu_version=jammy ARG base_image=swift:${swift_version}-${ubuntu_version} diff --git a/docker/docker-compose.2204.58.yaml b/docker/docker-compose.2204.58.yaml deleted file mode 100644 index 071d15ba..00000000 --- a/docker/docker-compose.2204.58.yaml +++ /dev/null @@ -1,19 +0,0 @@ -version: "3" - -services: - runtime-setup: - image: &image swift-openapi-runtime:22.04-5.8 - build: - args: - ubuntu_version: "jammy" - swift_version: "5.8" - - test: - image: *image - environment: - - WARN_AS_ERROR_ARG=-Xswiftc -warnings-as-errors - - IMPORT_CHECK_ARG=--explicit-target-dependency-import-check error - - STRICT_CONCURRENCY_ARG=-Xswiftc -strict-concurrency=complete - - shell: - image: *image diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 000a5ad0..c63046bb 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -4,7 +4,7 @@ # # % docker-compose \ # -f docker/docker-compose.yaml \ -# -f docker/docker-compose.2204.58.yaml \ +# -f docker/docker-compose.2204.59.yaml \ # run test # version: "3" From 336b77ce633f753284fa6bf4ceec385ae78c1095 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 27 Nov 2023 17:21:00 +0100 Subject: [PATCH 13/20] Remove deprecated code (#81) ### Motivation Preparing for 1.0.0-alpha.1, remove deprecated code. ### Modifications Removed deprecated code. ### Result No more deprecated code. ### Test Plan All tests pass, no warnings. --- .../Base/Base64EncodedData.swift | 6 - .../Deprecated/Deprecated.swift | 201 ------------------ .../Conversion/Test_Converter+Common.swift | 20 -- 3 files changed, 227 deletions(-) diff --git a/Sources/OpenAPIRuntime/Base/Base64EncodedData.swift b/Sources/OpenAPIRuntime/Base/Base64EncodedData.swift index 408e9f90..7af50f72 100644 --- a/Sources/OpenAPIRuntime/Base/Base64EncodedData.swift +++ b/Sources/OpenAPIRuntime/Base/Base64EncodedData.swift @@ -54,12 +54,6 @@ public struct Base64EncodedData: Sendable, Hashable { /// A container of the raw bytes. public var data: ArraySlice - /// Initializes an instance of ``Base64EncodedData`` wrapping the provided slice of bytes. - /// - Parameter data: The underlying bytes to wrap. - @available(*, deprecated, renamed: "init(_:)") - - public init(data: ArraySlice) { self.data = data } - /// Initializes an instance of ``Base64EncodedData`` wrapping the provided slice of bytes. /// - Parameter data: The underlying bytes to wrap. public init(_ data: ArraySlice) { self.data = data } diff --git a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift index 5dfee0b0..39cd0951 100644 --- a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift +++ b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift @@ -15,204 +15,3 @@ import Foundation import HTTPTypes // MARK: - Functionality to be removed in the future - -extension ClientError { - /// Creates a new error. - /// - Parameters: - /// - operationID: The OpenAPI operation identifier. - /// - operationInput: The operation-specific Input value. - /// - request: The HTTP request created during the operation. - /// - requestBody: The HTTP request body created during the operation. - /// - baseURL: The base URL for HTTP requests. - /// - response: The HTTP response received during the operation. - /// - responseBody: The HTTP response body received during the operation. - /// - underlyingError: The underlying error that caused the operation - /// to fail. - @available( - *, - deprecated, - renamed: - "ClientError.init(operationID:operationInput:request:requestBody:baseURL:response:responseBody:causeDescription:underlyingError:)", - message: "Use the initializer with a causeDescription parameter." - ) public init( - operationID: String, - operationInput: any Sendable, - request: HTTPRequest? = nil, - requestBody: HTTPBody? = nil, - baseURL: URL? = nil, - response: HTTPResponse? = nil, - responseBody: HTTPBody? = nil, - underlyingError: any Error - ) { - self.init( - operationID: operationID, - operationInput: operationInput, - request: request, - requestBody: requestBody, - baseURL: baseURL, - response: response, - responseBody: responseBody, - causeDescription: "Legacy error without a causeDescription.", - underlyingError: underlyingError - ) - } -} - -extension ServerError { - /// Creates a new error. - /// - Parameters: - /// - operationID: The OpenAPI operation identifier. - /// - request: The HTTP request provided to the server. - /// - requestBody: The HTTP request body provided to 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 - /// to fail. - @available( - *, - deprecated, - renamed: - "ServerError.init(operationID:request:requestBody:requestMetadata:operationInput:operationOutput:causeDescription:underlyingError:)", - message: "Use the initializer with a causeDescription parameter." - ) public init( - operationID: String, - request: HTTPRequest, - requestBody: HTTPBody?, - requestMetadata: ServerRequestMetadata, - operationInput: (any Sendable)? = nil, - operationOutput: (any Sendable)? = nil, - underlyingError: any Error - ) { - self.init( - operationID: operationID, - request: request, - requestBody: requestBody, - requestMetadata: requestMetadata, - operationInput: operationInput, - operationOutput: operationOutput, - causeDescription: "Legacy error without a causeDescription.", - underlyingError: underlyingError - ) - } -} - -extension Converter { - /// Returns an error to be thrown when an unexpected content type is - /// received. - /// - Parameter contentType: The content type that was received. - /// - Returns: An error representing an unexpected content type. - @available(*, deprecated) public func makeUnexpectedContentTypeError(contentType: OpenAPIMIMEType?) -> any Error { - RuntimeError.unexpectedContentTypeHeader(contentType?.description ?? "") - } - - /// Checks whether a concrete content type matches an expected content type. - /// - /// The concrete content type can contain parameters, such as `charset`, but - /// they are ignored in the equality comparison. - /// - /// The expected content type can contain wildcards, such as */* and text/*. - /// - Parameters: - /// - received: The concrete content type to validate against the other. - /// - expectedRaw: The expected content type, can contain wildcards. - /// - Throws: A `RuntimeError` when `expectedRaw` is not a valid content type. - /// - Returns: A Boolean value representing whether the concrete content - /// type matches the expected one. - @available(*, deprecated) public func isMatchingContentType(received: OpenAPIMIMEType?, expectedRaw: String) throws - -> Bool - { - guard let received else { return false } - guard case let .concrete(type: receivedType, subtype: receivedSubtype) = received.kind else { return false } - guard let expectedContentType = OpenAPIMIMEType(expectedRaw) else { - throw RuntimeError.invalidExpectedContentType(expectedRaw) - } - switch expectedContentType.kind { - case .any: return true - case .anySubtype(let expectedType): return receivedType.lowercased() == expectedType.lowercased() - case .concrete(let expectedType, let expectedSubtype): - return receivedType.lowercased() == expectedType.lowercased() - && receivedSubtype.lowercased() == expectedSubtype.lowercased() - } - } -} - -extension DecodingError { - /// Returns a decoding error used by the oneOf decoder when not a single - /// child schema decodes the received payload. - /// - Parameters: - /// - type: The type representing the oneOf schema in which the decoding - /// occurred. - /// - codingPath: The coding path to the decoder that attempted to decode - /// the type. - /// - Returns: A decoding error. - @_spi(Generated) @available(*, deprecated) public static func failedToDecodeOneOfSchema( - type: Any.Type, - codingPath: [any CodingKey] - ) -> Self { - DecodingError.valueNotFound( - type, - DecodingError.Context.init( - codingPath: codingPath, - debugDescription: "The oneOf structure did not decode into any child schema." - ) - ) - } - - /// Returns a decoding error used by the anyOf decoder when not a single - /// child schema decodes the received payload. - /// - Parameters: - /// - type: The type representing the anyOf schema in which the decoding - /// occurred. - /// - codingPath: The coding path to the decoder that attempted to decode - /// the type. - /// - Returns: A decoding error. - @available(*, deprecated) static func failedToDecodeAnySchema(type: Any.Type, codingPath: [any CodingKey]) -> Self { - DecodingError.valueNotFound( - type, - DecodingError.Context.init( - codingPath: codingPath, - debugDescription: "The anyOf structure did not decode into any child schema." - ) - ) - } - - /// Verifies that the anyOf decoder successfully decoded at least one - /// child schema, and throws an error otherwise. - /// - Parameters: - /// - values: An array of optional values to check. - /// - type: The type representing the anyOf schema in which the decoding - /// occurred. - /// - codingPath: The coding path to the decoder that attempted to decode - /// the type. - /// - Throws: An error of type `DecodingError.failedToDecodeAnySchema` if none of the child schemas were successfully decoded. - @_spi(Generated) @available(*, deprecated) public static func verifyAtLeastOneSchemaIsNotNil( - _ values: [Any?], - type: Any.Type, - codingPath: [any CodingKey] - ) throws { - guard values.contains(where: { $0 != nil }) else { - throw DecodingError.failedToDecodeAnySchema(type: type, codingPath: codingPath) - } - } -} - -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/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift index bca29837..925ebf4f 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift @@ -21,26 +21,6 @@ final class Test_CommonConverterExtensions: Test_Runtime { // MARK: Miscs - @available(*, deprecated) 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))" - ) - } - } - func testBestContentType() throws { func testCase( received: String?, From 7d1644b125c02be1bedcfe1a69b58fae881b2393 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 27 Nov 2023 18:15:01 +0100 Subject: [PATCH 14/20] Bump version docs to 1.0.0-alpha.1 (#82) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump version docs to 1.0.0-alpha.1 ### Motivation _[Explain here the context, and why you're making that change. What is the problem you're trying to solve.]_ ### Modifications _[Describe the modifications you've made.]_ ### Result _[After your change, what will change.]_ ### Test Plan _[Describe the steps you took, or will take, to qualify the change - such as adjusting tests and manual testing.]_ Reviewed by: simonjbeaumont Builds: ✔︎ pull request validation (5.10) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (api breakage) - Build finished. ✔︎ pull request validation (docc test) - Build finished. ✔︎ pull request validation (integration test) - Build finished. ✔︎ pull request validation (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. https://github.com/apple/swift-openapi-runtime/pull/82 --- 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 5fd6a69a..b74f3b9a 100644 --- a/README.md +++ b/README.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.3.0") + exact: "1.0.0-alpha.1" ), ``` diff --git a/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md b/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md index f124e7d8..1d1fec26 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.3.0") + exact: "1.0.0-alpha.1" ), ``` From 32cef1a271e8edc41a637a2527b299a03832adfe Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 30 Nov 2023 09:48:40 +0100 Subject: [PATCH 15/20] Add visionOS platform support (#85) ### Motivation While this isn't technically necessary, as all versions of a platform not explicitly mentioned are assumed to be supported, it's better to be explicit here. ### Modifications Add `visionOS(.v1)` to the list of supported platforms. ### Result Clearer support matrix. ### Test Plan N/A, this is basically just a documentation change. --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index f8045977..fcdf464f 100644 --- a/Package.swift +++ b/Package.swift @@ -24,7 +24,7 @@ let swiftSettings: [SwiftSetting] = [ let package = Package( name: "swift-openapi-runtime", platforms: [ - .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), + .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .visionOS(.v1) ], products: [ .library( From 3300cc48eb3563339dbdb5dd0fb21d9327940d4d Mon Sep 17 00:00:00 2001 From: Si Beaumont Date: Thu, 30 Nov 2023 20:23:12 +0000 Subject: [PATCH 16/20] Add Docker Compose file for Swift 5.9.0 (#86) --- docker/docker-compose.2204.590.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docker/docker-compose.2204.590.yaml diff --git a/docker/docker-compose.2204.590.yaml b/docker/docker-compose.2204.590.yaml new file mode 100644 index 00000000..dacf9e3c --- /dev/null +++ b/docker/docker-compose.2204.590.yaml @@ -0,0 +1,19 @@ +version: "3" + +services: + runtime-setup: + image: &image swift-openapi-runtime:22.04-5.9.0 + build: + args: + ubuntu_version: "jammy" + swift_version: "5.9.0" + + test: + image: *image + environment: + - WARN_AS_ERROR_ARG=-Xswiftc -warnings-as-errors + - IMPORT_CHECK_ARG=--explicit-target-dependency-import-check error + - STRICT_CONCURRENCY_ARG=-Xswiftc -strict-concurrency=complete + + shell: + image: *image From 3452f2b837c634f48d07909fa819224bcacde5c6 Mon Sep 17 00:00:00 2001 From: Mahdi Bahrami Date: Fri, 1 Dec 2023 12:33:15 +0330 Subject: [PATCH 17/20] Remove no-longer-needed `@preconcurrency`s (#83) See https://github.com/apple/swift-openapi-generator/pull/396. --------- Co-authored-by: Honza Dvorsky --- Sources/OpenAPIRuntime/Conversion/Converter.swift | 4 ++-- Sources/OpenAPIRuntime/Errors/ClientError.swift | 4 ++-- Sources/OpenAPIRuntime/Interface/ClientTransport.swift | 4 ---- Sources/OpenAPIRuntime/Interface/UniversalClient.swift | 2 +- Sources/OpenAPIRuntime/Interface/UniversalServer.swift | 4 +--- .../URICoder/Test_URICodingRoundtrip.swift | 8 +++++--- 6 files changed, 11 insertions(+), 15 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/Converter.swift b/Sources/OpenAPIRuntime/Conversion/Converter.swift index bd7566b9..69223da5 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter.swift @@ -12,11 +12,11 @@ // //===----------------------------------------------------------------------===// #if canImport(Darwin) -import Foundation +import class Foundation.JSONEncoder #else @preconcurrency import class Foundation.JSONEncoder -@preconcurrency import class Foundation.JSONDecoder #endif +import class Foundation.JSONDecoder /// Converter between generated and HTTP currency types. @_spi(Generated) public struct Converter: Sendable { diff --git a/Sources/OpenAPIRuntime/Errors/ClientError.swift b/Sources/OpenAPIRuntime/Errors/ClientError.swift index 5a20f224..90481bff 100644 --- a/Sources/OpenAPIRuntime/Errors/ClientError.swift +++ b/Sources/OpenAPIRuntime/Errors/ClientError.swift @@ -14,11 +14,11 @@ import HTTPTypes #if canImport(Darwin) -import Foundation +import struct Foundation.URL #else @preconcurrency import struct Foundation.URL -@preconcurrency import protocol Foundation.LocalizedError #endif +import protocol Foundation.LocalizedError /// An error thrown by a client performing an OpenAPI operation. /// diff --git a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift index 3786bcea..200520ca 100644 --- a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift +++ b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift @@ -13,11 +13,7 @@ //===----------------------------------------------------------------------===// import HTTPTypes -#if canImport(Darwin) import struct Foundation.URL -#else -@preconcurrency import struct Foundation.URL -#endif /// A type that performs HTTP operations. /// diff --git a/Sources/OpenAPIRuntime/Interface/UniversalClient.swift b/Sources/OpenAPIRuntime/Interface/UniversalClient.swift index 431fb8af..5afff2b1 100644 --- a/Sources/OpenAPIRuntime/Interface/UniversalClient.swift +++ b/Sources/OpenAPIRuntime/Interface/UniversalClient.swift @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// import HTTPTypes #if canImport(Darwin) -import Foundation +import struct Foundation.URL #else @preconcurrency import struct Foundation.URL #endif diff --git a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift index 80d69e25..4608dafe 100644 --- a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift +++ b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift @@ -13,14 +13,12 @@ //===----------------------------------------------------------------------===// import HTTPTypes - #if canImport(Darwin) import struct Foundation.URL -import struct Foundation.URLComponents #else @preconcurrency import struct Foundation.URL -@preconcurrency import struct Foundation.URLComponents #endif +import struct Foundation.URLComponents /// OpenAPI document-agnostic HTTP server used by OpenAPI document-specific, /// generated servers to perform request deserialization, middleware and handler diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift index 0487c756..ccfe52c4 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift @@ -12,10 +12,12 @@ // //===----------------------------------------------------------------------===// import XCTest -@_spi(Generated) @testable import OpenAPIRuntime -#if os(Linux) -@preconcurrency import Foundation +#if canImport(Darwin) || swift(>=5.9.1) +import struct Foundation.Date +#else +@preconcurrency import struct Foundation.Date #endif +@_spi(Generated) @testable import OpenAPIRuntime final class Test_URICodingRoundtrip: Test_Runtime { From 860a3ed237c952c0ddf3f9814ed20dcc1b36363a Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 5 Dec 2023 17:54:38 +0100 Subject: [PATCH 18/20] [Docs] Make sure all symbols are curated (#88) ### Motivation Ensure all symbols are curated. ### Modifications Added symbols that were showing up in automatic sections to existing curated sections. ### Result All symbols are curated. ### Test Plan Built docs locally. --- .../Documentation.docc/Documentation.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md b/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md index 1d1fec26..2e6f3399 100644 --- a/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md +++ b/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md @@ -68,6 +68,18 @@ Please report any issues related to this library in the [swift-openapi-generator - ``Configuration`` - ``DateTranscoder`` - ``ISO8601DateTranscoder`` +- ``MultipartBoundaryGenerator`` +- ``RandomMultipartBoundaryGenerator`` +- ``ConstantMultipartBoundaryGenerator`` +- ``IterationBehavior`` + +### Content types +- ``HTTPBody`` +- ``Base64EncodedData`` +- ``MultipartBody`` +- ``MultipartRawPart`` +- ``MultipartPart`` +- ``MultipartDynamicallyNamedPart`` ### Errors - ``ClientError`` @@ -77,6 +89,9 @@ Please report any issues related to this library in the [swift-openapi-generator ### HTTP Currency Types - ``HTTPBody`` - ``ServerRequestMetadata`` +- ``AcceptableProtocol`` +- ``AcceptHeaderContentType`` +- ``QualityValue`` ### Dynamic Payloads - ``OpenAPIValueContainer`` From 160ff92214cf5a1aa9503ca3092220c2c4ff4f1a Mon Sep 17 00:00:00 2001 From: Si Beaumont Date: Mon, 11 Dec 2023 09:53:58 +0000 Subject: [PATCH 19/20] Add issue template, redirecting to swift-openapi-generator issues --- .github/ISSUE_TEMPLATE/config.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..548a1a83 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,6 @@ +blank_issues_enabled: false +contact_links: + - name: 🐞 Open an issue on the Swift OpenAPI Generator repository + url: https://github.com/apple/swift-openapi-generator/issues + about: > + Issues for all repositories in the Swift OpenAPI Generator project are centralized in the swift-openapi-generator repository. From bc1023f948f0093e2d79244feedb2203379d148a Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 11 Dec 2023 11:52:19 +0100 Subject: [PATCH 20/20] [Docs] Prep 1.0.0 (#89) ### Motivation Prep docs for 1.0. ### Modifications See above. ### Result Ready to tag 1.0. ### Test Plan Manual inspection. --- README.md | 28 +++++++--------- .../Base/Base64EncodedData.swift | 2 +- .../Documentation.docc/Documentation.md | 21 ++++-------- .../Interface/ClientTransport.swift | 33 ++++--------------- .../Interface/ServerTransport.swift | 15 ++++----- 5 files changed, 34 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index b74f3b9a..75c8720f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Swift OpenAPI Generator Runtime [![](https://img.shields.io/badge/docc-read_documentation-blue)](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation) +[![](https://img.shields.io/github/v/release/apple/swift-openapi-runtime)](https://github.com/apple/swift-openapi-runtime/releases) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fapple%2Fswift-openapi-runtime%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/apple/swift-openapi-runtime) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fapple%2Fswift-openapi-runtime%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/apple/swift-openapi-runtime) @@ -10,27 +11,26 @@ This library provides common abstractions and helper functions used by the clien 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. +- Protocol definitions for pluggable layers, including [`ClientTransport`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/clienttransport), [`ServerTransport`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/servertransport), [`ClientMiddleware`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/clientmiddleware), and [`ServerMiddleware`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/servermiddleware). + +Many of the HTTP currency types used are defined in the [Swift HTTP Types](https://github.com/apple/swift-http-types) library. + +> Tip: Check out the [example projects](https://swiftpackageindex.com/apple/swift-openapi-generator/documentation/swift-openapi-generator/checking-out-an-example-project) focused on middlewares. ## Usage Add the package dependency in your `Package.swift`: ```swift -.package( - url: "https://github.com/apple/swift-openapi-runtime", - exact: "1.0.0-alpha.1" -), +.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.0.0"), ``` -Note that this repository does not have a 1.0 tag yet, so the API is not stable. - Next, in your target, add `OpenAPIRuntime` to your dependencies: ```swift .target(name: "MyTarget", dependencies: [ .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), -], +]), ``` The next step depends on your use case. @@ -45,17 +45,13 @@ Swift OpenAPI Generator generates client and server code that is designed to be Implement a new transport or middleware by providing a type that adopts one of the protocols from the runtime library: -* `ClientTransport` -* `ClientMiddleware` -* `ServerTransport` -* `ServerMiddleware` +* [`ClientTransport`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/clienttransport) +* [`ClientMiddleware`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/clientmiddleware) +* [`ServerTransport`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/servertransport) +* [`ServerMiddleware`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/servermiddleware) You can also publish your transport or middleware as a Swift package to allow others to use it with their generated code. -## Reporting issues - -Please report any issues related to this library in the [swift-openapi-generator](https://github.com/apple/swift-openapi-generator/issues) repository. - ## Documentation To learn more, check out the full [documentation][2]. diff --git a/Sources/OpenAPIRuntime/Base/Base64EncodedData.swift b/Sources/OpenAPIRuntime/Base/Base64EncodedData.swift index 7af50f72..ce3c43fa 100644 --- a/Sources/OpenAPIRuntime/Base/Base64EncodedData.swift +++ b/Sources/OpenAPIRuntime/Base/Base64EncodedData.swift @@ -14,7 +14,7 @@ import Foundation -/// Provides a route to encode or decode base64-encoded data +/// A type for converting data as a base64 string. /// /// This type holds raw, unencoded, data as a slice of bytes. It can be used to encode that /// data to a provided `Encoder` as base64-encoded data or to decode from base64 encoding when diff --git a/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md b/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md index 2e6f3399..8b16cf53 100644 --- a/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md +++ b/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md @@ -8,29 +8,26 @@ This library provides common abstractions and helper functions used by the clien 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. +- Protocol definitions for pluggable layers, including ``ClientTransport``, ``ServerTransport``, ``ClientMiddleware``, and ``ServerMiddleware``. Many of the HTTP currency types used are defined in the [Swift HTTP Types](https://github.com/apple/swift-http-types) library. +> Tip: Check out the [example projects](https://swiftpackageindex.com/apple/swift-openapi-generator/documentation/swift-openapi-generator/checking-out-an-example-project) focused on middlewares. + ### Usage Add the package dependency in your `Package.swift`: ```swift -.package( - url: "https://github.com/apple/swift-openapi-runtime", - exact: "1.0.0-alpha.1" -), +.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.0.0"), ``` -Note that this repository does not have a 1.0 tag yet, so the API is not stable. - Next, in your target, add `OpenAPIRuntime` to your dependencies: ```swift .target(name: "MyTarget", dependencies: [ .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), -], +]), ``` The next step depends on your use case. @@ -52,19 +49,15 @@ Implement a new transport or middleware by providing a type that adopts one of t You can also publish your transport or middleware as a Swift package to allow others to use it with their generated code. -### Reporting issues - -Please report any issues related to this library in the [swift-openapi-generator](https://github.com/apple/swift-openapi-generator/issues) repository. - ## Topics ### Essentials - ``ClientTransport`` - ``ServerTransport`` - -### Customization - ``ClientMiddleware`` - ``ServerMiddleware`` + +### Customization - ``Configuration`` - ``DateTranscoder`` - ``ISO8601DateTranscoder`` diff --git a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift index 200520ca..cb20c651 100644 --- a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift +++ b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift @@ -41,17 +41,11 @@ import struct Foundation.URL /// /// 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, +/// serverURL: URL(string: "https://example.com")!, /// transport: transport /// ) /// @@ -59,16 +53,7 @@ import struct Foundation.URL /// 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, _): -/// // ... -/// } +/// let response = try await client.checkHealth() /// /// The generated operation method takes an `Input` type unique to /// the operation, and returns an `Output` type unique to the operation. @@ -107,16 +92,13 @@ import struct Foundation.URL /// Then in your test code, instantiate and provide the test transport to your /// generated client instead: /// -/// let transport = TestTransport() +/// var 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, +/// serverURL: URL(string: "https://example.com")!, /// transport: transport /// ) -/// let response = try await client.checkHealth(.init()) -/// // ... +/// let response = try await client.checkHealth() /// /// Implementing a test client transport is just one way to help test your /// code that integrates with a generated client. Another is to implement @@ -172,7 +154,7 @@ public protocol ClientTransport: Sendable { /// the middleware to the initializer of the generated `Client` type: /// /// let client = Client( -/// serverURL: serverURL, +/// serverURL: URL(string: "https://example.com")!, /// transport: transport, /// middlewares: [ /// loggingMiddleware, @@ -181,8 +163,7 @@ public protocol ClientTransport: Sendable { /// /// Then make a call to one of the generated client methods: /// -/// let response = try await client.checkHealth(.init()) -/// // ... +/// let response = try await client.checkHealth() /// /// As part of the invocation of `checkHealth`, the client first invokes /// the middlewares in the order you provided them, and then passes the request diff --git a/Sources/OpenAPIRuntime/Interface/ServerTransport.swift b/Sources/OpenAPIRuntime/Interface/ServerTransport.swift index 2ee147bc..40e16e8f 100644 --- a/Sources/OpenAPIRuntime/Interface/ServerTransport.swift +++ b/Sources/OpenAPIRuntime/Interface/ServerTransport.swift @@ -71,20 +71,19 @@ import HTTPTypes /// /// 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() +/// be expected by the clients. /// /// Register the generated request handlers by calling the method generated /// on the `APIProtocol` protocol: /// -/// try handler.registerHandlers(on: transport, serverURL: serverURL) +/// try handler.registerHandlers( +/// on: transport, +/// serverURL: URL(string: "/api/v1")! +/// ) /// /// Start the server by following the documentation of your chosen transport: /// -/// try app.run() +/// try await app.execute() /// /// ### Implement a custom server transport /// @@ -161,7 +160,7 @@ public protocol ServerTransport { /// /// try handler.registerHandlers( /// on: transport, -/// serverURL: serverURL, +/// serverURL: URL(string: "/api/v1")!, /// middlewares: [ /// loggingMiddleware, /// ]