From 1eaf2367556fb4ff6662694d19e7bba811f26ae6 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 27 Sep 2023 09:19:21 +0200 Subject: [PATCH] [Runtime] Async bodies + swift-http-types adoption (#47) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Runtime] Async bodies + swift-http-types adoption ### Motivation Runtime changes of the approved proposals https://github.com/apple/swift-openapi-generator/pull/255 and https://github.com/apple/swift-openapi-generator/pull/254. ### Blockers of merging this - [x] 1.0 of swift-http-types ### Modifications ⚠️ Contains breaking changes, this will land to main and then 0.3.0 will be tagged, so not backwards compatible with 0.2.0. - add a dependency on https://github.com/apple/swift-http-types - remove our currency types `Request`, `Response`, `HeaderField` - replace them with the types provided by `HTTPTypes` - remove `...AsString` and the whole string-based serialization strategy, which was only ever used by bodies, but with streaming, we can't safely stream string chunks, only byte chunks, so we instead provide utils on `HTTPBody` to create it from string, and to collect it into a string. This means that the underlying type for the `text/plain` content type is now `HTTPBody` (a sequence of byte chunks) as opposed to `String` - adapted Converter extensions to work with the new types - added some internal utils for working with the query section of a path, as `HTTPTypes` doesn't provide that, the `path` property contains both the path and the query components (in `setEscapedQueryItem`) - adapted error types - adapted printing of request/response types, now no bytes of the body are printed, as they cannot be assumed to be consumable more than once - adjusted the transport and middleware protocols, as described in the proposal - removed the `queryParameters` property from `ServerRequestMetadata`, as now we parse the full query ourselves, instead of relying on the server transport to do it for us - removed `RouterPathComponent` as now we pass the full path string to the server transport in the `register` function, allowing transport with more flexible routers to handle mixed path components, e.g. `/foo/{bar}.zip`. - introduced `HTTPBody`, as described by the proposal - adjusted UniversalClient and UniversalServer - moved from String to Substring in a few types, to allow more passthrough of parsed string data without copying ### Result SOAR-0004 and SOAR-0005 implemented. ### Test Plan Introduced and adjusted tests for all of the above. Reviewed by: Builds: ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (docc test) - Build finished. ✔︎ pull request validation (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. ✖︎ pull request validation (api breakage) - Build finished. ✖︎ pull request validation (integration test) - Build finished. https://github.com/apple/swift-openapi-runtime/pull/47 --- .swift-format | 2 +- Package.swift | 7 +- README.md | 2 +- .../Conversion/Converter+Client.swift | 108 +-- .../Conversion/Converter+Common.swift | 17 +- .../Conversion/Converter+Server.swift | 130 +-- .../Conversion/CurrencyExtensions.swift | 511 ++++++++---- .../Conversion/FoundationExtensions.swift | 39 - .../Documentation.docc/Documentation.md | 10 +- .../OpenAPIRuntime/Errors/ClientError.swift | 43 +- .../OpenAPIRuntime/Errors/RuntimeError.swift | 9 + .../OpenAPIRuntime/Errors/ServerError.swift | 33 +- .../Interface/ClientTransport.swift | 59 +- .../Interface/CurrencyTypes.swift | 311 ++----- .../OpenAPIRuntime/Interface/HTTPBody.swift | 782 ++++++++++++++++++ .../Interface/ServerTransport.swift | 44 +- .../Interface/UniversalClient.swift | 37 +- .../Interface/UniversalServer.swift | 110 +-- .../StringCoder/StringDecoder.swift | 448 ---------- .../StringCoder/StringEncoder.swift | 446 ---------- .../URICoder/Decoding/URIDecoder.swift | 6 +- .../URIValueFromNodeDecoder+Unkeyed.swift | 2 +- .../URICoder/Parsing/URIParser.swift | 2 +- .../Conversion/Test_Converter+Client.swift | 179 ++-- .../Conversion/Test_Converter+Common.swift | 81 +- .../Conversion/Test_Converter+Server.swift | 176 ++-- .../Conversion/Test_CurrencyExtensions.swift | 86 -- .../Interface/Test_HTTPBody.swift | 292 +++++++ .../Interface/Test_UniversalServer.swift | 16 +- .../Test_StringCodingRoundtrip.swift | 132 --- Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 71 +- .../URICoder/Parsing/Test_URIParser.swift | 2 +- .../URICoder/Test_URICodingRoundtrip.swift | 2 +- 33 files changed, 1976 insertions(+), 2219 deletions(-) create mode 100644 Sources/OpenAPIRuntime/Interface/HTTPBody.swift delete mode 100644 Sources/OpenAPIRuntime/StringCoder/StringDecoder.swift delete mode 100644 Sources/OpenAPIRuntime/StringCoder/StringEncoder.swift delete mode 100644 Tests/OpenAPIRuntimeTests/Conversion/Test_CurrencyExtensions.swift create mode 100644 Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift delete mode 100644 Tests/OpenAPIRuntimeTests/StringCoder/Test_StringCodingRoundtrip.swift diff --git a/.swift-format b/.swift-format index e6a70edf..2afa7389 100644 --- a/.swift-format +++ b/.swift-format @@ -47,7 +47,7 @@ "UseLetInEveryBoundCaseVariable" : false, "UseShorthandTypeNames" : true, "UseSingleLinePropertyGetter" : false, - "UseSynthesizedInitializer" : true, + "UseSynthesizedInitializer" : false, "UseTripleSlashForDocumentationComments" : true, "UseWhereClausesInForLoops" : false, "ValidateDocumentationComments" : false diff --git a/Package.swift b/Package.swift index ea000944..960e3311 100644 --- a/Package.swift +++ b/Package.swift @@ -34,15 +34,18 @@ let package = Package( .library( name: "OpenAPIRuntime", targets: ["OpenAPIRuntime"] - ), + ) ], dependencies: [ + .package(url: "https://github.com/apple/swift-http-types", from: "1.0.0"), .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), ], targets: [ .target( name: "OpenAPIRuntime", - dependencies: [], + dependencies: [ + .product(name: "HTTPTypes", package: "swift-http-types") + ], swiftSettings: swiftSettings ), .testTarget( diff --git a/README.md b/README.md index 7565c433..d54d870b 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Add the package dependency in your `Package.swift`: ```swift .package( url: "https://github.com/apple/swift-openapi-runtime", - .upToNextMinor(from: "0.2.0") + .upToNextMinor(from: "0.3.0") ), ``` diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index b0f59867..40fd3e86 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// import Foundation +import HTTPTypes extension Converter { @@ -20,15 +21,10 @@ extension Converter { /// - headerFields: The header fields where to add the "accept" header. /// - contentTypes: The array of acceptable content types by the client. public func setAcceptHeader( - in headerFields: inout [HeaderField], + in headerFields: inout HTTPFields, contentTypes: [AcceptHeaderContentType] ) { - headerFields.append( - .init( - name: "accept", - value: contentTypes.map(\.rawValue).joined(separator: ", ") - ) - ) + headerFields[.accept] = contentTypes.map(\.rawValue).joined(separator: ", ") } // | client | set | request path | URI | required | renderedPath | @@ -60,7 +56,7 @@ extension Converter { // | client | set | request query | URI | both | setQueryItemAsURI | public func setQueryItemAsURI( - in request: inout Request, + in request: inout HTTPRequest, style: ParameterStyle?, explode: Bool?, name: String, @@ -84,40 +80,12 @@ extension Converter { ) } - // | client | set | request body | string | optional | setOptionalRequestBodyAsString | - public func setOptionalRequestBodyAsString( - _ value: T?, - headerFields: inout [HeaderField], - contentType: String - ) throws -> Data? { - try setOptionalRequestBody( - value, - headerFields: &headerFields, - contentType: contentType, - convert: convertToStringData - ) - } - - // | client | set | request body | string | required | setRequiredRequestBodyAsString | - public func setRequiredRequestBodyAsString( - _ value: T, - headerFields: inout [HeaderField], - contentType: String - ) throws -> Data { - try setRequiredRequestBody( - value, - headerFields: &headerFields, - contentType: contentType, - convert: convertToStringData - ) - } - // | client | set | request body | JSON | optional | setOptionalRequestBodyAsJSON | public func setOptionalRequestBodyAsJSON( _ value: T?, - headerFields: inout [HeaderField], + headerFields: inout HTTPFields, contentType: String - ) throws -> Data? { + ) throws -> HTTPBody? { try setOptionalRequestBody( value, headerFields: &headerFields, @@ -129,9 +97,9 @@ extension Converter { // | client | set | request body | JSON | required | setRequiredRequestBodyAsJSON | public func setRequiredRequestBodyAsJSON( _ value: T, - headerFields: inout [HeaderField], + headerFields: inout HTTPFields, contentType: String - ) throws -> Data { + ) throws -> HTTPBody { try setRequiredRequestBody( value, headerFields: &headerFields, @@ -142,38 +110,38 @@ extension Converter { // | client | set | request body | binary | optional | setOptionalRequestBodyAsBinary | public func setOptionalRequestBodyAsBinary( - _ value: Data?, - headerFields: inout [HeaderField], + _ value: HTTPBody?, + headerFields: inout HTTPFields, contentType: String - ) throws -> Data? { + ) throws -> HTTPBody? { try setOptionalRequestBody( value, headerFields: &headerFields, contentType: contentType, - convert: convertDataToBinary + convert: { $0 } ) } // | client | set | request body | binary | required | setRequiredRequestBodyAsBinary | public func setRequiredRequestBodyAsBinary( - _ value: Data, - headerFields: inout [HeaderField], + _ value: HTTPBody, + headerFields: inout HTTPFields, contentType: String - ) throws -> Data { + ) throws -> HTTPBody { try setRequiredRequestBody( value, headerFields: &headerFields, contentType: contentType, - convert: convertDataToBinary + convert: { $0 } ) } // | client | set | request body | urlEncodedForm | codable | optional | setOptionalRequestBodyAsURLEncodedForm | public func setOptionalRequestBodyAsURLEncodedForm( _ value: T, - headerFields: inout [HeaderField], + headerFields: inout HTTPFields, contentType: String - ) throws -> Data? { + ) throws -> HTTPBody? { try setOptionalRequestBody( value, headerFields: &headerFields, @@ -185,9 +153,9 @@ extension Converter { // | client | set | request body | urlEncodedForm | codable | required | setRequiredRequestBodyAsURLEncodedForm | public func setRequiredRequestBodyAsURLEncodedForm( _ value: T, - headerFields: inout [HeaderField], + headerFields: inout HTTPFields, contentType: String - ) throws -> Data { + ) throws -> HTTPBody { try setRequiredRequestBody( value, headerFields: &headerFields, @@ -196,27 +164,16 @@ extension Converter { ) } - // | client | get | response body | string | required | getResponseBodyAsString | - public func getResponseBodyAsString( - _ type: T.Type, - from data: Data, - transforming transform: (T) -> C - ) throws -> C { - try getResponseBody( - type, - from: data, - transforming: transform, - convert: convertFromStringData - ) - } - // | client | get | response body | JSON | required | getResponseBodyAsJSON | public func getResponseBodyAsJSON( _ type: T.Type, - from data: Data, + from data: HTTPBody?, transforming transform: (T) -> C - ) throws -> C { - try getResponseBody( + ) async throws -> C { + guard let data else { + throw RuntimeError.missingRequiredResponseBody + } + return try await getBufferingResponseBody( type, from: data, transforming: transform, @@ -226,15 +183,18 @@ extension Converter { // | client | get | response body | binary | required | getResponseBodyAsBinary | public func getResponseBodyAsBinary( - _ type: Data.Type, - from data: Data, - transforming transform: (Data) -> C + _ type: HTTPBody.Type, + from data: HTTPBody?, + transforming transform: (HTTPBody) -> C ) throws -> C { - try getResponseBody( + guard let data else { + throw RuntimeError.missingRequiredResponseBody + } + return try getResponseBody( type, from: data, transforming: transform, - convert: convertBinaryToData + convert: { $0 } ) } } diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift index 1630a397..fc9235a4 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// import Foundation +import HTTPTypes extension Converter { @@ -21,8 +22,8 @@ extension Converter { /// - Parameter headerFields: The header fields to inspect for the content /// type header. /// - Returns: The content type value, or nil if not found or invalid. - public func extractContentTypeIfPresent(in headerFields: [HeaderField]) -> OpenAPIMIMEType? { - guard let rawValue = headerFields.firstValue(name: "content-type") else { + public func extractContentTypeIfPresent(in headerFields: HTTPFields) -> OpenAPIMIMEType? { + guard let rawValue = headerFields[.contentType] else { return nil } return OpenAPIMIMEType(rawValue) @@ -72,7 +73,7 @@ extension Converter { // | common | set | header field | URI | both | setHeaderFieldAsURI | public func setHeaderFieldAsURI( - in headerFields: inout [HeaderField], + in headerFields: inout HTTPFields, name: String, value: T? ) throws { @@ -97,7 +98,7 @@ extension Converter { // | common | set | header field | JSON | both | setHeaderFieldAsJSON | public func setHeaderFieldAsJSON( - in headerFields: inout [HeaderField], + in headerFields: inout HTTPFields, name: String, value: T? ) throws { @@ -111,7 +112,7 @@ extension Converter { // | common | get | header field | URI | optional | getOptionalHeaderFieldAsURI | public func getOptionalHeaderFieldAsURI( - in headerFields: [HeaderField], + in headerFields: HTTPFields, name: String, as type: T.Type ) throws -> T? { @@ -133,7 +134,7 @@ extension Converter { // | common | get | header field | URI | required | getRequiredHeaderFieldAsURI | public func getRequiredHeaderFieldAsURI( - in headerFields: [HeaderField], + in headerFields: HTTPFields, name: String, as type: T.Type ) throws -> T { @@ -155,7 +156,7 @@ extension Converter { // | common | get | header field | JSON | optional | getOptionalHeaderFieldAsJSON | public func getOptionalHeaderFieldAsJSON( - in headerFields: [HeaderField], + in headerFields: HTTPFields, name: String, as type: T.Type ) throws -> T? { @@ -169,7 +170,7 @@ extension Converter { // | common | get | header field | JSON | required | getRequiredHeaderFieldAsJSON | public func getRequiredHeaderFieldAsJSON( - in headerFields: [HeaderField], + in headerFields: HTTPFields, name: String, as type: T.Type ) throws -> T { diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index db063a67..b8ff2aa1 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// import Foundation +import HTTPTypes extension Converter { @@ -23,9 +24,9 @@ extension Converter { /// - Returns: The parsed content types, or the default content types if /// the header was not provided. public func extractAcceptHeaderIfPresent( - in headerFields: [HeaderField] + in headerFields: HTTPFields ) throws -> [AcceptHeaderContentType] { - guard let rawValue = headerFields.firstValue(name: "accept") else { + guard let rawValue = headerFields[.accept] else { return AcceptHeaderContentType.defaultValues } let rawComponents = @@ -50,10 +51,12 @@ extension Converter { /// Also supports wildcars, such as "application/\*" and "\*/\*". public func validateAcceptIfPresent( _ substring: String, - in headerFields: [HeaderField] + in headerFields: HTTPFields ) throws { // for example: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8 - let acceptHeader = headerFields.values(name: "accept").joined(separator: ", ") + guard let acceptHeader = headerFields[.accept] else { + return + } // Split with commas to get the individual values let acceptValues = @@ -84,7 +87,7 @@ extension Converter { // | server | get | request path | URI | required | getPathParameterAsURI | public func getPathParameterAsURI( - in pathParameters: [String: String], + in pathParameters: [String: Substring], name: String, as type: T.Type ) throws -> T { @@ -113,7 +116,7 @@ extension Converter { // | server | get | request query | URI | optional | getOptionalQueryItemAsURI | public func getOptionalQueryItemAsURI( - in query: String?, + in query: Substring?, style: ParameterStyle?, explode: Bool?, name: String, @@ -146,7 +149,7 @@ extension Converter { // | server | get | request query | URI | required | getRequiredQueryItemAsURI | public func getRequiredQueryItemAsURI( - in query: String?, + in query: Substring?, style: ParameterStyle?, explode: Bool?, name: String, @@ -177,53 +180,13 @@ extension Converter { ) } - // | server | get | request body | string | optional | getOptionalRequestBodyAsString | - public func getOptionalRequestBodyAsString( - _ type: T.Type, - from data: Data?, - transforming transform: (T) -> C - ) throws -> C? { - try getOptionalRequestBody( - type, - from: data, - transforming: transform, - convert: { encodedData in - let decoder = StringDecoder( - dateTranscoder: configuration.dateTranscoder - ) - let encodedString = String(decoding: encodedData, as: UTF8.self) - return try decoder.decode(T.self, from: encodedString) - } - ) - } - - // | server | get | request body | string | required | getRequiredRequestBodyAsString | - public func getRequiredRequestBodyAsString( - _ type: T.Type, - from data: Data?, - transforming transform: (T) -> C - ) throws -> C { - try getRequiredRequestBody( - type, - from: data, - transforming: transform, - convert: { encodedData in - let decoder = StringDecoder( - dateTranscoder: configuration.dateTranscoder - ) - let encodedString = String(decoding: encodedData, as: UTF8.self) - return try decoder.decode(T.self, from: encodedString) - } - ) - } - // | server | get | request body | JSON | optional | getOptionalRequestBodyAsJSON | public func getOptionalRequestBodyAsJSON( _ type: T.Type, - from data: Data?, + from data: HTTPBody?, transforming transform: (T) -> C - ) throws -> C? { - try getOptionalRequestBody( + ) async throws -> C? { + try await getOptionalBufferingRequestBody( type, from: data, transforming: transform, @@ -234,10 +197,10 @@ extension Converter { // | server | get | request body | JSON | required | getRequiredRequestBodyAsJSON | public func getRequiredRequestBodyAsJSON( _ type: T.Type, - from data: Data?, + from data: HTTPBody?, transforming transform: (T) -> C - ) throws -> C { - try getRequiredRequestBody( + ) async throws -> C { + try await getRequiredBufferingRequestBody( type, from: data, transforming: transform, @@ -247,39 +210,39 @@ extension Converter { // | server | get | request body | binary | optional | getOptionalRequestBodyAsBinary | public func getOptionalRequestBodyAsBinary( - _ type: Data.Type, - from data: Data?, - transforming transform: (Data) -> C + _ type: HTTPBody.Type, + from data: HTTPBody?, + transforming transform: (HTTPBody) -> C ) throws -> C? { try getOptionalRequestBody( type, from: data, transforming: transform, - convert: convertBinaryToData + convert: { $0 } ) } // | server | get | request body | binary | required | getRequiredRequestBodyAsBinary | public func getRequiredRequestBodyAsBinary( - _ type: Data.Type, - from data: Data?, - transforming transform: (Data) -> C + _ type: HTTPBody.Type, + from data: HTTPBody?, + transforming transform: (HTTPBody) -> C ) throws -> C { try getRequiredRequestBody( type, from: data, transforming: transform, - convert: convertBinaryToData + convert: { $0 } ) } // | server | get | request body | URLEncodedForm | codable | optional | getOptionalRequestBodyAsURLEncodedForm | public func getOptionalRequestBodyAsURLEncodedForm( _ type: T.Type, - from data: Data?, + from data: HTTPBody?, transforming transform: (T) -> C - ) throws -> C? { - try getOptionalRequestBody( + ) async throws -> C? { + try await getOptionalBufferingRequestBody( type, from: data, transforming: transform, @@ -290,10 +253,10 @@ extension Converter { // | server | get | request body | URLEncodedForm | codable | required | getRequiredRequestBodyAsURLEncodedForm | public func getRequiredRequestBodyAsURLEncodedForm( _ type: T.Type, - from data: Data?, + from data: HTTPBody?, transforming transform: (T) -> C - ) throws -> C { - try getRequiredRequestBody( + ) async throws -> C { + try await getRequiredBufferingRequestBody( type, from: data, transforming: transform, @@ -301,33 +264,12 @@ extension Converter { ) } - // | server | set | response body | string | required | setResponseBodyAsString | - public func setResponseBodyAsString( - _ value: T, - headerFields: inout [HeaderField], - contentType: String - ) throws -> Data { - try setResponseBody( - value, - headerFields: &headerFields, - contentType: contentType, - convert: { value in - let encoder = StringEncoder( - dateTranscoder: configuration.dateTranscoder - ) - let encodedString = try encoder.encode(value) - let encodedData = Data(encodedString.utf8) - return encodedData - } - ) - } - // | server | set | response body | JSON | required | setResponseBodyAsJSON | public func setResponseBodyAsJSON( _ value: T, - headerFields: inout [HeaderField], + headerFields: inout HTTPFields, contentType: String - ) throws -> Data { + ) throws -> HTTPBody { try setResponseBody( value, headerFields: &headerFields, @@ -338,15 +280,15 @@ extension Converter { // | server | set | response body | binary | required | setResponseBodyAsBinary | public func setResponseBodyAsBinary( - _ value: Data, - headerFields: inout [HeaderField], + _ value: HTTPBody, + headerFields: inout HTTPFields, contentType: String - ) throws -> Data { + ) throws -> HTTPBody { try setResponseBody( value, headerFields: &headerFields, contentType: contentType, - convert: convertDataToBinary + convert: { $0 } ) } } diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index 074795e1..b6c92d1e 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -12,89 +12,18 @@ // //===----------------------------------------------------------------------===// import Foundation - -extension HeaderField: CustomStringConvertible { - public var description: String { - "\(name): \(value)" - } -} - -extension Request: CustomStringConvertible { - public var description: String { - "path: \(path), query: \(query ?? ""), method: \(method), header fields: \(headerFields.description), body (prefix): \(body?.prettyPrefix ?? "")" - } -} - -extension Response: CustomStringConvertible { - public var description: String { - "status: \(statusCode), header fields: \(headerFields.description), body: \(body.prettyPrefix)" - } -} - -extension ServerRequestMetadata: CustomStringConvertible { - public var description: String { - "path parameters: \(pathParameters.description), query parameters: \(queryParameters.description)" - } -} - -extension Array where Element == HeaderField { - - /// Adds a header for the provided name and value. - /// - Parameters: - /// - name: Header name. - /// - value: Header value. If nil, the header is not added. - mutating func add(name: String, value: String?) { - guard let value = value else { - return - } - append(.init(name: name, value: value)) - } - - /// Adds headers for the provided name and values. - /// - Parameters: - /// - name: Header name. - /// - value: Header values. - mutating func add(name: String, values: [String]?) { - guard let values = values else { - return - } - for value in values { - append(.init(name: name, value: value)) - } - } - - /// Removes all headers matching the provided (case-insensitive) name. - /// - Parameters: - /// - name: Header name. - mutating func removeAll(named name: String) { - removeAll { - $0.name.caseInsensitiveCompare(name) == .orderedSame - } - } - - /// Returns the first header value for the provided (case-insensitive) name. - /// - Parameter name: Header name. - /// - Returns: First value for the given name. Nil if one does not exist. - func firstValue(name: String) -> String? { - first { $0.name.caseInsensitiveCompare(name) == .orderedSame }?.value - } - - /// Returns all header values for the given (case-insensitive) name. - /// - Parameter name: Header name. - /// - Returns: All values for the given name, might be empty if none are found. - func values(name: String) -> [String] { - filter { $0.name.caseInsensitiveCompare(name) == .orderedSame }.map { $0.value } - } -} +import HTTPTypes extension ParameterStyle { /// Returns the parameter style and explode parameter that should be used /// based on the provided inputs, taking defaults into considerations. /// - Parameters: + /// - name: The name of the query item for which to resolve the inputs. /// - style: The provided parameter style, if any. /// - explode: The provided explode value, if any. /// - Throws: For an unsupported input combination. + /// - Returns: A tuple of the style and explode values. static func resolvedQueryStyleAndExplode( name: String, style: ParameterStyle?, @@ -114,10 +43,44 @@ extension ParameterStyle { } } +extension HTTPField.Name { + + /// Creates a new name for the provided string. + /// - Parameter name: A field name. + /// - Throws: If the name isn't a valid field name. + init(validated name: String) throws { + guard let fieldName = Self(name) else { + throw RuntimeError.invalidHeaderFieldName(name) + } + self = fieldName + } +} + +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 + } + return path[...] + } + } +} + extension Converter { // MARK: Converter helpers + /// Creates a new configuration for the URI coder. + /// - Parameters: + /// - style: A parameter style. + /// - explode: An explode value. + /// - inBody: A Boolean value indicating whether the URI coder is being + /// used for encoding a body URI. Specify `false` if used for a query, + /// header, and so on. + /// - Returns: A new URI coder configuration. func uriCoderConfiguration( style: ParameterStyle, explode: Bool, @@ -131,6 +94,16 @@ extension Converter { ) } + /// Returns a URI encoded string for the provided inputs. + /// - Parameters: + /// - style: A parameter style. + /// - explode: An explode value. + /// - inBody: A Boolean value indicating whether the URI coder is being + /// used for encoding a body URI. Specify `false` if used for a query, + /// header, and so on. + /// - key: The key to be encoded with the value. + /// - value: The value to be encoded. + /// - Returns: A URI encoded string. func convertToURI( style: ParameterStyle, explode: Bool, @@ -149,12 +122,22 @@ extension Converter { return encodedString } + /// Returns a value decoded from a URI encoded string. + /// - Parameters: + /// - style: A parameter style. + /// - explode: An explode value. + /// - inBody: A Boolean value indicating whether the URI coder is being + /// used for encoding a body URI. Specify `false` if used for a query, + /// header, and so on. + /// - key: The key for which the value was decoded. + /// - encodedValue: The encoded value to be decoded. + /// - Returns: A decoded value. func convertFromURI( style: ParameterStyle, explode: Bool, inBody: Bool, key: String, - encodedValue: String + encodedValue: Substring ) throws -> T { let decoder = URIDecoder( configuration: uriCoderConfiguration( @@ -171,15 +154,32 @@ extension Converter { return value } + /// Returns a value decoded from a JSON body. + /// - Parameter body: The body containing the raw JSON bytes. + /// - Returns: A decoded value. func convertJSONToBodyCodable( - _ data: Data - ) throws -> T { - try decoder.decode(T.self, from: data) + _ body: HTTPBody + ) async throws -> T { + let data = try await Data(collecting: body, upTo: .max) + return try decoder.decode(T.self, from: data) } + /// Returns a JSON body for the provided encodable value. + /// - Parameter value: The value to encode as JSON. + /// - Returns: The raw JSON body. + func convertBodyCodableToJSON( + _ value: T + ) throws -> HTTPBody { + let data = try encoder.encode(value) + return HTTPBody(data) + } + + /// Returns a value decoded from a URL-encoded form body. + /// - Parameter body: The body containing the raw URL-encoded form bytes. + /// - Returns: A decoded value. func convertURLEncodedFormToCodable( - _ data: Data - ) throws -> T { + _ body: HTTPBody + ) async throws -> T { let decoder = URIDecoder( configuration: .init( style: .form, @@ -188,19 +188,17 @@ extension Converter { dateTranscoder: configuration.dateTranscoder ) ) - let uriString = String(decoding: data, as: UTF8.self) + let data = try await Data(collecting: body, upTo: .max) + let uriString = Substring(decoding: data, as: UTF8.self) return try decoder.decode(T.self, from: uriString) } - func convertBodyCodableToJSON( - _ value: T - ) throws -> Data { - try encoder.encode(value) - } - + /// Returns a URL-encoded form string for the provided encodable value. + /// - Parameter value: The value to encode. + /// - Returns: The raw URL-encoded form body. func convertBodyCodableToURLFormData( _ value: T - ) throws -> Data { + ) throws -> HTTPBody { let encoder = URIEncoder( configuration: .init( style: .form, @@ -210,10 +208,12 @@ extension Converter { ) ) let encodedString = try encoder.encode(value, forKey: "") - let data = Data(encodedString.utf8) - return data + return HTTPBody(encodedString) } + /// Returns a JSON string for the provided encodable value. + /// - Parameter value: The value to encode. + /// - Returns: A JSON string. func convertHeaderFieldCodableToJSON( _ value: T ) throws -> String { @@ -222,53 +222,26 @@ extension Converter { return stringValue } + /// Returns a value decoded from the provided JSON string. + /// - Parameter stringValue: A JSON string. + /// - Returns: The decoded value. func convertJSONToHeaderFieldCodable( - _ stringValue: String + _ stringValue: Substring ) throws -> T { let data = Data(stringValue.utf8) return try decoder.decode(T.self, from: data) } - func convertFromStringData( - _ data: Data - ) throws -> T { - let encodedString = String(decoding: data, as: UTF8.self) - let decoder = StringDecoder( - dateTranscoder: configuration.dateTranscoder - ) - let value = try decoder.decode( - T.self, - from: encodedString - ) - return value - } - - func convertToStringData( - _ value: T - ) throws -> Data { - let encoder = StringEncoder( - dateTranscoder: configuration.dateTranscoder - ) - let encodedString = try encoder.encode(value) - return Data(encodedString.utf8) - } - - func convertBinaryToData( - _ binary: Data - ) throws -> Data { - binary - } - - func convertDataToBinary( - _ data: Data - ) throws -> Data { - data - } - // MARK: - Helpers for specific types of parameters + /// Sets the provided header field into the header field storage. + /// - Parameters: + /// - headerFields: The header field storage. + /// - name: The name of the header to set. + /// - value: The value of the header to set. + /// - convert: The closure used to serialize the header value to string. func setHeaderField( - in headerFields: inout [HeaderField], + in headerFields: inout HTTPFields, name: String, value: T?, convert: (T) throws -> String @@ -276,59 +249,86 @@ extension Converter { guard let value else { return } - headerFields.add( - name: name, - value: try convert(value) + try headerFields.append( + .init( + name: .init(validated: name), + value: convert(value) + ) ) } + /// Returns the value of the header with the provided name from the provided + /// header field storage. + /// - Parameters: + /// - headerFields: The header field storage. + /// - name: The name of the header field. + /// - Returns: The value of the header field, if found. Nil otherwise. func getHeaderFieldValuesString( - in headerFields: [HeaderField], + in headerFields: HTTPFields, name: String - ) -> String? { - let values = headerFields.values(name: name) - guard !values.isEmpty else { - return nil - } - return values.joined(separator: ",") + ) throws -> String? { + try headerFields[.init(validated: name)] } + /// Returns a decoded value for the header field with the provided name. + /// - Parameters: + /// - headerFields: The header field storage. + /// - name: The name of the header field. + /// - type: The type to decode the value as. + /// - convert: The closure to convert the value from string. + /// - Returns: The decoded value, if found. Nil otherwise. func getOptionalHeaderField( - in headerFields: [HeaderField], + in headerFields: HTTPFields, name: String, as type: T.Type, - convert: (String) throws -> T + convert: (Substring) throws -> T ) throws -> T? { guard - let stringValue = getHeaderFieldValuesString( + let stringValue = try getHeaderFieldValuesString( in: headerFields, name: name ) else { return nil } - return try convert(stringValue) + return try convert(stringValue[...]) } + /// Returns a decoded value for the header field with the provided name. + /// - Parameters: + /// - headerFields: The header field storage. + /// - name: The name of the header field. + /// - type: The type to decode the value as. + /// - convert: The closure to convert the value from string. + /// - Returns: The decoded value. func getRequiredHeaderField( - in headerFields: [HeaderField], + in headerFields: HTTPFields, name: String, as type: T.Type, - convert: (String) throws -> T + convert: (Substring) throws -> T ) throws -> T { guard - let stringValue = getHeaderFieldValuesString( + let stringValue = try getHeaderFieldValuesString( in: headerFields, name: name ) else { throw RuntimeError.missingRequiredHeaderField(name) } - return try convert(stringValue) + return try convert(stringValue[...]) } + /// Sets a query parameter with the provided inputs. + /// - Parameters: + /// - request: The request to set the query parameter on. + /// - style: A parameter style. + /// - explode: An explode value. + /// - name: The name of the query parameter. + /// - value: The value of the query parameter. Must already be + /// percent-escaped. + /// - convert: The closure that converts the provided value to string. func setEscapedQueryItem( - in request: inout Request, + in request: inout HTTPRequest, style: ParameterStyle?, explode: Bool?, name: String, @@ -343,17 +343,50 @@ extension Converter { style: style, explode: explode ) - let uriSnippet = try convert(value, resolvedStyle, resolvedExplode) - request.addEscapedQuerySnippet(uriSnippet) + let escapedUriSnippet = try convert(value, resolvedStyle, resolvedExplode) + + let pathAndAll = try request.requiredPath + + // https://datatracker.ietf.org/doc/html/rfc3986#section-3.4 + // > The query component is indicated by the first question + // > mark ("?") character and terminated by a number sign ("#") + // > character or by the end of the URI. + + let fragmentStart = pathAndAll.firstIndex(of: "#") ?? pathAndAll.endIndex + let fragment = pathAndAll[fragmentStart..( - in query: String?, + in query: Substring?, style: ParameterStyle?, explode: Bool?, name: String, as type: T.Type, - convert: (String, ParameterStyle, Bool) throws -> T? + convert: (Substring, ParameterStyle, Bool) throws -> T? ) throws -> T? { guard let query, !query.isEmpty else { return nil @@ -366,13 +399,23 @@ extension Converter { return try convert(query, resolvedStyle, resolvedExplode) } + /// Returns the decoded value for the provided name of a query parameter. + /// - Parameters: + /// - query: The full encoded query string from which to extract the + /// parameter. + /// - style: A parameter style. + /// - explode: An explode value. + /// - name: The name of the query parameter. + /// - type: The type to decode the string value as. + /// - convert: The closure that decodes the value from string. + /// - Returns: A decoded value. func getRequiredQueryItem( - in query: String?, + in query: Substring?, style: ParameterStyle?, explode: Bool?, name: String, as type: T.Type, - convert: (String, ParameterStyle, Bool) throws -> T + convert: (Substring, ParameterStyle, Bool) throws -> T ) throws -> T { guard let value = try getOptionalQueryItem( @@ -389,22 +432,38 @@ extension Converter { return value } + /// Sets the provided request body and the appropriate content type. + /// - Parameters: + /// - value: The value to encode into the body. + /// - headerFields: The header fields storage where to save the content + /// type. + /// - contentType: The content type value. + /// - convert: The closure that encodes the value into a raw body. + /// - Returns: The body. func setRequiredRequestBody( _ value: T, - headerFields: inout [HeaderField], + headerFields: inout HTTPFields, contentType: String, - convert: (T) throws -> Data - ) throws -> Data { - headerFields.add(name: "content-type", value: contentType) + convert: (T) throws -> HTTPBody + ) throws -> HTTPBody { + headerFields[.contentType] = contentType return try convert(value) } + /// Sets the provided request body and the appropriate content type. + /// - Parameters: + /// - value: The value to encode into the body. + /// - headerFields: The header fields storage where to save the content + /// type. + /// - contentType: The content type value. + /// - convert: The closure that encodes the value into a raw body. + /// - Returns: The body, if value was not nil. func setOptionalRequestBody( _ value: T?, - headerFields: inout [HeaderField], + headerFields: inout HTTPFields, contentType: String, - convert: (T) throws -> Data - ) throws -> Data? { + convert: (T) throws -> HTTPBody + ) throws -> HTTPBody? { guard let value else { return nil } @@ -416,29 +475,89 @@ extension Converter { ) } + /// Returns a value decoded from the provided body. + /// - Parameters: + /// - type: The type to decode. + /// - body: The body to decode the value from. + /// - transform: The closure that wraps the body in its generated type. + /// - convert: The closure that decodes the body. + /// - Returns: A decoded wrapped type, if body is not nil. + func getOptionalBufferingRequestBody( + _ type: T.Type, + from body: HTTPBody?, + transforming transform: (T) -> C, + convert: (HTTPBody) async throws -> T + ) async throws -> C? { + guard let body else { + return nil + } + let decoded = try await convert(body) + return transform(decoded) + } + + /// Returns a value decoded from the provided body. + /// - Parameters: + /// - type: The type to decode. + /// - body: The body to decode the value from. + /// - transform: The closure that wraps the body in its generated type. + /// - convert: The closure that decodes the body. + /// - Returns: A decoded wrapped type. + func getRequiredBufferingRequestBody( + _ type: T.Type, + from body: HTTPBody?, + transforming transform: (T) -> C, + convert: (HTTPBody) async throws -> T + ) async throws -> C { + guard + let body = try await getOptionalBufferingRequestBody( + type, + from: body, + transforming: transform, + convert: convert + ) + else { + throw RuntimeError.missingRequiredRequestBody + } + return body + } + + /// Returns a value decoded from the provided body. + /// - Parameters: + /// - type: The type to decode. + /// - body: The body to decode the value from. + /// - transform: The closure that wraps the body in its generated type. + /// - convert: The closure that decodes the body. + /// - Returns: A decoded wrapped type, if body is not nil. func getOptionalRequestBody( _ type: T.Type, - from data: Data?, + from body: HTTPBody?, transforming transform: (T) -> C, - convert: (Data) throws -> T + convert: (HTTPBody) throws -> T ) throws -> C? { - guard let data else { + guard let body else { return nil } - let decoded = try convert(data) + let decoded = try convert(body) return transform(decoded) } + /// Returns a value decoded from the provided body. + /// - Parameters: + /// - type: The type to decode. + /// - body: The body to decode the value from. + /// - transform: The closure that wraps the body in its generated type. + /// - convert: The closure that decodes the body. + /// - Returns: A decoded wrapped type. func getRequiredRequestBody( _ type: T.Type, - from data: Data?, + from body: HTTPBody?, transforming transform: (T) -> C, - convert: (Data) throws -> T + convert: (HTTPBody) throws -> T ) throws -> C { guard let body = try getOptionalRequestBody( type, - from: data, + from: body, transforming: transform, convert: convert ) @@ -448,32 +567,72 @@ extension Converter { return body } + /// Returns a value decoded from the provided body. + /// - Parameters: + /// - type: The type to decode. + /// - body: The body to decode the value from. + /// - transform: The closure that wraps the body in its generated type. + /// - convert: The closure that decodes the body. + /// - Returns: A decoded wrapped type. + func getBufferingResponseBody( + _ type: T.Type, + from body: HTTPBody, + transforming transform: (T) -> C, + convert: (HTTPBody) async throws -> T + ) async throws -> C { + let parsedValue = try await convert(body) + let transformedValue = transform(parsedValue) + return transformedValue + } + + /// Returns a value decoded from the provided body. + /// - Parameters: + /// - type: The type to decode. + /// - body: The body to decode the value from. + /// - transform: The closure that wraps the body in its generated type. + /// - convert: The closure that decodes the body. + /// - Returns: A decoded wrapped type. func getResponseBody( _ type: T.Type, - from data: Data, + from body: HTTPBody, transforming transform: (T) -> C, - convert: (Data) throws -> T + convert: (HTTPBody) throws -> T ) throws -> C { - let parsedValue = try convert(data) + let parsedValue = try convert(body) let transformedValue = transform(parsedValue) return transformedValue } + /// Sets the provided request body and the appropriate content type. + /// - Parameters: + /// - value: The value to encode into the body. + /// - headerFields: The header fields storage where to save the content + /// type. + /// - contentType: The content type value. + /// - convert: The closure that encodes the value into a raw body. + /// - Returns: The body, if value was not nil. func setResponseBody( _ value: T, - headerFields: inout [HeaderField], + headerFields: inout HTTPFields, contentType: String, - convert: (T) throws -> Data - ) throws -> Data { - headerFields.add(name: "content-type", value: contentType) + convert: (T) throws -> HTTPBody + ) throws -> HTTPBody { + headerFields[.contentType] = contentType return try convert(value) } + /// Returns a decoded value for the provided path parameter. + /// - Parameters: + /// - pathParameters: The storage of path parameters. + /// - name: The name of the path parameter. + /// - type: The type to decode the value as. + /// - convert: The closure that decodes the value from string. + /// - Returns: A decoded value. func getRequiredRequestPath( - in pathParameters: [String: String], + in pathParameters: [String: Substring], name: String, as type: T.Type, - convert: (String) throws -> T + convert: (Substring) throws -> T ) throws -> T { guard let untypedValue = pathParameters[name] else { throw RuntimeError.missingRequiredPathParameter(name) diff --git a/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift b/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift index ce6d6833..0d39d580 100644 --- a/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift @@ -13,45 +13,6 @@ //===----------------------------------------------------------------------===// import Foundation -extension Data { - /// Returns a pretty representation of the Data. - var pretty: String { - String(decoding: self, as: UTF8.self) - } - - /// Returns a prefix of a pretty representation of the Data. - var prettyPrefix: String { - prefix(256).pretty - } -} - -extension Request { - /// Allows modifying the parsed query parameters of the request. - mutating func mutateQuery(_ closure: (inout URLComponents) throws -> Void) rethrows { - var urlComponents = URLComponents() - if let query { - urlComponents.percentEncodedQuery = query - } - try closure(&urlComponents) - query = urlComponents.percentEncodedQuery - } - - /// Adds the provided URI snippet to the URL's query. - /// - /// Percent encoding is already applied. - /// - Parameters: - /// - snippet: A full URI snippet. - mutating func addEscapedQuerySnippet(_ snippet: String) { - let prefix: String - if let query { - prefix = query + "&" - } else { - prefix = "" - } - query = prefix + snippet - } -} - extension String { /// Returns the string with leading and trailing whitespace (such as spaces diff --git a/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md b/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md index 10119b46..f124e7d8 100644 --- a/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md +++ b/Sources/OpenAPIRuntime/Documentation.docc/Documentation.md @@ -10,6 +10,8 @@ It contains: - Common types used in the code generated by the `swift-openapi-generator` package plugin. - Protocol definitions for pluggable layers, including ``ClientTransport``, ``ServerTransport``, and middleware. +Many of the HTTP currency types used are defined in the [Swift HTTP Types](https://github.com/apple/swift-http-types) library. + ### Usage Add the package dependency in your `Package.swift`: @@ -17,7 +19,7 @@ Add the package dependency in your `Package.swift`: ```swift .package( url: "https://github.com/apple/swift-openapi-runtime", - .upToNextMinor(from: "0.2.0") + .upToNextMinor(from: "0.3.0") ), ``` @@ -73,12 +75,8 @@ Please report any issues related to this library in the [swift-openapi-generator - ``UndocumentedPayload`` ### HTTP Currency Types -- ``Request`` -- ``Response`` -- ``HTTPMethod`` -- ``HeaderField`` +- ``HTTPBody`` - ``ServerRequestMetadata`` -- ``RouterPathComponent`` ### Dynamic Payloads - ``OpenAPIValueContainer`` diff --git a/Sources/OpenAPIRuntime/Errors/ClientError.swift b/Sources/OpenAPIRuntime/Errors/ClientError.swift index 0bd5d349..7663688a 100644 --- a/Sources/OpenAPIRuntime/Errors/ClientError.swift +++ b/Sources/OpenAPIRuntime/Errors/ClientError.swift @@ -11,6 +11,8 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// + +import HTTPTypes #if canImport(Darwin) import Foundation #else @@ -20,11 +22,14 @@ import Foundation /// An error thrown by a client performing an OpenAPI operation. /// -/// Use a `ClientError` to inspect details about the request and response that resulted in an error. +/// Use a `ClientError` to inspect details about the request and response +/// that resulted in an error. /// -/// You don't create or throw instances of `ClientError` yourself; they are created and thrown on -/// your behalf by the runtime library when a client operation fails. +/// You don't create or throw instances of `ClientError` yourself; they are +/// created and thrown on your behalf by the runtime library when a client +/// operation fails. public struct ClientError: Error { + /// The identifier of the operation, as defined in the OpenAPI document. public var operationID: String @@ -35,7 +40,13 @@ public struct ClientError: Error { /// /// Will be nil if the error resulted before the request was generated, /// for example if generating the request from the Input failed. - public var request: Request? + public var request: HTTPRequest? + + /// The HTTP request body created during the operation. + /// + /// Will be nil if the error resulted before the request was generated, + /// for example if generating the request from the Input failed. + public var requestBody: HTTPBody? /// The base URL for HTTP requests. /// @@ -45,8 +56,13 @@ public struct ClientError: Error { /// The HTTP response received during the operation. /// - /// Will be `nil` if the error resulted before the `Response` was received. - public var response: Response? + /// Will be nil if the error resulted before the response was received. + public var response: HTTPResponse? + + /// The HTTP response body received during the operation. + /// + /// Will be nil if the error resulted before the response was received. + public var responseBody: HTTPBody? /// The underlying error that caused the operation to fail. public var underlyingError: any Error @@ -56,22 +72,29 @@ public struct ClientError: Error { /// - operationID: The OpenAPI operation identifier. /// - operationInput: The operation-specific Input value. /// - request: The HTTP request created during the operation. + /// - request: The HTTP request body created during the operation. /// - baseURL: The base URL for HTTP requests. /// - response: The HTTP response received during the operation. - /// - underlyingError: The underlying error that caused the operation to fail. + /// - response: The HTTP response body received during the operation. + /// - underlyingError: The underlying error that caused the operation + /// to fail. public init( operationID: String, operationInput: any Sendable, - request: Request? = nil, + request: HTTPRequest? = nil, + requestBody: HTTPBody? = nil, baseURL: URL? = nil, - response: Response? = nil, + response: HTTPResponse? = nil, + responseBody: HTTPBody? = nil, underlyingError: any Error ) { self.operationID = operationID self.operationInput = operationInput self.request = request + self.requestBody = requestBody self.baseURL = baseURL self.response = response + self.responseBody = responseBody self.underlyingError = underlyingError } @@ -87,7 +110,7 @@ public struct ClientError: Error { extension ClientError: CustomStringConvertible { public var description: String { - "Client error - operationID: \(operationID), operationInput: \(String(describing: operationInput)), request: \(request?.description ?? ""), baseURL: \(baseURL?.absoluteString ?? ""), response: \(response?.description ?? ""), underlying error: \(underlyingErrorDescription)" + "Client error - operationID: \(operationID), operationInput: \(String(describing: operationInput)), request: \(request?.prettyDescription ?? ""), requestBody: \(requestBody?.prettyDescription ?? ""), baseURL: \(baseURL?.absoluteString ?? ""), response: \(response?.prettyDescription ?? ""), responseBody: \(responseBody?.prettyDescription ?? "") , underlying error: \(underlyingErrorDescription)" } } diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index 205b0e33..4304b4cd 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -20,6 +20,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret // Miscs case invalidServerURL(String) case invalidExpectedContentType(String) + case invalidHeaderFieldName(String) // Data conversion case failedToDecodeStringConvertibleValue(type: String) @@ -41,12 +42,14 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret // Path case missingRequiredPathParameter(String) + case pathUnset // Query case missingRequiredQueryParameter(String) // Body case missingRequiredRequestBody + case missingRequiredResponseBody // Transport/Handler case transportFailed(any Error) @@ -64,6 +67,8 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret return "Invalid server URL: \(string)" case .invalidExpectedContentType(let string): return "Invalid expected content type: '\(string)'" + case .invalidHeaderFieldName(let name): + return "Invalid header field name: '\(name)'" case .failedToDecodeStringConvertibleValue(let string): return "Failed to decode a value of type '\(string)'." case .unsupportedParameterStyle(name: let name, location: let location, style: let style, explode: let explode): @@ -79,10 +84,14 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret return "Malformed Accept header: \(accept)" case .missingRequiredPathParameter(let name): return "Missing required path parameter named: \(name)" + case .pathUnset: + return "Path was not set on the request." case .missingRequiredQueryParameter(let name): return "Missing required query parameter named: \(name)" case .missingRequiredRequestBody: return "Missing required request body" + case .missingRequiredResponseBody: + return "Missing required response body" case .transportFailed(let underlyingError): return "Transport failed with error: \(underlyingError.localizedDescription)" case .handlerFailed(let underlyingError): diff --git a/Sources/OpenAPIRuntime/Errors/ServerError.swift b/Sources/OpenAPIRuntime/Errors/ServerError.swift index 52985d6a..b93ac72b 100644 --- a/Sources/OpenAPIRuntime/Errors/ServerError.swift +++ b/Sources/OpenAPIRuntime/Errors/ServerError.swift @@ -11,25 +11,31 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// -import Foundation + +import HTTPTypes +import protocol Foundation.LocalizedError /// An error thrown by a server handling an OpenAPI operation. public struct ServerError: Error { + /// Identifier of the operation that threw the error. public var operationID: String - /// HTTP request provided to the server. - public var request: Request + /// The HTTP request provided to the server. + public var request: HTTPRequest + + /// The HTTP request body provided to the server. + public var requestBody: HTTPBody? - /// Request metadata extracted by the server. + /// The request metadata extracted by the server. public var requestMetadata: ServerRequestMetadata - /// Operation-specific Input value. + /// An operation-specific Input value. /// /// Is nil if error was thrown during request -> Input conversion. public var operationInput: (any Sendable)? - /// Operation-specific Output value. + /// An operation-specific Output value. /// /// Is nil if error was thrown before/during Output -> response conversion. public var operationOutput: (any Sendable)? @@ -40,15 +46,17 @@ public struct ServerError: Error { /// Creates a new error. /// - Parameters: /// - operationID: The OpenAPI operation identifier. - /// - request: HTTP request provided to the server. - /// - requestMetadata: Request metadata extracted by the server. - /// - operationInput: Operation-specific Input value. - /// - operationOutput: Operation-specific Output value. + /// - 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. public init( operationID: String, - request: Request, + request: HTTPRequest, + requestBody: HTTPBody?, requestMetadata: ServerRequestMetadata, operationInput: (any Sendable)? = nil, operationOutput: (any Sendable)? = nil, @@ -56,6 +64,7 @@ public struct ServerError: Error { ) { self.operationID = operationID self.request = request + self.requestBody = requestBody self.requestMetadata = requestMetadata self.operationInput = operationInput self.operationOutput = operationOutput @@ -74,7 +83,7 @@ public struct ServerError: Error { extension ServerError: CustomStringConvertible { public var description: String { - "Server error - operationID: \(operationID), request: \(request.description), metadata: \(requestMetadata.description), operationInput: \(operationInput.map { String(describing: $0) } ?? ""), operationOutput: \(operationOutput.map { String(describing: $0) } ?? ""), underlying error: \(underlyingErrorDescription)" + "Server error - operationID: \(operationID), request: \(request.prettyDescription), requestBody: \(requestBody?.prettyDescription ?? ""), metadata: \(requestMetadata.description), operationInput: \(operationInput.map { String(describing: $0) } ?? ""), operationOutput: \(operationOutput.map { String(describing: $0) } ?? ""), underlying error: \(underlyingErrorDescription)" } } diff --git a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift index 1bbb7a06..9ac1bf27 100644 --- a/Sources/OpenAPIRuntime/Interface/ClientTransport.swift +++ b/Sources/OpenAPIRuntime/Interface/ClientTransport.swift @@ -11,7 +11,13 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// -import Foundation + +import HTTPTypes +#if canImport(Darwin) +import struct Foundation.URL +#else +@preconcurrency import struct Foundation.URL +#endif /// A type that performs HTTP operations. /// @@ -71,10 +77,10 @@ import Foundation /// The generated operation method takes an `Input` type unique to /// the operation, and returns an `Output` type unique to the operation. /// -/// > Note: You use the `Input` type to provide parameters such as HTTP request headers, -/// query items, path parameters, and request bodies; and inspect the `Output` -/// type to handle the received HTTP response status code, response header and -/// body. +/// > Note: You use the `Input` type to provide parameters such as HTTP request +/// headers, query items, path parameters, and request bodies; and inspect +/// the `Output` type to handle the received HTTP response status code, response +/// header and body. /// /// ### Implement a custom client transport /// @@ -90,11 +96,15 @@ import Foundation /// struct TestTransport: ClientTransport { /// var isHealthy: Bool = true /// func send( -/// _ request: Request, +/// _ request: HTTPRequest, +/// body: HTTPBody?, /// baseURL: URL, /// operationID: String -/// ) async throws -> Response { -/// Response(statusCode: isHealthy ? 200 : 500) +/// ) async throws -> (HTTPResponse, HTTPBody?) { +/// ( +/// HTTPResponse(status: isHealthy ? .ok : .internalServerError), +/// nil +/// ) /// } /// } /// @@ -122,14 +132,16 @@ public protocol ClientTransport: Sendable { /// HTTP response. /// - Parameters: /// - request: An HTTP request. + /// - body: An HTTP request body. /// - baseURL: A server base URL. /// - operationID: The identifier of the OpenAPI operation. - /// - Returns: An HTTP response. + /// - Returns: An HTTP response and its body. func send( - _ request: Request, + _ request: HTTPRequest, + body: HTTPBody?, baseURL: URL, operationID: String - ) async throws -> Response + ) async throws -> (HTTPResponse, HTTPBody?) } /// A type that intercepts HTTP requests and responses. @@ -200,16 +212,15 @@ public protocol ClientTransport: Sendable { /// var bearerToken: String /// /// func intercept( -/// _ request: Request, +/// _ request: HTTPRequest, +/// body: HTTPBody?, /// baseURL: URL, /// operationID: String, -/// next: (Request, URL) async throws -> Response -/// ) async throws -> Response { +/// next: (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) +/// ) async throws -> (HTTPResponse, HTTPBody?) { /// var request = request -/// request.headerFields.append(.init( -/// name: "Authorization", value: "Bearer \(bearerToken)" -/// )) -/// return try await next(request, baseURL) +/// request.headerFields[.authorization] = "Bearer \(bearerToken)" +/// return try await next(request, body, baseURL) /// } /// } /// @@ -225,14 +236,16 @@ public protocol ClientMiddleware: Sendable { /// Intercepts an outgoing HTTP request and an incoming HTTP response. /// - Parameters: /// - request: An HTTP request. - /// - baseURL: baseURL: A server base URL. + /// - body: An HTTP request body. + /// - baseURL: A server base URL. /// - operationID: The identifier of the OpenAPI operation. /// - next: A closure that calls the next middleware, or the transport. - /// - Returns: An HTTP response. + /// - Returns: An HTTP response and its body. func intercept( - _ request: Request, + _ request: HTTPRequest, + body: HTTPBody?, baseURL: URL, operationID: String, - next: @Sendable (Request, URL) async throws -> Response - ) async throws -> Response + next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) + ) async throws -> (HTTPResponse, HTTPBody?) } diff --git a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift index 88b82794..328c5e56 100644 --- a/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift +++ b/Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift @@ -11,281 +11,104 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// -#if canImport(Darwin) -import Foundation -#else -@preconcurrency import struct Foundation.Data -@preconcurrency import struct Foundation.URLQueryItem -#endif -/// A header field used in an HTTP request or response. -public struct HeaderField: Hashable, Sendable { +import HTTPTypes - /// The name of the HTTP header field. - public var name: String +/// A container for request metadata already parsed and validated +/// by the server transport. +public struct ServerRequestMetadata: Hashable, Sendable { - /// The value of the HTTP header field. - public var value: String + /// The path parameters parsed from the URL of the HTTP request. + public var pathParameters: [String: Substring] - /// Creates a new HTTP header field. + /// Creates a new metadata wrapper with the specified path and query parameters. /// - Parameters: - /// - name: A name of the HTTP header field. - /// - value: A value of the HTTP header field. - public init(name: String, value: String) { - self.name = name - self.value = value + /// - pathParameters: Path parameters parsed from the URL of the HTTP + /// request. + public init( + pathParameters: [String: Substring] = [:] + ) { + self.pathParameters = pathParameters } } -/// Describes the HTTP method used in an OpenAPI operation. -/// -/// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#fixed-fields-7 -public struct HTTPMethod: RawRepresentable, Hashable, Sendable { +extension HTTPRequest { - /// Describes an HTTP method explicitly supported by OpenAPI. - private enum OpenAPIHTTPMethod: String, Hashable, Sendable { - case GET - case PUT - case POST - case DELETE - case OPTIONS - case HEAD - case PATCH - case TRACE - } - - /// The underlying HTTP method. - private let value: OpenAPIHTTPMethod - - /// Creates a new method from the provided known supported HTTP method. - private init(value: OpenAPIHTTPMethod) { - self.value = value - } - - public init?(rawValue: String) { - guard let value = OpenAPIHTTPMethod(rawValue: rawValue) else { + /// Creates a new request. + /// - Parameters: + /// - path: The URL path of the resource. + /// - method: The HTTP method. + /// - headerFields: The HTTP header fields. + @_spi(Generated) + public init(soar_path path: String, method: Method, headerFields: HTTPFields = .init()) { + self.init(method: method, scheme: nil, authority: nil, path: path, headerFields: headerFields) + } + + /// The query substring of the request's path. + @_spi(Generated) + public var soar_query: Substring? { + guard let path else { return nil } - self.value = value - } - - public var rawValue: String { - value.rawValue - } - - /// The name of the HTTP method. - public var name: String { - rawValue - } - - /// Returns an HTTP GET method. - public static var get: Self { - .init(value: .GET) - } - - /// Returns an HTTP PUT method. - public static var put: Self { - .init(value: .PUT) - } - - /// Returns an HTTP POST method. - public static var post: Self { - .init(value: .POST) - } - - /// Returns an HTTP DELETE method. - public static var delete: Self { - .init(value: .DELETE) - } - - /// Returns an HTTP OPTIONS method. - public static var options: Self { - .init(value: .OPTIONS) - } - - /// Returns an HTTP HEAD method. - public static var head: Self { - .init(value: .HEAD) - } - - /// Returns an HTTP PATCH method. - public static var patch: Self { - .init(value: .PATCH) + 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)]" + } } -extension RouterPathComponent: ExpressibleByStringLiteral { - /// Creates a new component for the provided value. - /// - Parameter value: A string literal. If the string begins with - /// a colon (for example `":petId"`), it gets parsed as a parameter - /// component, otherwise it is treated as a constant component. - public init(stringLiteral value: StringLiteralType) { - if value.first == ":" { - self = .parameter(String(value.dropFirst())) - } else { - self = .constant(value) - } +extension HTTPResponse: PrettyStringConvertible { + var prettyDescription: String { + "\(status.code) [\(headerFields.prettyDescription)]" } } -extension RouterPathComponent: CustomStringConvertible { - public var description: String { - switch self { - case .constant(let string): - return string - case .parameter(let string): - return ":\(string)" - } +extension HTTPBody: PrettyStringConvertible { + var prettyDescription: String { + String(describing: self) } } diff --git a/Sources/OpenAPIRuntime/Interface/HTTPBody.swift b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift new file mode 100644 index 00000000..50b8b92d --- /dev/null +++ b/Sources/OpenAPIRuntime/Interface/HTTPBody.swift @@ -0,0 +1,782 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import class Foundation.NSLock +import protocol Foundation.LocalizedError +import struct Foundation.Data // only for convenience initializers + +/// A body of an HTTP request or HTTP response. +/// +/// Under the hood, it represents an async sequence of byte chunks. +/// +/// ## Creating a body from a buffer +/// There are convenience initializers to create a body from common types, such +/// as `Data`, `[UInt8]`, `ArraySlice`, and `String`. +/// +/// Create an empty body: +/// ```swift +/// let body = HTTPBody() +/// ``` +/// +/// Create a body from a byte chunk: +/// ```swift +/// let bytes: ArraySlice = ... +/// let body = HTTPBody(bytes) +/// ``` +/// +/// Create a body from `Foundation.Data`: +/// ```swift +/// let data: Foundation.Data = ... +/// let body = HTTPBody(data) +/// ``` +/// +/// Create a body from a string: +/// ```swift +/// let body = HTTPBody("Hello, world!") +/// ``` +/// +/// ## Creating a body from an async sequence +/// The body type also supports initialization from an async sequence. +/// +/// ```swift +/// let producingSequence = ... // an AsyncSequence +/// let length: HTTPBody.Length = .known(1024) // or .unknown +/// let body = HTTPBody( +/// producingSequence, +/// length: length, +/// iterationBehavior: .single // or .multiple +/// ) +/// ``` +/// +/// In addition to the async sequence, also provide the total body length, +/// if known (this can be sent in the `content-length` header), and whether +/// the sequence is safe to be iterated multiple times, or can only be iterated +/// once. +/// +/// Sequences that can be iterated multiple times work better when an HTTP +/// request needs to be retried, or if a redirect is encountered. +/// +/// In addition to providing the async sequence, you can also produce the body +/// using an `AsyncStream` or `AsyncThrowingStream`: +/// +/// ```swift +/// let body = HTTPBody( +/// AsyncStream(ArraySlice.self, { continuation in +/// continuation.yield([72, 69]) +/// continuation.yield([76, 76, 79]) +/// continuation.finish() +/// }), +/// length: .known(5) +/// ) +/// ``` +/// +/// ## Consuming a body as an async sequence +/// The `HTTPBody` type conforms to `AsyncSequence` and uses `ArraySlice` +/// as its element type, so it can be consumed in a streaming fashion, without +/// ever buffering the whole body in your process. +/// +/// For example, to get another sequence that contains only the size of each +/// chunk, and print each size, use: +/// +/// ```swift +/// let chunkSizes = body.map { chunk in chunk.count } +/// for try await chunkSize in chunkSizes { +/// print("Chunk size: \(chunkSize)") +/// } +/// ``` +/// +/// ## Consuming a body as a buffer +/// If you need to collect the whole body before processing it, use one of +/// the convenience initializers on the target types that take an `HTTPBody`. +/// +/// To get all the bytes, use the initializer on `ArraySlice` or `[UInt8]`: +/// +/// ```swift +/// let buffer = try await ArraySlice(collecting: body, upTo: 2 * 1024 * 1024) +/// ``` +/// +/// The body type provides more variants of the collecting initializer on commonly +/// used buffers, such as: +/// - `Foundation.Data` +/// - `Swift.String` +/// +/// > Important: You must provide the maximum number of bytes you can buffer in +/// memory, in the example above we provide 2 MB. If more bytes are available, +/// the method throws the `TooManyBytesError` to stop the process running out +/// of memory. While discouraged, you can provide `upTo: .max` to +/// read all the available bytes, without a limit. +public final class HTTPBody: @unchecked Sendable { + + /// The underlying byte chunk type. + 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 input sequence can be iterated. + public let iterationBehavior: IterationBehavior + + /// Describes the total length of the body, if known. + public enum Length: Sendable { + + /// Total length not known yet. + case unknown + + /// Total length is known. + case known(Int) + } + + /// The total length of the body, if known. + public let length: Length + + /// The underlying type-erased async sequence. + private let sequence: BodySequence + + /// A lock for shared mutable state. + private let lock: NSLock = { + let lock = NSLock() + lock.name = "com.apple.swift-openapi-generator.runtime.body" + return lock + }() + + /// 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 + } + + /// Verifying that creating another iterator is allowed based on + /// the values of `iterationBehavior` and `locked_iteratorCreated`. + /// - Throws: If another iterator is not allowed to be created. + private func checkIfCanCreateIterator() throws { + lock.lock() + defer { + lock.unlock() + } + guard iterationBehavior == .single else { + return + } + if locked_iteratorCreated { + throw TooManyIterationsError() + } + } + + /// Tries to mark an iterator as created, verifying that it is allowed + /// based on the values of `iterationBehavior` and `locked_iteratorCreated`. + /// - Throws: If another iterator is not allowed to be created. + private func tryToMarkIteratorCreated() throws { + lock.lock() + defer { + locked_iteratorCreated = true + lock.unlock() + } + guard iterationBehavior == .single else { + return + } + if locked_iteratorCreated { + throw TooManyIterationsError() + } + } + + /// Creates a new body. + /// - Parameters: + /// - sequence: The input sequence providing the byte chunks. + /// - length: The total length of the body, in other words the accumulated + /// length of all the byte chunks. + /// - iterationBehavior: The sequence's iteration behavior, which + /// indicates whether the sequence can be iterated multiple times. + @usableFromInline init( + _ sequence: BodySequence, + length: Length, + iterationBehavior: IterationBehavior + ) { + self.sequence = sequence + self.length = length + self.iterationBehavior = iterationBehavior + } + + /// Creates a new body with the provided sequence of byte chunks. + /// - Parameters: + /// - byteChunks: A sequence of byte chunks. + /// - length: The total length of the body. + /// - iterationBehavior: The iteration behavior of the sequence, which + /// indicates whether it can be iterated multiple times. + @usableFromInline convenience init( + _ byteChunks: some Sequence & Sendable, + length: Length, + iterationBehavior: IterationBehavior + ) { + self.init( + .init(WrappedSyncSequence(sequence: byteChunks)), + length: length, + iterationBehavior: iterationBehavior + ) + } +} + +extension HTTPBody: Equatable { + public static func == ( + lhs: HTTPBody, + rhs: HTTPBody + ) -> Bool { + ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } +} + +extension HTTPBody: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } +} + +// MARK: - Creating the HTTPBody + +extension HTTPBody { + + /// Creates a new empty body. + @inlinable public convenience init() { + 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 + ) { + 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 + ) { + self.init([bytes], length: .known(bytes.count), iterationBehavior: .multiple) + } + + /// Creates a new body with the provided byte sequence. + /// - Parameters: + /// - bytes: A byte chunk. + /// - length: The total length of the body. + /// - iterationBehavior: The iteration behavior of the sequence, which + /// indicates whether it can be iterated multiple times. + @inlinable public convenience init( + _ bytes: some Sequence & Sendable, + 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 + ) + } + + /// Creates a new body with the provided byte collection. + /// - Parameters: + /// - bytes: A byte chunk. + @inlinable public convenience init( + _ bytes: some Collection & Sendable + ) { + self.init(bytes, length: .known(bytes.count)) + } + + /// Creates a new body with the provided async throwing stream. + /// - Parameters: + /// - stream: An async throwing stream that provides the byte chunks. + /// - length: The total length of the body. + @inlinable public convenience init( + _ stream: AsyncThrowingStream, + length: HTTPBody.Length + ) { + self.init( + .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 + ) + } + + /// Creates a new body with the provided async sequence. + /// - Parameters: + /// - sequence: An async sequence that provides the byte chunks. + /// - length: The total length of the body. + /// - iterationBehavior: The iteration behavior of the sequence, which + /// indicates whether it can be iterated multiple times. + @inlinable public convenience init( + _ sequence: Bytes, + length: HTTPBody.Length, + iterationBehavior: IterationBehavior + ) where Bytes.Element == ByteChunk, Bytes: Sendable { + self.init( + .init(sequence), + length: length, + iterationBehavior: iterationBehavior + ) + } + + /// Creates a new body with the provided async sequence of byte sequences. + /// - Parameters: + /// - sequence: An async sequence that provides the byte chunks. + /// - length: The total length of the body. + /// - iterationBehavior: The iteration behavior of the sequence, which + /// indicates whether it can be iterated multiple times. + @inlinable public convenience init( + _ sequence: Bytes, + 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 + ) + } +} + +// MARK: - Consuming the body + +extension HTTPBody: AsyncSequence { + public typealias Element = ByteChunk + public typealias AsyncIterator = Iterator + public func makeAsyncIterator() -> AsyncIterator { + // The crash on error is intentional here. + try! tryToMarkIteratorCreated() + return sequence.makeAsyncIterator() + } +} + +extension HTTPBody { + + /// An error thrown by the collecting initializer when the body contains more + /// than the maximum allowed number of bytes. + private struct TooManyBytesError: Error, CustomStringConvertible, LocalizedError { + + /// 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 errorDescription: String? { + description + } + } + + /// An error thrown by the collecting initializer when another iteration of + /// the body is not allowed. + private struct TooManyIterationsError: Error, CustomStringConvertible, LocalizedError { + + var description: String { + "OpenAPIRuntime.HTTPBody attempted to create a second iterator, but the underlying sequence is only safe to be iterated once." + } + + var errorDescription: String? { + description + } + } + + /// Accumulates the full body in-memory into a single buffer + /// up to the provided maximum number of bytes and returns it. + /// - Parameters: + /// - maxBytes: The maximum number of bytes this method is allowed + /// to accumulate in memory before it throws an error. + /// - Throws: `TooManyBytesError` if the body contains more + /// than `maxBytes`. + /// - Returns: A byte chunk containing all the accumulated bytes. + fileprivate func collect(upTo maxBytes: Int) async throws -> ByteChunk { + + // Check that we're allowed to iterate again. + try checkIfCanCreateIterator() + + // If the length is known, verify it's within the limit. + if case .known(let knownBytes) = length { + guard knownBytes <= maxBytes else { + throw TooManyBytesError(maxBytes: maxBytes) + } + } + + // Accumulate the byte chunks. + var buffer = ByteChunk() + for try await chunk in self { + guard buffer.count + chunk.count <= maxBytes else { + throw TooManyBytesError(maxBytes: maxBytes) + } + buffer.append(contentsOf: chunk) + } + return buffer + } +} + +extension HTTPBody.ByteChunk { + /// Creates a byte chunk by accumulating the full body in-memory into a single buffer + /// up to the provided maximum number of bytes and returning it. + /// - Parameters: + /// - body: The HTTP body to collect. + /// - maxBytes: The maximum number of bytes this method is allowed + /// to accumulate in memory before it throws an error. + /// - Throws: `TooManyBytesError` if the body contains more + /// than `maxBytes`. + public init(collecting body: HTTPBody, upTo maxBytes: Int) async throws { + self = try await body.collect(upTo: maxBytes) + } +} + +extension Array where Element == UInt8 { + /// Creates a byte array by accumulating the full body in-memory into a single buffer + /// up to the provided maximum number of bytes and returning it. + /// - Parameters: + /// - body: The HTTP body to collect. + /// - maxBytes: The maximum number of bytes this method is allowed + /// to accumulate in memory before it throws an error. + /// - Throws: `TooManyBytesError` if the body contains more + /// than `maxBytes`. + public init(collecting body: HTTPBody, upTo maxBytes: Int) async throws { + self = try await Array(body.collect(upTo: maxBytes)) + } +} + +// MARK: - String-based bodies + +extension HTTPBody { + + /// Creates a new body with the provided string encoded as UTF-8 bytes. + /// - Parameters: + /// - string: A string to encode as bytes. + /// - length: The total length of the body. + @inlinable public convenience init( + _ string: some StringProtocol & Sendable, + length: Length + ) { + self.init( + ByteChunk(string), + length: length + ) + } + + /// Creates a new body with the provided string encoded as UTF-8 bytes. + /// - Parameters: + /// - string: A string to encode as bytes. + @inlinable public convenience init( + _ string: some StringProtocol & Sendable + ) { + self.init( + ByteChunk(string), + length: .known(string.count) + ) + } + + /// Creates a new body with the provided async throwing stream of strings. + /// - Parameters: + /// - stream: An async throwing stream that provides the string chunks. + /// - length: The total length of the body. + @inlinable public convenience init( + _ stream: AsyncThrowingStream, + length: HTTPBody.Length + ) { + 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 + ) + } + + /// Creates a new body with the provided async sequence of string chunks. + /// - Parameters: + /// - sequence: An async sequence that provides the string chunks. + /// - length: The total length of the body. + /// - iterationBehavior: The iteration behavior of the sequence, which + /// indicates whether it can be iterated multiple times. + @inlinable public convenience init( + _ sequence: Strings, + length: HTTPBody.Length, + iterationBehavior: IterationBehavior + ) where Strings.Element: StringProtocol & Sendable, Strings: Sendable { + self.init( + .init(sequence.map { ByteChunk.init($0) }), + length: length, + iterationBehavior: iterationBehavior + ) + } +} + +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)[...] + } +} + +extension String { + /// Creates a string by accumulating the full body in-memory into a single buffer up to + /// the provided maximum number of bytes, converting it to string using UTF-8 encoding. + /// - Parameters: + /// - body: The HTTP body to collect. + /// - maxBytes: The maximum number of bytes this method is allowed + /// to accumulate in memory before it throws an error. + /// - Throws: `TooManyBytesError` if the 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 + ) + } +} + +// MARK: - HTTPBody conversions + +extension HTTPBody: ExpressibleByStringLiteral { + 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[...]) + } +} + +extension HTTPBody: ExpressibleByArrayLiteral { + public typealias ArrayLiteralElement = UInt8 + 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)) + } +} + +extension Data { + /// Creates a Data by accumulating the full body in-memory into a single buffer up to + /// the provided maximum number of bytes and converting it to `Data`. + /// - Parameters: + /// - body: The HTTP body to collect. + /// - maxBytes: The maximum number of bytes this method is allowed + /// to accumulate in memory before it throws an error. + /// - Throws: `TooManyBytesError` if the body contains more + /// than `maxBytes`. + public init(collecting body: HTTPBody, upTo maxBytes: Int) async throws { + self = try await Data(body.collect(upTo: maxBytes)) + } +} + +// MARK: - Underlying async sequences + +extension HTTPBody { + + /// An async iterator of both input async sequences and of the body itself. + public struct Iterator: AsyncIteratorProtocol { + + /// The element byte chunk type. + public typealias Element = HTTPBody.ByteChunk + + /// 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() + } + } + + 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/Interface/ServerTransport.swift b/Sources/OpenAPIRuntime/Interface/ServerTransport.swift index 11774140..7944c3d9 100644 --- a/Sources/OpenAPIRuntime/Interface/ServerTransport.swift +++ b/Sources/OpenAPIRuntime/Interface/ServerTransport.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import HTTPTypes + /// A type that registers and handles HTTP operations. /// /// Decouples the HTTP server framework from the generated server code. @@ -108,14 +110,15 @@ public protocol ServerTransport { /// - Parameters: /// - handler: A handler to be invoked when an HTTP request is received. /// - method: An HTTP request method. - /// - path: The URL path components, for example `["pets", ":petId"]`. - /// - queryItemNames: The names of query items to be extracted - /// from the request URL that matches the provided HTTP operation. + /// - path: A URL template for the path, for example `/pets/{petId}`. + /// - Important: The `path` can have mixed components, such + /// as `/file/{name}.zip`. func register( - _ handler: @Sendable @escaping (Request, ServerRequestMetadata) async throws -> Response, - method: HTTPMethod, - path: [RouterPathComponent], - queryItemNames: Set + _ handler: @Sendable @escaping (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> ( + HTTPResponse, HTTPBody? + ), + method: HTTPRequest.Method, + path: String ) throws } @@ -182,16 +185,17 @@ public protocol ServerTransport { /// /// A middleware that prints request and response metadata. /// struct PrintingMiddleware: ServerMiddleware { /// func intercept( -/// _ request: Request, +/// _ request: HTTPRequest, +/// body: HTTPBody?, /// metadata: ServerRequestMetadata, /// operationID: String, -/// next: (Request, ServerRequestMetadata) async throws -> Response -/// ) async throws -> Response { -/// print(">>>: \(request.method.name) \(request.path)") +/// next: (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody?) +/// ) async throws -> (HTTPResponse, HTTPBody?) { +/// print(">>>: \(request.method.rawValue) \(request.soar_pathOnly)") /// do { -/// let response = try await next(request, metadata) -/// print("<<<: \(response.statusCode)") -/// return response +/// let (response, responseBody) = try await next(request, body, metadata) +/// print("<<<: \(response.status.code)") +/// return (response, responseBody) /// } catch { /// print("!!!: \(error.localizedDescription)") /// throw error @@ -208,15 +212,17 @@ public protocol ServerMiddleware: Sendable { /// Intercepts an incoming HTTP request and an outgoing HTTP response. /// - Parameters: /// - request: An HTTP request. + /// - body: An HTTP request body. /// - metadata: The metadata parsed from the HTTP request, including path - /// and query parameters. + /// parameters. /// - operationID: The identifier of the OpenAPI operation. /// - next: A closure that calls the next middleware, or the transport. - /// - Returns: An HTTP response. + /// - Returns: An HTTP response and its body. func intercept( - _ request: Request, + _ request: HTTPRequest, + body: HTTPBody?, metadata: ServerRequestMetadata, operationID: String, - next: @Sendable (Request, ServerRequestMetadata) async throws -> Response - ) async throws -> Response + next: @Sendable (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody?) + ) async throws -> (HTTPResponse, HTTPBody?) } diff --git a/Sources/OpenAPIRuntime/Interface/UniversalClient.swift b/Sources/OpenAPIRuntime/Interface/UniversalClient.swift index 7e486328..019ca728 100644 --- a/Sources/OpenAPIRuntime/Interface/UniversalClient.swift +++ b/Sources/OpenAPIRuntime/Interface/UniversalClient.swift @@ -11,6 +11,7 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// +import HTTPTypes #if canImport(Darwin) import Foundation #else @@ -22,20 +23,19 @@ import Foundation /// invocation, and response deserialization. /// /// Do not call this directly, only invoked by generated code. -@_spi(Generated) -public struct UniversalClient: Sendable { +@_spi(Generated) public struct UniversalClient: Sendable { /// The URL of the server, used as the base URL for requests made by the /// client. public let serverURL: URL - /// Converter for encoding/decoding data. + /// A converter for encoding/decoding data. public let converter: Converter - /// Type capable of sending HTTP requests and receiving HTTP responses. + /// A type capable of sending HTTP requests and receiving HTTP responses. public var transport: any ClientTransport - /// Middlewares to be invoked before `transport`. + /// The middlewares to be invoked before the transport. public var middlewares: [any ClientMiddleware] /// Internal initializer that takes an initialized `Converter`. @@ -86,8 +86,8 @@ public struct UniversalClient: Sendable { public func send( input: OperationInput, forOperation operationID: String, - serializer: @Sendable (OperationInput) throws -> Request, - deserializer: @Sendable (Response) throws -> OperationOutput + serializer: @Sendable (OperationInput) throws -> (HTTPRequest, HTTPBody?), + deserializer: @Sendable (HTTPResponse, HTTPBody?) async throws -> OperationOutput ) async throws -> OperationOutput where OperationInput: Sendable, OperationOutput: Sendable { @Sendable func wrappingErrors( @@ -102,30 +102,36 @@ public struct UniversalClient: Sendable { } let baseURL = serverURL func makeError( - request: Request? = nil, + request: HTTPRequest? = nil, + requestBody: HTTPBody? = nil, baseURL: URL? = nil, - response: Response? = nil, + response: HTTPResponse? = nil, + responseBody: HTTPBody? = nil, error: any Error ) -> any Error { ClientError( operationID: operationID, operationInput: input, request: request, + requestBody: requestBody, baseURL: baseURL, response: response, + responseBody: responseBody, underlyingError: error ) } - let request: Request = try await wrappingErrors { + let (request, requestBody): (HTTPRequest, HTTPBody?) = try await wrappingErrors { try serializer(input) } mapError: { error in makeError(error: error) } - let response: Response = try await wrappingErrors { - var next: @Sendable (Request, URL) async throws -> Response = { (_request, _url) in + let (response, responseBody): (HTTPResponse, HTTPBody?) = try await wrappingErrors { + var next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) = { + (_request, _body, _url) in try await wrappingErrors { try await transport.send( _request, + body: _body, baseURL: _url, operationID: operationID ) @@ -138,18 +144,19 @@ public struct UniversalClient: Sendable { next = { try await middleware.intercept( $0, - baseURL: $1, + body: $1, + baseURL: $2, operationID: operationID, next: tmp ) } } - return try await next(request, baseURL) + return try await next(request, requestBody, baseURL) } mapError: { error in makeError(request: request, baseURL: baseURL, error: error) } return try await wrappingErrors { - try deserializer(response) + try await deserializer(response, responseBody) } mapError: { error in makeError(request: request, baseURL: baseURL, response: response, error: error) } diff --git a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift index e6c862b6..53cd613a 100644 --- a/Sources/OpenAPIRuntime/Interface/UniversalServer.swift +++ b/Sources/OpenAPIRuntime/Interface/UniversalServer.swift @@ -11,8 +11,12 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// + +import HTTPTypes + #if canImport(Darwin) -import Foundation +import struct Foundation.URL +import struct Foundation.URLComponents #else @preconcurrency import struct Foundation.URL @preconcurrency import struct Foundation.URLComponents @@ -23,20 +27,19 @@ import Foundation /// invocation, and response serialization. /// /// Do not call this directly, only invoked by generated code. -@_spi(Generated) -public struct UniversalServer: Sendable { +@_spi(Generated) public struct UniversalServer: Sendable { /// The URL of the server, used to determine the path prefix for /// registered request handlers. public var serverURL: URL - /// Helper for configuration driven data conversion + /// A converter for encoding/decoding data. public var converter: Converter - /// Implements the handler for each HTTP operation. + /// A type capable of handling HTTP requests and returning HTTP responses. public var handler: APIHandler - /// Middlewares to be invoked before `api` handles the request. + /// The middlewares to be invoked before the handler receives the request. public var middlewares: [any ServerMiddleware] /// Internal initializer that takes an initialized converter. @@ -78,21 +81,26 @@ public struct UniversalServer: Sendable { /// /// It wraps any thrown errors and attaching appropriate context. /// - Parameters: - /// - request: HTTP request. - /// - metadata: HTTP request metadata. + /// - request: The HTTP request. + /// - requestBody: The HTTP request body. + /// - metadata: The HTTP request metadata. /// - operationID: The OpenAPI operation identifier. - /// - handlerMethod: User handler method. - /// - deserializer: Creates an Input value from the provided HTTP request. - /// - serializer: Creates an HTTP response from the provided Output value. - /// - Returns: The HTTP response produced by `serializer`. + /// - handlerMethod: The user handler method. + /// - deserializer: A closure that creates an Input value from the + /// provided HTTP request. + /// - serializer: A closure that creates an HTTP response from the + /// provided Output value. + /// - Returns: The HTTP response and its body produced by the serializer. public func handle( - request: Request, - with metadata: ServerRequestMetadata, + request: HTTPRequest, + requestBody: HTTPBody?, + metadata: ServerRequestMetadata, forOperation operationID: String, using handlerMethod: @Sendable @escaping (APIHandler) -> ((OperationInput) async throws -> OperationOutput), - deserializer: @Sendable @escaping (Request, ServerRequestMetadata) throws -> OperationInput, - serializer: @Sendable @escaping (OperationOutput, Request) throws -> Response - ) async throws -> Response where OperationInput: Sendable, OperationOutput: Sendable { + deserializer: @Sendable @escaping (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> + OperationInput, + serializer: @Sendable @escaping (OperationOutput, HTTPRequest) throws -> (HTTPResponse, HTTPBody?) + ) async throws -> (HTTPResponse, HTTPBody?) where OperationInput: Sendable, OperationOutput: Sendable { @Sendable func wrappingErrors( work: () async throws -> R, @@ -113,62 +121,70 @@ public struct UniversalServer: Sendable { ServerError( operationID: operationID, request: request, + requestBody: requestBody, requestMetadata: metadata, operationInput: input, operationOutput: output, underlyingError: error ) } - var next: @Sendable (Request, ServerRequestMetadata) async throws -> Response = { _request, _metadata in - let input: OperationInput = try await wrappingErrors { - try deserializer(_request, _metadata) - } mapError: { error in - makeError(error: error) - } - let output: OperationOutput = try await wrappingErrors { - let method = handlerMethod(handler) + var next: @Sendable (HTTPRequest, HTTPBody?, ServerRequestMetadata) async throws -> (HTTPResponse, HTTPBody?) = + { + _request, + _requestBody, + _metadata in + let input: OperationInput = try await wrappingErrors { + try await deserializer(_request, _requestBody, _metadata) + } mapError: { error in + makeError(error: error) + } + let output: OperationOutput = try await wrappingErrors { + let method = handlerMethod(handler) + return try await wrappingErrors { + try await method(input) + } mapError: { error in + RuntimeError.handlerFailed(error) + } + } mapError: { error in + makeError(input: input, error: error) + } return try await wrappingErrors { - try await method(input) + try serializer(output, _request) } mapError: { error in - RuntimeError.handlerFailed(error) + makeError(input: input, output: output, error: error) } - } mapError: { error in - makeError(input: input, error: error) - } - return try await wrappingErrors { - try serializer(output, _request) - } mapError: { error in - makeError(input: input, output: output, error: error) } - } for middleware in middlewares.reversed() { let tmp = next next = { try await middleware.intercept( $0, - metadata: $1, + body: $1, + metadata: $2, operationID: operationID, next: tmp ) } } - return try await next(request, metadata) + return try await next(request, requestBody, metadata) } - /// Prepends the path components for the selected remote server URL. + /// Returns the path with the server URL's path prefix prepended. + /// - Parameter path: The path suffix. + /// - Returns: The path appended to the server URL's path. public func apiPathComponentsWithServerPrefix( - _ path: [RouterPathComponent] - ) throws -> [RouterPathComponent] { - // Operation path is for example [pets, 42] + _ 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] + // Goal is to return something like "/foo/bar/v1/pets/42". guard let components = URLComponents(url: serverURL, resolvingAgainstBaseURL: false) else { throw RuntimeError.invalidServerURL(serverURL.absoluteString) } - let prefixComponents = components.path - .split(separator: "/") - .filter { !$0.isEmpty } - .map { RouterPathComponent.constant(String($0)) } - return prefixComponents + path + let prefixPath = components.path + guard prefixPath == "/" else { + return prefixPath + path + } + return path } } diff --git a/Sources/OpenAPIRuntime/StringCoder/StringDecoder.swift b/Sources/OpenAPIRuntime/StringCoder/StringDecoder.swift deleted file mode 100644 index 22add7bf..00000000 --- a/Sources/OpenAPIRuntime/StringCoder/StringDecoder.swift +++ /dev/null @@ -1,448 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftOpenAPIGenerator open source project -// -// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Foundation - -/// A type that decodes a `Decodable` objects from a string -/// using `LosslessStringConvertible`. -struct StringDecoder: Sendable { - - /// The coder used to serialize Date values. - let dateTranscoder: any DateTranscoder -} - -extension StringDecoder { - - /// Attempt to decode an object from a string. - /// - Parameters: - /// - type: The type to decode. - /// - data: The encoded string. - /// - Returns: The decoded value. - func decode( - _ type: T.Type = T.self, - from data: String - ) throws -> T { - let decoder = LosslessStringConvertibleDecoder( - dateTranscoder: dateTranscoder, - encodedString: data - ) - // We have to catch the special values early, otherwise we fall - // back to their Codable implementations, which don't give us - // a chance to customize the coding in the containers. - let value: T - switch type { - case is Date.Type: - value = try decoder.singleValueContainer().decode(Date.self) as! T - default: - value = try T.init(from: decoder) - } - return value - } -} - -/// The decoder used by `StringDecoder`. -private struct LosslessStringConvertibleDecoder { - - /// The coder used to serialize Date values. - let dateTranscoder: any DateTranscoder - - /// The underlying encoded string. - let encodedString: String -} - -extension LosslessStringConvertibleDecoder { - - /// A decoder error. - enum DecoderError: Swift.Error { - - /// The `LosslessStringConvertible` initializer returned nil for the - /// provided raw string. - case failedToDecodeValue - - /// The decoder tried to decode a nested container, which are not - /// supported. - case containersNotSupported - } -} - -extension LosslessStringConvertibleDecoder: Decoder { - - var codingPath: [any CodingKey] { - [] - } - - var userInfo: [CodingUserInfoKey: Any] { - [:] - } - - func container( - keyedBy type: Key.Type - ) throws -> KeyedDecodingContainer where Key: CodingKey { - KeyedDecodingContainer(KeyedContainer(decoder: self)) - } - - func unkeyedContainer() throws -> any UnkeyedDecodingContainer { - UnkeyedContainer(decoder: self) - } - - func singleValueContainer() throws -> any SingleValueDecodingContainer { - SingleValueContainer(decoder: self) - } -} - -extension LosslessStringConvertibleDecoder { - - /// A single value container used by `LosslessStringConvertibleDecoder`. - struct SingleValueContainer { - - /// The underlying decoder. - let decoder: LosslessStringConvertibleDecoder - - /// Decodes a value of type conforming to `LosslessStringConvertible`. - /// - Returns: The decoded value. - private func _decodeLosslessStringConvertible( - _: T.Type = T.self - ) throws -> T { - guard let parsedValue = T(String(decoder.encodedString)) else { - throw DecodingError.typeMismatch( - T.self, - .init( - codingPath: codingPath, - debugDescription: "Failed to convert to the requested type." - ) - ) - } - return parsedValue - } - } - - /// An unkeyed container used by `LosslessStringConvertibleDecoder`. - struct UnkeyedContainer { - - /// The underlying decoder. - let decoder: LosslessStringConvertibleDecoder - } - - /// A keyed container used by `LosslessStringConvertibleDecoder`. - struct KeyedContainer { - - /// The underlying decoder. - let decoder: LosslessStringConvertibleDecoder - } -} - -extension LosslessStringConvertibleDecoder.SingleValueContainer: SingleValueDecodingContainer { - - var codingPath: [any CodingKey] { - [] - } - - func decodeNil() -> Bool { - false - } - - func decode(_ type: Bool.Type) throws -> Bool { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: String.Type) throws -> String { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: Double.Type) throws -> Double { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: Float.Type) throws -> Float { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: Int.Type) throws -> Int { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: Int8.Type) throws -> Int8 { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: Int16.Type) throws -> Int16 { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: Int32.Type) throws -> Int32 { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: Int64.Type) throws -> Int64 { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: UInt.Type) throws -> UInt { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: UInt8.Type) throws -> UInt8 { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: UInt16.Type) throws -> UInt16 { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: UInt32.Type) throws -> UInt32 { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: UInt64.Type) throws -> UInt64 { - try _decodeLosslessStringConvertible() - } - - func decode(_ type: T.Type) throws -> T where T: Decodable { - switch type { - case is Bool.Type: - return try decode(Bool.self) as! T - case is String.Type: - return try decode(String.self) as! T - case is Double.Type: - return try decode(Double.self) as! T - case is Float.Type: - return try decode(Float.self) as! T - case is Int.Type: - return try decode(Int.self) as! T - case is Int8.Type: - return try decode(Int8.self) as! T - case is Int16.Type: - return try decode(Int16.self) as! T - case is Int32.Type: - return try decode(Int32.self) as! T - case is Int64.Type: - return try decode(Int64.self) as! T - case is UInt.Type: - return try decode(UInt.self) as! T - case is UInt8.Type: - return try decode(UInt8.self) as! T - case is UInt16.Type: - return try decode(UInt16.self) as! T - case is UInt32.Type: - return try decode(UInt32.self) as! T - case is UInt64.Type: - return try decode(UInt64.self) as! T - case is Date.Type: - return try decoder - .dateTranscoder - .decode(String(decoder.encodedString)) as! T - default: - guard let convertileType = T.self as? any LosslessStringConvertible.Type else { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - return try _decodeLosslessStringConvertible(convertileType) as! T - } - } -} - -extension LosslessStringConvertibleDecoder.UnkeyedContainer: UnkeyedDecodingContainer { - - var codingPath: [any CodingKey] { - [] - } - - var count: Int? { - nil - } - - var isAtEnd: Bool { - true - } - - var currentIndex: Int { - 0 - } - - mutating func decodeNil() throws -> Bool { - false - } - - mutating func decode(_ type: Bool.Type) throws -> Bool { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: String.Type) throws -> String { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: Double.Type) throws -> Double { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: Float.Type) throws -> Float { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: Int.Type) throws -> Int { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: Int8.Type) throws -> Int8 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: Int16.Type) throws -> Int16 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: Int32.Type) throws -> Int32 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: Int64.Type) throws -> Int64 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: UInt.Type) throws -> UInt { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: UInt8.Type) throws -> UInt8 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: UInt16.Type) throws -> UInt16 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: UInt32.Type) throws -> UInt32 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: UInt64.Type) throws -> UInt64 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func decode(_ type: T.Type) throws -> T where T: Decodable { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func nestedContainer( - keyedBy type: NestedKey.Type - ) throws -> KeyedDecodingContainer where NestedKey: CodingKey { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func nestedUnkeyedContainer() throws -> any UnkeyedDecodingContainer { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - mutating func superDecoder() throws -> any Decoder { - decoder - } - -} - -extension LosslessStringConvertibleDecoder.KeyedContainer: KeyedDecodingContainerProtocol { - - var codingPath: [any CodingKey] { - [] - } - - var allKeys: [Key] { - [] - } - - func contains(_ key: Key) -> Bool { - false - } - - func decodeNil(forKey key: Key) throws -> Bool { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: String.Type, forKey key: Key) throws -> String { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: Double.Type, forKey key: Key) throws -> Double { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: Float.Type, forKey key: Key) throws -> Float { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: Int.Type, forKey key: Key) throws -> Int { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func decode(_ type: T.Type, forKey key: Key) throws -> T where T: Decodable { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func nestedContainer( - keyedBy type: NestedKey.Type, - forKey key: Key - ) throws -> KeyedDecodingContainer where NestedKey: CodingKey { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func nestedUnkeyedContainer(forKey key: Key) throws -> any UnkeyedDecodingContainer { - throw LosslessStringConvertibleDecoder.DecoderError.containersNotSupported - } - - func superDecoder() throws -> any Decoder { - decoder - } - - func superDecoder(forKey key: Key) throws -> any Decoder { - decoder - } -} diff --git a/Sources/OpenAPIRuntime/StringCoder/StringEncoder.swift b/Sources/OpenAPIRuntime/StringCoder/StringEncoder.swift deleted file mode 100644 index ba3fc943..00000000 --- a/Sources/OpenAPIRuntime/StringCoder/StringEncoder.swift +++ /dev/null @@ -1,446 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftOpenAPIGenerator open source project -// -// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Foundation - -/// A type that encodes an `Encodable` objects to a string, if it conforms -/// to `CustomStringConvertible`. -struct StringEncoder: Sendable { - - /// The coder used to serialize Date values. - let dateTranscoder: any DateTranscoder -} - -extension StringEncoder { - - /// Attempt to encode a value into a string using `CustomStringConvertible`. - /// - /// - Parameters: - /// - value: The value to encode. - /// - Returns: The encoded string. - func encode(_ value: some Encodable) throws -> String { - let encoder = CustomStringConvertibleEncoder( - dateTranscoder: dateTranscoder - ) - - // We have to catch the special values early, otherwise we fall - // back to their Codable implementations, which don't give us - // a chance to customize the coding in the containers. - if let date = value as? Date { - var container = encoder.singleValueContainer() - try container.encode(date) - } else { - try value.encode(to: encoder) - } - - return try encoder.nonNilEncodedString() - } -} - -/// The encoded used by `StringEncoder`. -private final class CustomStringConvertibleEncoder { - - /// The coder used to serialize Date values. - let dateTranscoder: any DateTranscoder - - /// The underlying encoded string. - /// - /// Nil before the encoder set the value. - private(set) var encodedString: String? - - /// Creates a new encoder. - /// - Parameter dateTranscoder: The coder used to serialize Date values. - init(dateTranscoder: any DateTranscoder) { - self.dateTranscoder = dateTranscoder - self.encodedString = nil - } -} - -extension CustomStringConvertibleEncoder { - - /// An encoder error. - enum EncoderError: Swift.Error { - - /// No value was set during the `encode(to:)` of the provided value. - case valueNotSet - - /// The encoder set a nil values, which is not supported. - case nilNotSupported - - /// The encoder encoded a container, which is not supported. - case containersNotSupported - - /// The encoder set a value multiple times, which is not supported. - case cannotEncodeMultipleValues - } - - /// Sets the provided value as the underlying string. - /// - Parameter value: The encoded string. - /// - Throws: An error if a value was already set previously. - func setEncodedString(_ value: String) throws { - guard encodedString == nil else { - throw EncoderError.cannotEncodeMultipleValues - } - encodedString = value - } - - /// Checks that the underlying string was set, and returns it. - /// - Returns: The underlying string. - /// - Throws: If the underlying string is nil. - func nonNilEncodedString() throws -> String { - guard let encodedString else { - throw EncoderError.valueNotSet - } - return encodedString - } -} - -extension CustomStringConvertibleEncoder: Encoder { - - var codingPath: [any CodingKey] { - [] - } - - var userInfo: [CodingUserInfoKey: Any] { - [:] - } - - func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key: CodingKey { - KeyedEncodingContainer(CustomStringConvertibleEncoder.KeyedContainer(encoder: self)) - } - - func unkeyedContainer() -> any UnkeyedEncodingContainer { - CustomStringConvertibleEncoder.UnkeyedContainer(encoder: self) - } - - func singleValueContainer() -> any SingleValueEncodingContainer { - SingleValueContainer(encoder: self) - } -} - -extension CustomStringConvertibleEncoder { - - /// A single value container used by `CustomStringConvertibleEncoder`. - struct SingleValueContainer { - - /// The underlying encoder. - let encoder: CustomStringConvertibleEncoder - - /// Converts the provided value to string and sets the result as the - /// underlying encoder's encoded value. - /// - Parameter value: The value to be encoded. - mutating func _encodeCustomStringConvertible(_ value: some CustomStringConvertible) throws { - try encoder.setEncodedString(value.description) - } - } - - /// An unkeyed container used by `CustomStringConvertibleEncoder`. - struct UnkeyedContainer { - - /// The underlying encoder. - let encoder: CustomStringConvertibleEncoder - } - - /// A keyed container used by `CustomStringConvertibleEncoder`. - struct KeyedContainer { - - /// The underlying encoder. - let encoder: CustomStringConvertibleEncoder - } -} - -extension CustomStringConvertibleEncoder.SingleValueContainer: SingleValueEncodingContainer { - - var codingPath: [any CodingKey] { - [] - } - - mutating func encodeNil() throws { - throw CustomStringConvertibleEncoder.EncoderError.nilNotSupported - } - - mutating func encode(_ value: Bool) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: String) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: Double) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: Float) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: Int) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: Int8) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: Int16) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: Int32) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: Int64) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: UInt) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: UInt8) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: UInt16) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: UInt32) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: UInt64) throws { - try _encodeCustomStringConvertible(value) - } - - mutating func encode(_ value: T) throws where T: Encodable { - switch value { - case let value as UInt8: - try encode(value) - case let value as Int8: - try encode(value) - case let value as UInt16: - try encode(value) - case let value as Int16: - try encode(value) - case let value as UInt32: - try encode(value) - case let value as Int32: - try encode(value) - case let value as UInt64: - try encode(value) - case let value as Int64: - try encode(value) - case let value as Int: - try encode(value) - case let value as UInt: - try encode(value) - case let value as Float: - try encode(value) - case let value as Double: - try encode(value) - case let value as String: - try encode(value) - case let value as Bool: - try encode(value) - case let value as Date: - try _encodeCustomStringConvertible(encoder.dateTranscoder.encode(value)) - default: - guard let customStringConvertible = value as? any CustomStringConvertible else { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - try _encodeCustomStringConvertible(customStringConvertible) - } - } -} - -extension CustomStringConvertibleEncoder.UnkeyedContainer: UnkeyedEncodingContainer { - - var codingPath: [any CodingKey] { - [] - } - - var count: Int { - 0 - } - - mutating func encodeNil() throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Bool) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: String) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Double) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Float) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int8) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int16) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int32) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int64) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt8) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt16) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt32) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt64) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: T) throws where T: Encodable { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer - where NestedKey: CodingKey { - encoder.container(keyedBy: NestedKey.self) - } - - mutating func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { - encoder.unkeyedContainer() - } - - mutating func superEncoder() -> any Encoder { - encoder - } -} - -extension CustomStringConvertibleEncoder.KeyedContainer: KeyedEncodingContainerProtocol { - - var codingPath: [any CodingKey] { - [] - } - - mutating func superEncoder() -> any Encoder { - encoder - } - - mutating func encodeNil(forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Bool, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: String, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Double, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Float, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int8, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int16, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int32, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: Int64, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt8, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt16, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt32, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: UInt64, forKey key: Key) throws { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func encode(_ value: T, forKey key: Key) throws where T: Encodable { - throw CustomStringConvertibleEncoder.EncoderError.containersNotSupported - } - - mutating func nestedContainer( - keyedBy keyType: NestedKey.Type, - forKey key: Key - ) -> KeyedEncodingContainer where NestedKey: CodingKey { - encoder.container(keyedBy: NestedKey.self) - } - - mutating func nestedUnkeyedContainer(forKey key: Key) -> any UnkeyedEncodingContainer { - encoder.unkeyedContainer() - } - - mutating func superEncoder(forKey key: Key) -> any Encoder { - encoder - } -} diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift index 84b3cbfb..c6b09dcb 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIDecoder.swift @@ -75,7 +75,7 @@ extension URIDecoder { func decode( _ type: T.Type = T.self, forKey key: String = "", - from data: String + from data: Substring ) throws -> T { try withCachedParser(from: data) { decoder in try decoder.decode(type, forKey: key) @@ -97,7 +97,7 @@ extension URIDecoder { func decodeIfPresent( _ type: T.Type = T.self, forKey key: String = "", - from data: String + from data: Substring ) throws -> T? { try withCachedParser(from: data) { decoder in try decoder.decodeIfPresent(type, forKey: key) @@ -113,7 +113,7 @@ extension URIDecoder { /// the `decode` method on `URICachedDecoder`. /// - Returns: The result of the closure invocation. func withCachedParser( - from data: String, + from data: Substring, calls: (URICachedDecoder) throws -> R ) throws -> R { var parser = URIParser(configuration: configuration, data: data) diff --git a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift index abb55d7a..5f0d78be 100644 --- a/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift +++ b/Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Unkeyed.swift @@ -54,7 +54,7 @@ extension URIUnkeyedDecodingContainer { return try work() } - /// Returns the the current item in the underlying array and increments + /// Returns the current item in the underlying array and increments /// the index. /// - Returns: The next value found. /// - Throws: An error if the container ran out of items. diff --git a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift index 795acc6e..8953da91 100644 --- a/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift +++ b/Sources/OpenAPIRuntime/URICoder/Parsing/URIParser.swift @@ -29,7 +29,7 @@ struct URIParser: Sendable { /// - configuration: The configuration instructing the parser how /// to interpret the raw string. /// - data: The string to parse. - init(configuration: URICoderConfiguration, data: String) { + init(configuration: URICoderConfiguration, data: Substring) { self.configuration = configuration self.data = data[...] } diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift index ff7bbb16..f86961ad 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -13,11 +13,12 @@ //===----------------------------------------------------------------------===// import XCTest @_spi(Generated) import OpenAPIRuntime +import HTTPTypes final class Test_ClientConverterExtensions: Test_Runtime { func test_setAcceptHeader() throws { - var headerFields: [HeaderField] = [] + var headerFields: HTTPFields = [:] converter.setAcceptHeader( in: &headerFields, contentTypes: [.init(contentType: TestAcceptable.json, quality: 0.8)] @@ -25,7 +26,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { XCTAssertEqual( headerFields, [ - .init(name: "accept", value: "application/json; q=0.800") + .accept: "application/json; q=0.800" ] ) } @@ -55,7 +56,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { name: "search", value: "foo" ) - XCTAssertEqual(request.query, "search=foo") + XCTAssertEqual(request.soar_query, "search=foo") } func test_setQueryItemAsURI_stringConvertible_needsEncoding() throws { @@ -67,7 +68,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { name: "search", value: "h%llo" ) - XCTAssertEqual(request.query, "search=h%25llo") + XCTAssertEqual(request.soar_query, "search=h%25llo") } func test_setQueryItemAsURI_arrayOfStrings() throws { @@ -79,7 +80,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { name: "search", value: ["foo", "bar"] ) - XCTAssertEqual(request.query, "search=foo&search=bar") + XCTAssertEqual(request.soar_query, "search=foo&search=bar") } func test_setQueryItemAsURI_arrayOfStrings_unexploded() throws { @@ -91,7 +92,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { name: "search", value: ["foo", "bar"] ) - XCTAssertEqual(request.query, "search=foo,bar") + XCTAssertEqual(request.soar_query, "search=foo,bar") } func test_setQueryItemAsURI_date() throws { @@ -103,7 +104,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { name: "search", value: testDate ) - XCTAssertEqual(request.query, "search=2023-01-18T10%3A04%3A11Z") + XCTAssertEqual(request.soar_query, "search=2023-01-18T10%3A04%3A11Z") } func test_setQueryItemAsURI_arrayOfDates() throws { @@ -115,128 +116,62 @@ final class Test_ClientConverterExtensions: Test_Runtime { name: "search", value: [testDate, testDate] ) - XCTAssertEqual(request.query, "search=2023-01-18T10%3A04%3A11Z&search=2023-01-18T10%3A04%3A11Z") - } - - // | client | set | request body | string | optional | setOptionalRequestBodyAsString | - func test_setOptionalRequestBodyAsString_string() throws { - var headerFields: [HeaderField] = [] - let body = try converter.setOptionalRequestBodyAsString( - testString, - headerFields: &headerFields, - contentType: "text/plain" - ) - XCTAssertEqual(body, testStringData) - XCTAssertEqual( - headerFields, - [ - .init(name: "content-type", value: "text/plain") - ] - ) - } - - // | client | set | request body | string | required | setRequiredRequestBodyAsString | - func test_setRequiredRequestBodyAsString_string() throws { - var headerFields: [HeaderField] = [] - let body = try converter.setRequiredRequestBodyAsString( - testString, - headerFields: &headerFields, - contentType: "text/plain" - ) - XCTAssertEqual(body, testStringData) - XCTAssertEqual( - headerFields, - [ - .init(name: "content-type", value: "text/plain") - ] - ) - } - - func test_setOptionalRequestBodyAsString_date() throws { - var headerFields: [HeaderField] = [] - let body = try converter.setOptionalRequestBodyAsString( - testDate, - headerFields: &headerFields, - contentType: "text/plain" - ) - XCTAssertEqual(body, testDateStringData) - XCTAssertEqual( - headerFields, - [ - .init(name: "content-type", value: "text/plain") - ] - ) - } - - func test_setRequiredRequestBodyAsString_date() throws { - var headerFields: [HeaderField] = [] - let body = try converter.setRequiredRequestBodyAsString( - testDate, - headerFields: &headerFields, - contentType: "text/plain" - ) - XCTAssertEqual(body, testDateStringData) - XCTAssertEqual( - headerFields, - [ - .init(name: "content-type", value: "text/plain") - ] - ) + XCTAssertEqual(request.soar_query, "search=2023-01-18T10%3A04%3A11Z&search=2023-01-18T10%3A04%3A11Z") } // | client | set | request body | JSON | optional | setOptionalRequestBodyAsJSON | - func test_setOptionalRequestBodyAsJSON_codable() throws { - var headerFields: [HeaderField] = [] + func test_setOptionalRequestBodyAsJSON_codable() async throws { + var headerFields: HTTPFields = [:] let body = try converter.setOptionalRequestBodyAsJSON( testStruct, headerFields: &headerFields, contentType: "application/json" ) - XCTAssertEqual(body, testStructPrettyData) + try await XCTAssertEqualStringifiedData(body, testStructPrettyString) XCTAssertEqual( headerFields, [ - .init(name: "content-type", value: "application/json") + .contentType: "application/json" ] ) } - func test_setOptionalRequestBodyAsJSON_codable_string() throws { - var headerFields: [HeaderField] = [] + func test_setOptionalRequestBodyAsJSON_codable_string() async throws { + var headerFields: HTTPFields = [:] let body = try converter.setOptionalRequestBodyAsJSON( testString, headerFields: &headerFields, contentType: "application/json" ) - XCTAssertEqual(body, testQuotedStringData) + try await XCTAssertEqualStringifiedData(body, testQuotedString) XCTAssertEqual( headerFields, [ - .init(name: "content-type", value: "application/json") + .contentType: "application/json" ] ) } // | client | set | request body | JSON | required | setRequiredRequestBodyAsJSON | - func test_setRequiredRequestBodyAsJSON_codable() throws { - var headerFields: [HeaderField] = [] + func test_setRequiredRequestBodyAsJSON_codable() async throws { + var headerFields: HTTPFields = [:] let body = try converter.setRequiredRequestBodyAsJSON( testStruct, headerFields: &headerFields, contentType: "application/json" ) - XCTAssertEqual(body, testStructPrettyData) + try await XCTAssertEqualStringifiedData(body, testStructPrettyString) XCTAssertEqual( headerFields, [ - .init(name: "content-type", value: "application/json") + .contentType: "application/json" ] ) } // | client | set | request body | urlEncodedForm | codable | optional | setRequiredRequestBodyAsURLEncodedForm | - func test_setOptionalRequestBodyAsURLEncodedForm_codable() throws { - var headerFields: [HeaderField] = [] + func test_setOptionalRequestBodyAsURLEncodedForm_codable() async throws { + var headerFields: HTTPFields = [:] let body = try converter.setOptionalRequestBodyAsURLEncodedForm( testStructDetailed, headerFields: &headerFields, @@ -248,104 +183,84 @@ final class Test_ClientConverterExtensions: Test_Runtime { return } - XCTAssertEqualStringifiedData(body, testStructURLFormString) + try await XCTAssertEqualStringifiedData(body, testStructURLFormString) XCTAssertEqual( headerFields, [ - .init(name: "content-type", value: "application/x-www-form-urlencoded") + .contentType: "application/x-www-form-urlencoded" ] ) } // | client | set | request body | urlEncodedForm | codable | required | setRequiredRequestBodyAsURLEncodedForm | - func test_setRequiredRequestBodyAsURLEncodedForm_codable() throws { - var headerFields: [HeaderField] = [] + func test_setRequiredRequestBodyAsURLEncodedForm_codable() async throws { + var headerFields: HTTPFields = [:] let body = try converter.setRequiredRequestBodyAsURLEncodedForm( testStructDetailed, headerFields: &headerFields, contentType: "application/x-www-form-urlencoded" ) - XCTAssertEqualStringifiedData(body, testStructURLFormString) + try await XCTAssertEqualStringifiedData(body, testStructURLFormString) XCTAssertEqual( headerFields, [ - .init(name: "content-type", value: "application/x-www-form-urlencoded") + .contentType: "application/x-www-form-urlencoded" ] ) } // | client | set | request body | binary | optional | setOptionalRequestBodyAsBinary | - func test_setOptionalRequestBodyAsBinary_data() throws { - var headerFields: [HeaderField] = [] + func test_setOptionalRequestBodyAsBinary_data() async throws { + var headerFields: HTTPFields = [:] let body = try converter.setOptionalRequestBodyAsBinary( - testStringData, + .init(testStringData), headerFields: &headerFields, contentType: "application/octet-stream" ) - XCTAssertEqual(body, testStringData) + try await XCTAssertEqualStringifiedData(body, testString) XCTAssertEqual( headerFields, [ - .init(name: "content-type", value: "application/octet-stream") + .contentType: "application/octet-stream" ] ) } // | client | set | request body | binary | required | setRequiredRequestBodyAsBinary | - func test_setRequiredRequestBodyAsBinary_data() throws { - var headerFields: [HeaderField] = [] + func test_setRequiredRequestBodyAsBinary_data() async throws { + var headerFields: HTTPFields = [:] let body = try converter.setRequiredRequestBodyAsBinary( - testStringData, + .init(testString), headerFields: &headerFields, contentType: "application/octet-stream" ) - XCTAssertEqual(body, testStringData) + try await XCTAssertEqualStringifiedData(body, testString) XCTAssertEqual( headerFields, [ - .init(name: "content-type", value: "application/octet-stream") + .contentType: "application/octet-stream" ] ) } - // | client | get | response body | string | required | getResponseBodyAsString | - func test_getResponseBodyAsString_stringConvertible() throws { - let value: String = try converter.getResponseBodyAsString( - String.self, - from: testStringData, - transforming: { $0 } - ) - XCTAssertEqual(value, testString) - } - - // | client | get | response body | string | required | getResponseBodyAsString | - func test_getResponseBodyAsString_date() throws { - let value: Date = try converter.getResponseBodyAsString( - Date.self, - from: testDateStringData, - transforming: { $0 } - ) - XCTAssertEqual(value, testDate) - } - // | client | get | response body | JSON | required | getResponseBodyAsJSON | - func test_getResponseBodyAsJSON_codable() throws { - let value: TestPet = try converter.getResponseBodyAsJSON( + func test_getResponseBodyAsJSON_codable() async throws { + let value: TestPet = try await converter.getResponseBodyAsJSON( TestPet.self, - from: testStructData, + from: .init(testStructData), transforming: { $0 } ) XCTAssertEqual(value, testStruct) } // | client | get | response body | binary | required | getResponseBodyAsBinary | - func test_getResponseBodyAsBinary_data() throws { - let value: Data = try converter.getResponseBodyAsBinary( - Data.self, - from: testStringData, + func test_getResponseBodyAsBinary_data() async throws { + let value: HTTPBody = try converter.getResponseBodyAsBinary( + HTTPBody.self, + from: .init(testString), transforming: { $0 } ) - XCTAssertEqual(value, testStringData) + try await XCTAssertEqualStringifiedData(value, testString) } } diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift index 9e9b328f..cb5bd055 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift @@ -13,6 +13,13 @@ //===----------------------------------------------------------------------===// import XCTest @_spi(Generated) import OpenAPIRuntime +import HTTPTypes + +extension HTTPField.Name { + static var foo: Self { + Self("foo")! + } +} final class Test_CommonConverterExtensions: Test_Runtime { @@ -51,7 +58,7 @@ final class Test_CommonConverterExtensions: Test_Runtime { // | common | set | header field | URI | both | setHeaderFieldAsURI | func test_setHeaderFieldAsURI_string() throws { - var headerFields: [HeaderField] = [] + var headerFields: HTTPFields = [:] try converter.setHeaderFieldAsURI( in: &headerFields, name: "foo", @@ -60,13 +67,13 @@ final class Test_CommonConverterExtensions: Test_Runtime { XCTAssertEqual( headerFields, [ - .init(name: "foo", value: "bar") + .foo: "bar" ] ) } func test_setHeaderFieldAsURI_arrayOfStrings() throws { - var headerFields: [HeaderField] = [] + var headerFields: HTTPFields = [:] try converter.setHeaderFieldAsURI( in: &headerFields, name: "foo", @@ -75,13 +82,13 @@ final class Test_CommonConverterExtensions: Test_Runtime { XCTAssertEqual( headerFields, [ - .init(name: "foo", value: "bar,baz") + .foo: "bar,baz" ] ) } func test_setHeaderFieldAsURI_date() throws { - var headerFields: [HeaderField] = [] + var headerFields: HTTPFields = [:] try converter.setHeaderFieldAsURI( in: &headerFields, name: "foo", @@ -90,13 +97,13 @@ final class Test_CommonConverterExtensions: Test_Runtime { XCTAssertEqual( headerFields, [ - .init(name: "foo", value: testDateEscapedString) + .foo: testDateEscapedString ] ) } func test_setHeaderFieldAsURI_arrayOfDates() throws { - var headerFields: [HeaderField] = [] + var headerFields: HTTPFields = [:] try converter.setHeaderFieldAsURI( in: &headerFields, name: "foo", @@ -105,13 +112,13 @@ final class Test_CommonConverterExtensions: Test_Runtime { XCTAssertEqual( headerFields, [ - .init(name: "foo", value: "\(testDateEscapedString),\(testDateEscapedString)") + .foo: "\(testDateEscapedString),\(testDateEscapedString)" ] ) } func test_setHeaderFieldAsURI_struct() throws { - var headerFields: [HeaderField] = [] + var headerFields: HTTPFields = [:] try converter.setHeaderFieldAsURI( in: &headerFields, name: "foo", @@ -120,14 +127,14 @@ final class Test_CommonConverterExtensions: Test_Runtime { XCTAssertEqual( headerFields, [ - .init(name: "foo", value: "name,Fluffz") + .foo: "name,Fluffz" ] ) } // | common | set | header field | JSON | both | setHeaderFieldAsJSON | func test_setHeaderFieldAsJSON_codable() throws { - var headerFields: [HeaderField] = [] + var headerFields: HTTPFields = [:] try converter.setHeaderFieldAsJSON( in: &headerFields, name: "foo", @@ -136,13 +143,13 @@ final class Test_CommonConverterExtensions: Test_Runtime { XCTAssertEqual( headerFields, [ - .init(name: "foo", value: testStructString) + .foo: testStructString ] ) } func test_setHeaderFieldAsJSON_codable_string() throws { - var headerFields: [HeaderField] = [] + var headerFields: HTTPFields = [:] try converter.setHeaderFieldAsJSON( in: &headerFields, name: "foo", @@ -151,15 +158,15 @@ final class Test_CommonConverterExtensions: Test_Runtime { XCTAssertEqual( headerFields, [ - .init(name: "foo", value: "\"hello\"") + .foo: "\"hello\"" ] ) } // | common | get | header field | URI | optional | getOptionalHeaderFieldAsURI | func test_getOptionalHeaderFieldAsURI_string() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: "bar") + let headerFields: HTTPFields = [ + .foo: "bar" ] let value: String? = try converter.getOptionalHeaderFieldAsURI( in: headerFields, @@ -171,8 +178,8 @@ final class Test_CommonConverterExtensions: Test_Runtime { // | common | get | header field | URI | required | getRequiredHeaderFieldAsURI | func test_getRequiredHeaderFieldAsURI_stringConvertible() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: "bar") + let headerFields: HTTPFields = [ + .foo: "bar" ] let value: String = try converter.getRequiredHeaderFieldAsURI( in: headerFields, @@ -182,22 +189,9 @@ final class Test_CommonConverterExtensions: Test_Runtime { XCTAssertEqual(value, "bar") } - func test_getOptionalHeaderFieldAsURI_arrayOfStrings_splitHeaders() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: "bar"), - .init(name: "foo", value: "baz"), - ] - let value: [String]? = try converter.getOptionalHeaderFieldAsURI( - in: headerFields, - name: "foo", - as: [String].self - ) - XCTAssertEqual(value, ["bar", "baz"]) - } - func test_getOptionalHeaderFieldAsURI_arrayOfStrings_singleHeader() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: "bar,baz") + let headerFields: HTTPFields = [ + .foo: "bar,baz" ] let value: [String]? = try converter.getOptionalHeaderFieldAsURI( in: headerFields, @@ -208,8 +202,8 @@ final class Test_CommonConverterExtensions: Test_Runtime { } func test_getOptionalHeaderFieldAsURI_date() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: testDateEscapedString) + let headerFields: HTTPFields = [ + .foo: testDateEscapedString ] let value: Date? = try converter.getOptionalHeaderFieldAsURI( in: headerFields, @@ -220,9 +214,8 @@ final class Test_CommonConverterExtensions: Test_Runtime { } func test_getRequiredHeaderFieldAsURI_arrayOfDates() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: testDateString), // escaped - .init(name: "foo", value: testDateEscapedString), // unescaped + let headerFields: HTTPFields = [ + .foo: "\(testDateString),\(testDateEscapedString)" // escaped and unescaped ] let value: [Date] = try converter.getRequiredHeaderFieldAsURI( in: headerFields, @@ -233,8 +226,8 @@ final class Test_CommonConverterExtensions: Test_Runtime { } func test_getOptionalHeaderFieldAsURI_struct() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: "name,Sprinkles") + let headerFields: HTTPFields = [ + .foo: "name,Sprinkles" ] let value: TestPet? = try converter.getOptionalHeaderFieldAsURI( in: headerFields, @@ -246,8 +239,8 @@ final class Test_CommonConverterExtensions: Test_Runtime { // | common | get | header field | JSON | optional | getOptionalHeaderFieldAsJSON | func test_getOptionalHeaderFieldAsJSON_codable() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: testStructString) + let headerFields: HTTPFields = [ + .foo: testStructString ] let value: TestPet? = try converter.getOptionalHeaderFieldAsJSON( in: headerFields, @@ -259,8 +252,8 @@ final class Test_CommonConverterExtensions: Test_Runtime { // | common | get | header field | JSON | required | getRequiredHeaderFieldAsJSON | func test_getRequiredHeaderFieldAsJSON_codable() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: testStructString) + let headerFields: HTTPFields = [ + .foo: testStructString ] let value: TestPet = try converter.getRequiredHeaderFieldAsJSON( in: headerFields, diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift index 45e30173..6617f60a 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift @@ -13,12 +13,13 @@ //===----------------------------------------------------------------------===// import XCTest @_spi(Generated) import OpenAPIRuntime +import HTTPTypes final class Test_ServerConverterExtensions: Test_Runtime { func testExtractAccept() throws { - let headerFields: [HeaderField] = [ - .init(name: "accept", value: "application/json, */*; q=0.8") + let headerFields: HTTPFields = [ + .accept: "application/json, */*; q=0.8" ] let accept: [AcceptHeaderContentType] = try converter.extractAcceptHeaderIfPresent( in: headerFields @@ -35,27 +36,23 @@ final class Test_ServerConverterExtensions: Test_Runtime { // MARK: Miscs func testValidateAccept() throws { - let emptyHeaders: [HeaderField] = [] - let wildcard: [HeaderField] = [ - .init(name: "accept", value: "*/*") + let emptyHeaders: HTTPFields = [:] + let wildcard: HTTPFields = [ + .accept: "*/*" ] - let partialWildcard: [HeaderField] = [ - .init(name: "accept", value: "text/*") + let partialWildcard: HTTPFields = [ + .accept: "text/*" ] - let short: [HeaderField] = [ - .init(name: "accept", value: "text/plain") + let short: HTTPFields = [ + .accept: "text/plain" ] - let long: [HeaderField] = [ - .init( - name: "accept", - value: "text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8" - ) + let long: HTTPFields = [ + .accept: "text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8" ] - let multiple: [HeaderField] = [ - .init(name: "accept", value: "text/plain"), - .init(name: "accept", value: "application/json"), + let multiple: HTTPFields = [ + .accept: "text/plain, application/json" ] - let cases: [([HeaderField], String, Bool)] = [ + let cases: [(HTTPFields, String, Bool)] = [ // No Accept header, any string validates successfully (emptyHeaders, "foobar", true), @@ -107,7 +104,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { // | server | get | request path | URI | required | getPathParameterAsURI | func test_getPathParameterAsURI_various() throws { - let path: [String: String] = [ + let path: [String: Substring] = [ "foo": "bar", "number": "1", "habitats": "land,air", @@ -208,7 +205,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { } func test_getOptionalQueryItemAsURI_arrayOfStrings() throws { - let query = "search=foo&search=bar" + let query: Substring = "search=foo&search=bar" let value: [String]? = try converter.getOptionalQueryItemAsURI( in: query, style: nil, @@ -220,7 +217,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { } func test_getRequiredQueryItemAsURI_arrayOfStrings() throws { - let query = "search=foo&search=bar" + let query: Substring = "search=foo&search=bar" let value: [String] = try converter.getRequiredQueryItemAsURI( in: query, style: nil, @@ -232,7 +229,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { } func test_getRequiredQueryItemAsURI_arrayOfStrings_unexploded() throws { - let query = "search=foo,bar" + let query: Substring = "search=foo,bar" let value: [String] = try converter.getRequiredQueryItemAsURI( in: query, style: nil, @@ -244,7 +241,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { } func test_getOptionalQueryItemAsURI_date() throws { - let query = "search=\(testDateEscapedString)" + let query: Substring = "search=\(testDateEscapedString)" let value: Date? = try converter.getOptionalQueryItemAsURI( in: query, style: nil, @@ -256,7 +253,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { } func test_getRequiredQueryItemAsURI_arrayOfDates() throws { - let query = "search=\(testDateEscapedString)&search=\(testDateEscapedString)" + let query: Substring = "search=\(testDateEscapedString)&search=\(testDateEscapedString)" let value: [Date] = try converter.getRequiredQueryItemAsURI( in: query, style: nil, @@ -267,168 +264,105 @@ final class Test_ServerConverterExtensions: Test_Runtime { XCTAssertEqual(value, [testDate, testDate]) } - // | server | get | request body | string | optional | getOptionalRequestBodyAsString | - func test_getOptionalRequestBodyAsText_string() throws { - let body: String? = try converter.getOptionalRequestBodyAsString( - String.self, - from: testStringData, - transforming: { $0 } - ) - XCTAssertEqual(body, testString) - } - - // | server | get | request body | string | required | getRequiredRequestBodyAsString | - func test_getRequiredRequestBodyAsText_stringConvertible() throws { - let body: String = try converter.getRequiredRequestBodyAsString( - String.self, - from: testStringData, - transforming: { $0 } - ) - XCTAssertEqual(body, testString) - } - - func test_getRequiredRequestBodyAsText_date() throws { - let body: Date = try converter.getRequiredRequestBodyAsString( - Date.self, - from: testDateStringData, - transforming: { $0 } - ) - XCTAssertEqual(body, testDate) - } - // | server | get | request body | JSON | optional | getOptionalRequestBodyAsJSON | - func test_getOptionalRequestBodyAsJSON_codable() throws { - let body: TestPet? = try converter.getOptionalRequestBodyAsJSON( + func test_getOptionalRequestBodyAsJSON_codable() async throws { + let body: TestPet? = try await converter.getOptionalRequestBodyAsJSON( TestPet.self, - from: testStructData, + from: .init(testStructData), transforming: { $0 } ) XCTAssertEqual(body, testStruct) } - func test_getOptionalRequestBodyAsJSON_codable_string() throws { - let body: String? = try converter.getOptionalRequestBodyAsJSON( + func test_getOptionalRequestBodyAsJSON_codable_string() async throws { + let body: String? = try await converter.getOptionalRequestBodyAsJSON( String.self, - from: testQuotedStringData, + from: .init(testQuotedStringData), transforming: { $0 } ) XCTAssertEqual(body, testString) } // | server | get | request body | JSON | required | getRequiredRequestBodyAsJSON | - func test_getRequiredRequestBodyAsJSON_codable() throws { - let body: TestPet = try converter.getRequiredRequestBodyAsJSON( + func test_getRequiredRequestBodyAsJSON_codable() async throws { + let body: TestPet = try await converter.getRequiredRequestBodyAsJSON( TestPet.self, - from: testStructData, + from: .init(testStructData), transforming: { $0 } ) XCTAssertEqual(body, testStruct) } // | server | get | request body | urlEncodedForm | optional | getOptionalRequestBodyAsURLEncodedForm | - func test_getOptionalRequestBodyAsURLEncodedForm_codable() throws { - let body: TestPetDetailed? = try converter.getOptionalRequestBodyAsURLEncodedForm( + func test_getOptionalRequestBodyAsURLEncodedForm_codable() async throws { + let body: TestPetDetailed? = try await converter.getOptionalRequestBodyAsURLEncodedForm( TestPetDetailed.self, - from: testStructURLFormData, + from: .init(testStructURLFormData), transforming: { $0 } ) XCTAssertEqual(body, testStructDetailed) } // | server | get | request body | urlEncodedForm | required | getRequiredRequestBodyAsURLEncodedForm | - func test_getRequiredRequestBodyAsURLEncodedForm_codable() throws { - let body: TestPetDetailed = try converter.getRequiredRequestBodyAsURLEncodedForm( + func test_getRequiredRequestBodyAsURLEncodedForm_codable() async throws { + let body: TestPetDetailed = try await converter.getRequiredRequestBodyAsURLEncodedForm( TestPetDetailed.self, - from: testStructURLFormData, + from: .init(testStructURLFormData), transforming: { $0 } ) XCTAssertEqual(body, testStructDetailed) } // | server | get | request body | binary | optional | getOptionalRequestBodyAsBinary | - func test_getOptionalRequestBodyAsBinary_data() throws { - let body: Data? = try converter.getOptionalRequestBodyAsBinary( - Data.self, - from: testStringData, + func test_getOptionalRequestBodyAsBinary_data() async throws { + let body: HTTPBody? = try converter.getOptionalRequestBodyAsBinary( + HTTPBody.self, + from: .init(testStringData), transforming: { $0 } ) - XCTAssertEqual(body, testStringData) + try await XCTAssertEqualStringifiedData(body, testString) } // | server | get | request body | binary | required | getRequiredRequestBodyAsBinary | - func test_getRequiredRequestBodyAsBinary_data() throws { - let body: Data = try converter.getRequiredRequestBodyAsBinary( - Data.self, - from: testStringData, + func test_getRequiredRequestBodyAsBinary_data() async throws { + let body: HTTPBody = try converter.getRequiredRequestBodyAsBinary( + HTTPBody.self, + from: .init(testStringData), transforming: { $0 } ) - XCTAssertEqual(body, testStringData) - } - - // | server | set | response body | string | required | setResponseBodyAsString | - func test_setResponseBodyAsText_stringConvertible() throws { - var headers: [HeaderField] = [] - let data = try converter.setResponseBodyAsString( - testString, - headerFields: &headers, - contentType: "text/plain" - ) - XCTAssertEqual(data, testStringData) - XCTAssertEqual( - headers, - [ - .init(name: "content-type", value: "text/plain") - ] - ) - } - - // | server | set | response body | string | required | setResponseBodyAsString | - func test_setResponseBodyAsText_date() throws { - var headers: [HeaderField] = [] - let data = try converter.setResponseBodyAsString( - testDate, - headerFields: &headers, - contentType: "text/plain" - ) - XCTAssertEqual(data, testDateStringData) - XCTAssertEqual( - headers, - [ - .init(name: "content-type", value: "text/plain") - ] - ) + try await XCTAssertEqualStringifiedData(body, testString) } // | server | set | response body | JSON | required | setResponseBodyAsJSON | - func test_setResponseBodyAsJSON_codable() throws { - var headers: [HeaderField] = [] + func test_setResponseBodyAsJSON_codable() async throws { + var headers: HTTPFields = [:] let data = try converter.setResponseBodyAsJSON( testStruct, headerFields: &headers, contentType: "application/json" ) - XCTAssertEqual(data, testStructPrettyData) + try await XCTAssertEqualStringifiedData(data, testStructPrettyString) XCTAssertEqual( headers, [ - .init(name: "content-type", value: "application/json") + .contentType: "application/json" ] ) } // | server | set | response body | binary | required | setResponseBodyAsBinary | - func test_setResponseBodyAsBinary_data() throws { - var headers: [HeaderField] = [] + func test_setResponseBodyAsBinary_data() async throws { + var headers: HTTPFields = [:] let data = try converter.setResponseBodyAsBinary( - testStringData, + .init(testStringData), headerFields: &headers, contentType: "application/octet-stream" ) - XCTAssertEqual(data, testStringData) + try await XCTAssertEqualStringifiedData(data, testString) XCTAssertEqual( headers, [ - .init(name: "content-type", value: "application/octet-stream") + .contentType: "application/octet-stream" ] ) } diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_CurrencyExtensions.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_CurrencyExtensions.swift deleted file mode 100644 index c49bb682..00000000 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_CurrencyExtensions.swift +++ /dev/null @@ -1,86 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftOpenAPIGenerator open source project -// -// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// -import XCTest -@testable import OpenAPIRuntime - -final class Test_CurrencyExtensions: Test_Runtime { - - func testHeaderFields_add_string() throws { - var headerFields: [HeaderField] = [] - headerFields.add(name: "foo", value: "bar") - XCTAssertEqual( - headerFields, - [ - .init(name: "foo", value: "bar") - ] - ) - } - - func testHeaderFields_add_nil() throws { - var headerFields: [HeaderField] = [] - let value: String? = nil - headerFields.add(name: "foo", value: value) - XCTAssertEqual(headerFields, []) - } - - func testHeaderFields_firstValue_found() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: "bar") - ] - XCTAssertEqual(headerFields.firstValue(name: "foo"), "bar") - } - - func testHeaderFields_firstValue_nil() throws { - let headerFields: [HeaderField] = [] - XCTAssertNil(headerFields.firstValue(name: "foo")) - } - - func testHeaderFields_values() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: "bar"), - .init(name: "foo", value: "baz"), - ] - XCTAssertEqual(headerFields.values(name: "foo"), ["bar", "baz"]) - } - - func testHeaderFields_removeAll_noMatches() throws { - var headerFields: [HeaderField] = [ - .init(name: "one", value: "one"), - .init(name: "two", value: "two"), - ] - headerFields.removeAll(named: "three") - XCTAssertEqual(headerFields.map(\.name), ["one", "two"]) - } - - func testHeaderFields_removeAll_oneMatch() throws { - var headerFields: [HeaderField] = [ - .init(name: "one", value: "one"), - .init(name: "two", value: "two"), - .init(name: "three", value: "three"), - ] - headerFields.removeAll(named: "three") - XCTAssertEqual(headerFields.map(\.name), ["one", "two"]) - } - - func testHeaderFields_removeAll_manyMatches() throws { - var headerFields: [HeaderField] = [ - .init(name: "one", value: "one"), - .init(name: "three", value: "3"), - .init(name: "two", value: "two"), - .init(name: "three", value: "three"), - ] - headerFields.removeAll(named: "three") - XCTAssertEqual(headerFields.map(\.name), ["one", "two"]) - } -} diff --git a/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift new file mode 100644 index 00000000..83fbe182 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Interface/Test_HTTPBody.swift @@ -0,0 +1,292 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +@_spi(Generated)@testable import OpenAPIRuntime +import Foundation + +final class Test_Body: Test_Runtime { + + func testCreateAndCollect() async throws { + + // A single string. + do { + let body: HTTPBody = HTTPBody("hello") + try await _testConsume( + body, + expected: "hello" + ) + } + + // A literal string. + do { + let body: HTTPBody = "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" + ) + } + + // A single array of bytes. + do { + let body: HTTPBody = HTTPBody([0]) + try await _testConsume( + body, + expected: [0] + ) + } + + // A literal array of bytes. + do { + let body: HTTPBody = [0] + try await _testConsume( + body, + expected: [0] + ) + } + + // A single data. + do { + let body: HTTPBody = HTTPBody(Data([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][...] + ) + } + + // An async throwing stream. + do { + let body: HTTPBody = HTTPBody( + AsyncThrowingStream( + String.self, + { continuation in + continuation.yield("hel") + continuation.yield("lo") + continuation.finish() + } + ), + length: .known(5) + ) + try await _testConsume( + body, + expected: "hello" + ) + } + + // An async throwing stream, unknown length. + do { + let body: HTTPBody = HTTPBody( + AsyncThrowingStream( + String.self, + { continuation in + continuation.yield("hel") + continuation.yield("lo") + continuation.finish() + } + ), + length: .unknown + ) + try await _testConsume( + body, + expected: "hello" + ) + } + + // An async stream. + do { + let body: HTTPBody = HTTPBody( + AsyncStream( + String.self, + { continuation in + continuation.yield("hel") + continuation.yield("lo") + continuation.finish() + } + ), + length: .known(5) + ) + try await _testConsume( + body, + expected: "hello" + ) + } + + // Another async sequence. + do { + let sequence = AsyncStream( + String.self, + { continuation in + continuation.yield("hel") + continuation.yield("lo") + continuation.finish() + } + ) + .map { $0 } + let body: HTTPBody = HTTPBody( + sequence, + length: .known(5), + iterationBehavior: .single + ) + try await _testConsume( + body, + expected: "hello" + ) + } + } + + func testChunksPreserved() async throws { + let sequence = AsyncStream( + String.self, + { continuation in + continuation.yield("hel") + continuation.yield("lo") + continuation.finish() + } + ) + .map { $0 } + let body: HTTPBody = HTTPBody( + sequence, + length: .known(5), + iterationBehavior: .single + ) + var chunks: [HTTPBody.ByteChunk] = [] + for try await chunk in body { + chunks.append(chunk) + } + XCTAssertEqual(chunks, ["hel", "lo"].map { Array($0.utf8)[...] }) + } + + func testIterationBehavior_single() async throws { + let sequence = AsyncStream( + String.self, + { continuation in + continuation.yield("hel") + continuation.yield("lo") + continuation.finish() + } + ) + .map { $0 } + let body: HTTPBody = HTTPBody( + sequence, + length: .unknown, + iterationBehavior: .single + ) + + XCTAssertFalse(body.testing_iteratorCreated) + + var chunkCount = 0 + for try await _ in body { + chunkCount += 1 + } + XCTAssertEqual(chunkCount, 2) + + XCTAssertTrue(body.testing_iteratorCreated) + + do { + _ = try await String(collecting: body, upTo: .max) + XCTFail("Expected an error to be thrown") + } catch {} + } + + func testIterationBehavior_multiple() async throws { + let body: HTTPBody = HTTPBody([104, 105]) + + XCTAssertFalse(body.testing_iteratorCreated) + + do { + var chunkCount = 0 + for try await _ in body { + chunkCount += 1 + } + XCTAssertEqual(chunkCount, 1) + } + + XCTAssertTrue(body.testing_iteratorCreated) + + do { + var chunkCount = 0 + for try await _ in body { + chunkCount += 1 + } + XCTAssertEqual(chunkCount, 1) + } + + XCTAssertTrue(body.testing_iteratorCreated) + } + + func testIterationBehavior_multiple_byteLimit() async throws { + let body: HTTPBody = HTTPBody([104, 105]) + + do { + _ = try await String(collecting: body, upTo: 0) + XCTFail("Expected an error to be thrown") + } catch {} + + do { + _ = try await String(collecting: body, upTo: 1) + XCTFail("Expected an error to be thrown") + } catch {} + + do { + let string = try await String(collecting: body, upTo: 2) + XCTAssertEqual(string, "hi") + } + + do { + let string = try await String(collecting: body, upTo: .max) + XCTAssertEqual(string, "hi") + } + } + +} + +extension Test_Body { + 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 { + let output = try await String(collecting: body, upTo: .max) + XCTAssertEqual(output, expected.description, file: file, line: line) + } +} diff --git a/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift b/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift index 18646db6..b3992464 100644 --- a/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift +++ b/Tests/OpenAPIRuntimeTests/Interface/Test_UniversalServer.swift @@ -22,10 +22,7 @@ final class Test_UniversalServer: Test_Runtime { let server = UniversalServer( handler: MockHandler() ) - let components: [RouterPathComponent] = [ - .constant("foo"), - .parameter("bar"), - ] + let components = "/foo/{bar}" let prefixed = try server.apiPathComponentsWithServerPrefix(components) // When no server path prefix, components stay the same XCTAssertEqual(prefixed, components) @@ -36,16 +33,9 @@ final class Test_UniversalServer: Test_Runtime { serverURL: try serverURL, handler: MockHandler() ) - let components: [RouterPathComponent] = [ - .constant("foo"), - .parameter("bar"), - ] + let components = "/foo/{bar}" let prefixed = try server.apiPathComponentsWithServerPrefix(components) - let expected: [RouterPathComponent] = [ - .constant("api"), - .constant("foo"), - .parameter("bar"), - ] + let expected = "/api/foo/{bar}" XCTAssertEqual(prefixed, expected) } } diff --git a/Tests/OpenAPIRuntimeTests/StringCoder/Test_StringCodingRoundtrip.swift b/Tests/OpenAPIRuntimeTests/StringCoder/Test_StringCodingRoundtrip.swift deleted file mode 100644 index f4ba5188..00000000 --- a/Tests/OpenAPIRuntimeTests/StringCoder/Test_StringCodingRoundtrip.swift +++ /dev/null @@ -1,132 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftOpenAPIGenerator open source project -// -// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// -import XCTest -@testable import OpenAPIRuntime - -final class Test_StringCodingRoundtrip: Test_Runtime { - - func testRoundtrip() throws { - - enum SimpleEnum: String, Codable, Equatable { - case red - case green - case blue - } - - struct CustomValue: LosslessStringConvertible, Codable, Equatable { - var innerString: String - - init(innerString: String) { - self.innerString = innerString - } - - init?(_ description: String) { - self.init(innerString: description) - } - - var description: String { - innerString - } - - func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(innerString) - } - - enum CodingKeys: CodingKey { - case innerString - } - - init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - self.innerString = try container.decode(String.self) - } - } - - // An empty string. - try _test( - "", - "" - ) - - // An string with a space. - try _test( - "Hello World!", - "Hello World!" - ) - - // An enum. - try _test( - SimpleEnum.red, - "red" - ) - - // A custom value. - try _test( - CustomValue(innerString: "hello"), - "hello" - ) - - // An integer. - try _test( - 1234, - "1234" - ) - - // A float. - try _test( - 12.34, - "12.34" - ) - - // A bool. - try _test( - true, - "true" - ) - - // A Date. - try _test( - Date(timeIntervalSince1970: 1_692_948_899), - "2023-08-25T07:34:59Z" - ) - } - - func _test( - _ value: T, - _ expectedString: String, - file: StaticString = #file, - line: UInt = #line - ) throws { - let encoder = StringEncoder(dateTranscoder: .iso8601) - let encodedString = try encoder.encode(value) - XCTAssertEqual( - encodedString, - expectedString, - file: file, - line: line - ) - let decoder = StringDecoder(dateTranscoder: .iso8601) - let decodedValue = try decoder.decode( - T.self, - from: encodedString - ) - XCTAssertEqual( - decodedValue, - value, - file: file, - line: line - ) - } -} diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index 49dcc23d..3b2f1e83 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -14,6 +14,7 @@ import XCTest @_spi(Generated) import OpenAPIRuntime +import HTTPTypes class Test_Runtime: XCTestCase { @@ -42,8 +43,8 @@ class Test_Runtime: XCTestCase { return components } - var testRequest: OpenAPIRuntime.Request { - .init(path: "/api", query: nil, method: .get) + var testRequest: HTTPRequest { + .init(soar_path: "/api", method: .get) } var testDate: Date { @@ -171,38 +172,70 @@ struct AuthenticationMiddleware: ClientMiddleware { var token: String func intercept( - _ request: Request, + _ request: HTTPRequest, + body: HTTPBody?, baseURL: URL, operationID: String, - next: (Request, URL) async throws -> Response - ) async throws -> Response { + next: (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) + ) async throws -> (HTTPResponse, HTTPBody?) { var request = request - request.headerFields.append( - .init( - name: "Authorization", - value: "Bearer \(token)" - ) - ) - return try await next(request, baseURL) + request.headerFields[.authorization] = "Bearer \(token)" + return try await next(request, body, baseURL) } } /// Prints the request method + path and response status code. struct PrintingMiddleware: ClientMiddleware { func intercept( - _ request: Request, + _ request: HTTPRequest, + body: HTTPBody?, baseURL: URL, operationID: String, - next: (Request, URL) async throws -> Response - ) async throws -> Response { - print("Sending \(request.method.name) \(request.path)") + next: (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?) + ) async throws -> (HTTPResponse, HTTPBody?) { + print("Sending \(request.method) \(request.path ?? "")") do { - let response = try await next(request, baseURL) - print("Received: \(response.statusCode)") - return response + let (response, responseBody) = try await next(request, body, baseURL) + print("Received: \(response.status)") + return (response, responseBody) } catch { print("Failed with error: \(error.localizedDescription)") throw error } } } + +public func XCTAssertEqualStringifiedData( + _ expression1: @autoclosure () throws -> S?, + _ expression2: @autoclosure () throws -> String, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line +) where S.Element == UInt8 { + do { + guard let value1 = try expression1() else { + XCTFail("First value is nil", file: file, line: line) + return + } + let actualString = String(decoding: Array(value1), as: UTF8.self) + XCTAssertEqual(actualString, try expression2(), file: file, line: line) + } catch { + XCTFail(error.localizedDescription, file: file, line: line) + } +} + +public func XCTAssertEqualStringifiedData( + _ expression1: @autoclosure () throws -> HTTPBody?, + _ expression2: @autoclosure () throws -> String, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line +) async throws { + let data: Data + if let body = try expression1() { + data = try await Data(collecting: body, upTo: .max) + } else { + data = .init() + } + XCTAssertEqualStringifiedData(data, try expression2(), message(), file: file, line: line) +} diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift index 8ae57d60..33ede7b9 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Parsing/Test_URIParser.swift @@ -128,7 +128,7 @@ final class Test_URIParser: Test_Runtime { ) throws { var parser = URIParser( configuration: variant.config, - data: input.string + data: input.string[...] ) let parsedNode = try parser.parseRoot() XCTAssertEqual( diff --git a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift index c8c93e1d..11ef8705 100644 --- a/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift +++ b/Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift @@ -432,7 +432,7 @@ final class Test_URICodingRoundtrip: Test_Runtime { let decodedValue = try decoder.decode( T.self, forKey: key, - from: encodedString + from: encodedString[...] ) XCTAssertEqual( decodedValue,