From 9eea362037291fdcbc1789d684570d662a604be0 Mon Sep 17 00:00:00 2001 From: Ugo Cottin Date: Fri, 29 Mar 2024 17:11:41 +0100 Subject: [PATCH 01/17] add xml encoding and decoding methods Signed-off-by: Ugo Cottin --- .../Conversion/Converter+Client.swift | 70 +++++++++++++++++++ .../Conversion/Converter+Server.swift | 61 ++++++++++++++++ .../Conversion/CurrencyExtensions.swift | 22 ++++++ 3 files changed, 153 insertions(+) diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index ea575002..c6920aba 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -127,6 +127,52 @@ extension Converter { convert: convertBodyCodableToJSON ) } + + /// Sets an optional request body as XML in the specified header fields and returns an `HTTPBody`. + /// + /// - Parameters: + /// - value: The optional value to be set as the request body. + /// - headerFields: The header fields in which to set the content type. + /// - contentType: The content type to be set in the header fields. + /// + /// - Returns: An `HTTPBody` representing the XML-encoded request body, or `nil` if the `value` is `nil`. + /// + /// - Throws: An error if setting the request body as XML fails. + public func setOptionalRequestBodyAsXML( + _ value: T?, + headerFields: inout HTTPFields, + contentType: String + ) throws -> HTTPBody? { + try setOptionalRequestBody( + value, + headerFields: &headerFields, + contentType: contentType, + convert: convertBodyCodableToXML + ) + } + + /// Sets a required request body as XML in the specified header fields and returns an `HTTPBody`. + /// + /// - Parameters: + /// - value: The value to be set as the request body. + /// - headerFields: The header fields in which to set the content type. + /// - contentType: The content type to be set in the header fields. + /// + /// - Returns: An `HTTPBody` representing the XML-encoded request body. + /// + /// - Throws: An error if setting the request body as XML fails. + public func setRequiredRequestBodyAsXML( + _ value: T, + headerFields: inout HTTPFields, + contentType: String + ) throws -> HTTPBody { + try setRequiredRequestBody( + value, + headerFields: &headerFields, + contentType: contentType, + convert: convertBodyCodableToXML + ) + } /// Sets an optional request body as binary in the specified header fields and returns an `HTTPBody`. /// @@ -275,6 +321,30 @@ extension Converter { convert: convertJSONToBodyCodable ) } + + /// Retrieves the response body as XML and transforms it into a specified type. + /// + /// - Parameters: + /// - type: The type to decode the XML into. + /// - data: The HTTP body data containing the XML. + /// - transform: A transformation function to apply to the decoded XML. + /// + /// - Returns: The transformed result of type `C`. + /// + /// - Throws: An error if retrieving or transforming the response body fails. + public func getResponseBodyAsXML( + _ type: T.Type, + from data: HTTPBody?, + transforming transform: (T) -> C + ) async throws -> C { + guard let data else { throw RuntimeError.missingRequiredResponseBody } + return try await getBufferingResponseBody( + type, + from: data, + transforming: transform, + convert: convertXMLToBodyCodable + ) + } /// Retrieves the response body as binary data and transforms it into a specified type. /// diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index e8f36306..9f5331fa 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -214,6 +214,48 @@ extension Converter { ) } + /// Retrieves and decodes an optional XML-encoded request body and transforms it to a different type. + /// + /// - Parameters: + /// - type: The type to decode the request body into. + /// - data: The HTTP request body to decode, or `nil` if the body is not present. + /// - transform: A closure that transforms the decoded value to a different type. + /// - Returns: The transformed value, or `nil` if the request body is not present or if decoding fails. + /// - Throws: An error if there are issues decoding or transforming the request body. + public func getOptionalRequestBodyAsXML( + _ type: T.Type, + from data: HTTPBody?, + transforming transform: (T) -> C + ) async throws -> C? { + try await getOptionalBufferingRequestBody( + type, + from: data, + transforming: transform, + convert: convertXMLToBodyCodable + ) + } + + /// Retrieves and decodes a required XML-encoded request body and transforms it to a different type. + /// + /// - Parameters: + /// - type: The type to decode the request body into. + /// - data: The HTTP request body to decode, or `nil` if the body is not present. + /// - transform: A closure that transforms the decoded value to a different type. + /// - Returns: The transformed value. + /// - Throws: An error if the request body is not present, if decoding fails, or if there are issues transforming the request body. + public func getRequiredRequestBodyAsXML( + _ type: T.Type, + from data: HTTPBody?, + transforming transform: (T) -> C + ) async throws -> C { + try await getRequiredBufferingRequestBody( + type, + from: data, + transforming: transform, + convert: convertXMLToBodyCodable + ) + } + /// Retrieves and transforms an optional binary request body. /// /// - Parameters: @@ -347,6 +389,25 @@ extension Converter { convert: convertBodyCodableToJSON ) } + + /// Sets the response body as XML data, serializing the provided value. + /// + /// - Parameters: + /// - value: The value to be serialized into the response body. + /// - headerFields: The HTTP header fields to update with the new `contentType`. + /// - contentType: The content type to set in the HTTP header fields. + /// - Returns: An `HTTPBody` with the response body set as XML data. + /// - Throws: An error if serialization or setting the response body fails. + public func setResponseBodyAsXML(_ value: T, headerFields: inout HTTPFields, contentType: String) + throws -> HTTPBody + { + try setResponseBody( + value, + headerFields: &headerFields, + contentType: contentType, + convert: convertBodyCodableToXML + ) + } /// Sets the response body as binary data. /// diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index 38a17115..c59bceb1 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -144,6 +144,28 @@ extension Converter { return HTTPBody(data) } + /// Returns a value decoded from a XML body. + /// - Parameter body: The body containing the raw XML bytes. + /// - Returns: A decoded value. + /// - Throws: An error if decoding from the body fails. + func convertXMLToBodyCodable(_ body: HTTPBody) async throws -> T { + fatalError("Not implemented") + // TODO: decode data + // 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. + /// - Throws: An error if encoding to JSON fails. + func convertBodyCodableToXML(_ value: T) throws -> HTTPBody { + fatalError("Not implemented") + // TODO: encode data + // 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. From 2d4367c26155d721ca0dbae945b0c2b468a5c576 Mon Sep 17 00:00:00 2001 From: Ugo Cottin Date: Fri, 29 Mar 2024 18:13:00 +0100 Subject: [PATCH 02/17] add customCoder protocol and configuration Signed-off-by: Ugo Cottin --- .../Conversion/Configuration.swift | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Conversion/Configuration.swift b/Sources/OpenAPIRuntime/Conversion/Configuration.swift index e0b593a5..b350859d 100644 --- a/Sources/OpenAPIRuntime/Conversion/Configuration.swift +++ b/Sources/OpenAPIRuntime/Conversion/Configuration.swift @@ -96,6 +96,23 @@ extension JSONDecoder.DateDecodingStrategy { } } +/// A type that allows custom content type encoding and decoding. +public protocol CustomCoder: Sendable { + + /// Encodes the given value and returns its custom encoded representation. + /// + /// - parameter value: The value to encode. + /// - returns: A new `Data` value containing the custom encoded data. + func customEncode(_ value: T) throws -> Data + + /// Decodes a value of the given type from the given custom representation. + /// + /// - parameter type: The type of the value to decode. + /// - parameter data: The data to decode from. + /// - returns: A value of the requested type. + func customDecode(_ type: T.Type, from data: Data) throws -> T +} + /// A set of configuration values used by the generated client and server types. public struct Configuration: Sendable { @@ -105,17 +122,27 @@ public struct Configuration: Sendable { /// The generator to use when creating mutlipart bodies. public var multipartBoundaryGenerator: any MultipartBoundaryGenerator + public var customCoders: [String: any CustomCoder] + /// Creates a new configuration with the specified values. /// /// - Parameters: /// - dateTranscoder: The transcoder to use when converting between date /// and string values. /// - multipartBoundaryGenerator: The generator to use when creating mutlipart bodies. + /// - customCoders: Array of custom coder to use for encoding and decoding unsupported content types. public init( dateTranscoder: any DateTranscoder = .iso8601, - multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random + multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random, + customCoders: [String: any CustomCoder] = [:] ) { self.dateTranscoder = dateTranscoder self.multipartBoundaryGenerator = multipartBoundaryGenerator + self.customCoders = customCoders + } + + public func customCoder(for contentType: String) -> (any CustomCoder)? { + self.customCoders[contentType] } + } From 0c1bd9bc50770681b5f9c28bff231be20dd2b9d0 Mon Sep 17 00:00:00 2001 From: Ugo Cottin Date: Fri, 29 Mar 2024 18:13:26 +0100 Subject: [PATCH 03/17] add new RuntimeError for missing custom coder Signed-off-by: Ugo Cottin --- Sources/OpenAPIRuntime/Errors/RuntimeError.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index 150b804c..612dc00f 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -26,6 +26,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret // Data conversion case failedToDecodeStringConvertibleValue(type: String) + case missingCoderForCustomContentType(contentType: String) enum ParameterLocation: String, CustomStringConvertible { case query From de2abd6e891cb9722d6b776ef65fe7326be25048 Mon Sep 17 00:00:00 2001 From: Ugo Cottin Date: Fri, 29 Mar 2024 18:16:40 +0100 Subject: [PATCH 04/17] implement xml to body codable convertion Signed-off-by: Ugo Cottin --- .../Conversion/CurrencyExtensions.swift | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index c59bceb1..0658161f 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -148,22 +148,26 @@ extension Converter { /// - Parameter body: The body containing the raw XML bytes. /// - Returns: A decoded value. /// - Throws: An error if decoding from the body fails. + /// - Throws: An error if no custom coder is present for XML coding. func convertXMLToBodyCodable(_ body: HTTPBody) async throws -> T { - fatalError("Not implemented") - // TODO: decode data - // let data = try await Data(collecting: body, upTo: .max) - // return try decoder.decode(T.self, from: data) + guard let coder = configuration.customCoder(for: "application/xml") else { + throw RuntimeError.missingCoderForCustomContentType(contentType: "application/xml") + } + let data = try await Data(collecting: body, upTo: .max) + return try coder.customDecode(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. - /// - Throws: An error if encoding to JSON fails. + /// Returns a XML body for the provided encodable value. + /// - Parameter value: The value to encode as XML. + /// - Returns: The raw XML body. + /// - Throws: An error if encoding to XML fails. + /// - Throws: An error if no custom coder is present for XML coding. func convertBodyCodableToXML(_ value: T) throws -> HTTPBody { - fatalError("Not implemented") - // TODO: encode data - // let data = try encoder.encode(value) - // return HTTPBody(data) + guard let coder = configuration.customCoder(for: "application/xml") else { + throw RuntimeError.missingCoderForCustomContentType(contentType: "application/xml") + } + let data = try coder.customEncode(value) + return HTTPBody(data) } /// Returns a value decoded from a URL-encoded form body. From ec73e7d740050a383489b56fecf1e9685bf89ca7 Mon Sep 17 00:00:00 2001 From: Ugo Cottin Date: Fri, 29 Mar 2024 18:16:55 +0100 Subject: [PATCH 05/17] lint Configuration Signed-off-by: Ugo Cottin --- Sources/OpenAPIRuntime/Conversion/Configuration.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/Configuration.swift b/Sources/OpenAPIRuntime/Conversion/Configuration.swift index b350859d..c3b2fd72 100644 --- a/Sources/OpenAPIRuntime/Conversion/Configuration.swift +++ b/Sources/OpenAPIRuntime/Conversion/Configuration.swift @@ -98,19 +98,20 @@ extension JSONDecoder.DateDecodingStrategy { /// A type that allows custom content type encoding and decoding. public protocol CustomCoder: Sendable { - + /// Encodes the given value and returns its custom encoded representation. /// /// - parameter value: The value to encode. /// - returns: A new `Data` value containing the custom encoded data. func customEncode(_ value: T) throws -> Data - + /// Decodes a value of the given type from the given custom representation. /// /// - parameter type: The type of the value to decode. /// - parameter data: The data to decode from. /// - returns: A value of the requested type. func customDecode(_ type: T.Type, from data: Data) throws -> T + } /// A set of configuration values used by the generated client and server types. @@ -140,9 +141,9 @@ public struct Configuration: Sendable { self.multipartBoundaryGenerator = multipartBoundaryGenerator self.customCoders = customCoders } - + public func customCoder(for contentType: String) -> (any CustomCoder)? { self.customCoders[contentType] } - + } From 166598bf595a3bf765432dbe0c557740507e721f Mon Sep 17 00:00:00 2001 From: Ugo Cottin Date: Fri, 29 Mar 2024 18:18:51 +0100 Subject: [PATCH 06/17] add missingCoderForCustomContentType pretty description Signed-off-by: Ugo Cottin --- Sources/OpenAPIRuntime/Errors/RuntimeError.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index 612dc00f..b4904491 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -89,6 +89,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret case .invalidBase64String(let string): return "Invalid base64-encoded string (first 128 bytes): '\(string.prefix(128))'" case .failedToDecodeStringConvertibleValue(let string): return "Failed to decode a value of type '\(string)'." + case .missingCoderForCustomContentType(let contentType): return "Missing custom coder for content type '\(contentType)'" case .unsupportedParameterStyle(name: let name, location: let location, style: let style, explode: let explode): return "Unsupported parameter style, parameter name: '\(name)', kind: \(location), style: \(style), explode: \(explode)" From d9ff2f271d2c6b129a56ad5130fa81f2fafd841a Mon Sep 17 00:00:00 2001 From: Ugo Cottin Date: Tue, 2 Apr 2024 12:08:19 +0200 Subject: [PATCH 07/17] add Tests --- .../Conversion/Test_Converter+Client.swift | 34 +++++++++++++++++++ .../Conversion/Test_Converter+Server.swift | 32 +++++++++++++++++ Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 14 +++++++- 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift index 4a7b669c..ce76f319 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -120,6 +120,30 @@ final class Test_ClientConverterExtensions: Test_Runtime { try await XCTAssertEqualStringifiedData(body, testStructPrettyString) XCTAssertEqual(headerFields, [.contentType: "application/json", .contentLength: "23"]) } + + // | client | set | request body | XML | optional | setOptionalRequestBodyAsXML | + func test_setOptionalRequestBodyAsXML_codable() async throws { + var headerFields: HTTPFields = [:] + let body = try converter.setOptionalRequestBodyAsXML( + testStruct, + headerFields: &headerFields, + contentType: "application/xml" + ) + try await XCTAssertEqualStringifiedData(body, testStructString) + XCTAssertEqual(headerFields, [.contentType: "application/xml", .contentLength: "17"]) + } + + // | client | set | request body | XML | required | setRequiredRequestBodyAsXML | + func test_setRequiredRequestBodyAsXML_codable() async throws { + var headerFields: HTTPFields = [:] + let body = try converter.setRequiredRequestBodyAsXML( + testStruct, + headerFields: &headerFields, + contentType: "application/xml" + ) + try await XCTAssertEqualStringifiedData(body, testStructString) + XCTAssertEqual(headerFields, [.contentType: "application/xml", .contentLength: "17"]) + } // | client | set | request body | urlEncodedForm | codable | optional | setRequiredRequestBodyAsURLEncodedForm | func test_setOptionalRequestBodyAsURLEncodedForm_codable() async throws { @@ -206,6 +230,16 @@ final class Test_ClientConverterExtensions: Test_Runtime { ) XCTAssertEqual(value, testStruct) } + + // | client | get | response body | XML | required | getResponseBodyAsXML | + func test_getResponseBodyAsXML_codable() async throws { + let value: TestPet = try await converter.getResponseBodyAsXML( + TestPet.self, + from: .init(testStructData), + transforming: { $0 } + ) + XCTAssertEqual(value, testStruct) + } // | client | get | response body | binary | required | getResponseBodyAsBinary | func test_getResponseBodyAsBinary_data() async throws { diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift index 632116b2..8fdcb303 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift @@ -247,6 +247,26 @@ final class Test_ServerConverterExtensions: Test_Runtime { ) XCTAssertEqual(body, testStruct) } + + // | server | get | request body | XML | optional | getOptionalRequestBodyAsXML | + func test_getOptionalRequestBodyAsXML_codable() async throws { + let body: TestPet? = try await converter.getOptionalRequestBodyAsXML( + TestPet.self, + from: .init(testStructData), + transforming: { $0 } + ) + XCTAssertEqual(body, testStruct) + } + + // | server | get | request body | XML | required | getRequiredRequestBodyAsXML | + func test_getRequiredRequestBodyAsXML_codable() async throws { + let body: TestPet = try await converter.getRequiredRequestBodyAsXML( + TestPet.self, + from: .init(testStructData), + transforming: { $0 } + ) + XCTAssertEqual(body, testStruct) + } // | server | get | request body | urlEncodedForm | optional | getOptionalRequestBodyAsURLEncodedForm | func test_getOptionalRequestBodyAsURLEncodedForm_codable() async throws { @@ -318,6 +338,18 @@ final class Test_ServerConverterExtensions: Test_Runtime { try await XCTAssertEqualStringifiedData(data, testStructPrettyString) XCTAssertEqual(headers, [.contentType: "application/json", .contentLength: "23"]) } + + // | server | set | response body | XML | required | setResponseBodyAsXML | + func test_setResponseBodyAsXML_codable() async throws { + var headers: HTTPFields = [:] + let data = try converter.setResponseBodyAsXML( + testStruct, + headerFields: &headers, + contentType: "application/xml" + ) + try await XCTAssertEqualStringifiedData(data, testStructString) + XCTAssertEqual(headers, [.contentType: "application/xml", .contentLength: "17"]) + } // | server | set | response body | binary | required | setResponseBodyAsBinary | func test_setResponseBodyAsBinary_data() async throws { diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index 9de89902..0f6b1bab 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -26,7 +26,9 @@ class Test_Runtime: XCTestCase { var serverURL: URL { get throws { try URL(validatingOpenAPIServerURL: "/api") } } - var configuration: Configuration { .init(multipartBoundaryGenerator: .constant) } + var customCoder: any CustomCoder { MockCustomCoder() } + + var configuration: Configuration { .init(multipartBoundaryGenerator: .constant, customCoders: ["application/xml": customCoder]) } var converter: Converter { .init(configuration: configuration) } @@ -222,6 +224,16 @@ struct MockMiddleware: ClientMiddleware, ServerMiddleware { } } +struct MockCustomCoder: CustomCoder { + func customEncode(_ value: T) throws -> Data where T : Encodable { + try JSONEncoder().encode(value) + } + + func customDecode(_ type: T.Type, from data: Data) throws -> T where T : Decodable { + try JSONDecoder().decode(T.self, from: data) + } +} + /// Asserts that a given URL's absolute string representation is equal to an expected string. /// /// - Parameters: From bf232ee97deec90f3bfb40ff4876380cb5dffd70 Mon Sep 17 00:00:00 2001 From: Ugo Cottin Date: Tue, 2 Apr 2024 15:04:11 +0200 Subject: [PATCH 08/17] Update Sources/OpenAPIRuntime/Errors/RuntimeError.swift Co-authored-by: Honza Dvorsky --- Sources/OpenAPIRuntime/Errors/RuntimeError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index b4904491..77b435cc 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -89,7 +89,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret case .invalidBase64String(let string): return "Invalid base64-encoded string (first 128 bytes): '\(string.prefix(128))'" case .failedToDecodeStringConvertibleValue(let string): return "Failed to decode a value of type '\(string)'." - case .missingCoderForCustomContentType(let contentType): return "Missing custom coder for content type '\(contentType)'" + case .missingCoderForCustomContentType(let contentType): return "Missing custom coder for content type '\(contentType)'." case .unsupportedParameterStyle(name: let name, location: let location, style: let style, explode: let explode): return "Unsupported parameter style, parameter name: '\(name)', kind: \(location), style: \(style), explode: \(explode)" From 00e0559c44babeac160ca739f511458c6b7df4bd Mon Sep 17 00:00:00 2001 From: Ugo Cottin Date: Tue, 2 Apr 2024 15:13:58 +0200 Subject: [PATCH 09/17] add OpenAPIMIMEType.xml --- Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift b/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift index 6dc2a730..d7092b1d 100644 --- a/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift +++ b/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift @@ -16,6 +16,9 @@ import Foundation /// A container for a parsed, valid MIME type. @_spi(Generated) public struct OpenAPIMIMEType: Equatable { + /// XML MIME type + public static let xml: OpenAPIMIMEType = .init(kind: .concrete(type: "application", subtype: "xml")) + /// The kind of the MIME type. public enum Kind: Equatable { From e86a8bf032370f94438234677b77facc65a4a8c9 Mon Sep 17 00:00:00 2001 From: Ugo Cottin Date: Tue, 2 Apr 2024 15:30:51 +0200 Subject: [PATCH 10/17] Use OpenAPIMIMEType in `missingCoderForCustomContentType` error --- Sources/OpenAPIRuntime/Errors/RuntimeError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index 77b435cc..9188a6e8 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -26,7 +26,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret // Data conversion case failedToDecodeStringConvertibleValue(type: String) - case missingCoderForCustomContentType(contentType: String) + case missingCoderForCustomContentType(contentType: OpenAPIMIMEType) enum ParameterLocation: String, CustomStringConvertible { case query From c80edaacd5c1efe382e2f702b96ee054727b7ac5 Mon Sep 17 00:00:00 2001 From: Ugo Cottin Date: Tue, 2 Apr 2024 15:31:43 +0200 Subject: [PATCH 11/17] remove custom coders to specific xml custom coder --- .../OpenAPIRuntime/Conversion/Configuration.swift | 15 ++++++--------- .../Conversion/CurrencyExtensions.swift | 8 ++++---- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/Configuration.swift b/Sources/OpenAPIRuntime/Conversion/Configuration.swift index c3b2fd72..4b121464 100644 --- a/Sources/OpenAPIRuntime/Conversion/Configuration.swift +++ b/Sources/OpenAPIRuntime/Conversion/Configuration.swift @@ -123,7 +123,8 @@ public struct Configuration: Sendable { /// The generator to use when creating mutlipart bodies. public var multipartBoundaryGenerator: any MultipartBoundaryGenerator - public var customCoders: [String: any CustomCoder] + /// Custom XML coder for encoding and decoding xml bodies. + public var xmlCoder: (any CustomCoder)? /// Creates a new configuration with the specified values. /// @@ -131,19 +132,15 @@ public struct Configuration: Sendable { /// - dateTranscoder: The transcoder to use when converting between date /// and string values. /// - multipartBoundaryGenerator: The generator to use when creating mutlipart bodies. - /// - customCoders: Array of custom coder to use for encoding and decoding unsupported content types. + /// - xmlCoder: Custom XML coder for encoding and decoding xml bodies. public init( dateTranscoder: any DateTranscoder = .iso8601, multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random, - customCoders: [String: any CustomCoder] = [:] + xmlCoder: (any CustomCoder)? = nil ) { self.dateTranscoder = dateTranscoder self.multipartBoundaryGenerator = multipartBoundaryGenerator - self.customCoders = customCoders + self.xmlCoder = xmlCoder } - - public func customCoder(for contentType: String) -> (any CustomCoder)? { - self.customCoders[contentType] - } - + } diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index 0658161f..6799c2d7 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -150,8 +150,8 @@ extension Converter { /// - Throws: An error if decoding from the body fails. /// - Throws: An error if no custom coder is present for XML coding. func convertXMLToBodyCodable(_ body: HTTPBody) async throws -> T { - guard let coder = configuration.customCoder(for: "application/xml") else { - throw RuntimeError.missingCoderForCustomContentType(contentType: "application/xml") + guard let coder = configuration.xmlCoder else { + throw RuntimeError.missingCoderForCustomContentType(contentType: OpenAPIMIMEType.xml) } let data = try await Data(collecting: body, upTo: .max) return try coder.customDecode(T.self, from: data) @@ -163,8 +163,8 @@ extension Converter { /// - Throws: An error if encoding to XML fails. /// - Throws: An error if no custom coder is present for XML coding. func convertBodyCodableToXML(_ value: T) throws -> HTTPBody { - guard let coder = configuration.customCoder(for: "application/xml") else { - throw RuntimeError.missingCoderForCustomContentType(contentType: "application/xml") + guard let coder = configuration.xmlCoder else { + throw RuntimeError.missingCoderForCustomContentType(contentType: OpenAPIMIMEType.xml) } let data = try coder.customEncode(value) return HTTPBody(data) From 63a9a8e94dcb9946984593e8734d99e3d3c93011 Mon Sep 17 00:00:00 2001 From: Ugo Cottin Date: Tue, 2 Apr 2024 15:36:46 +0200 Subject: [PATCH 12/17] fix lint issues --- .../OpenAPIRuntime/Conversion/Configuration.swift | 2 -- .../OpenAPIRuntime/Conversion/Converter+Client.swift | 3 --- .../OpenAPIRuntime/Conversion/Converter+Server.swift | 4 +--- Sources/OpenAPIRuntime/Errors/RuntimeError.swift | 3 ++- .../Conversion/Test_Converter+Client.swift | 3 --- .../Conversion/Test_Converter+Server.swift | 3 --- Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 12 +++++------- 7 files changed, 8 insertions(+), 22 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/Configuration.swift b/Sources/OpenAPIRuntime/Conversion/Configuration.swift index 4b121464..121a6215 100644 --- a/Sources/OpenAPIRuntime/Conversion/Configuration.swift +++ b/Sources/OpenAPIRuntime/Conversion/Configuration.swift @@ -125,7 +125,6 @@ public struct Configuration: Sendable { /// Custom XML coder for encoding and decoding xml bodies. public var xmlCoder: (any CustomCoder)? - /// Creates a new configuration with the specified values. /// /// - Parameters: @@ -142,5 +141,4 @@ public struct Configuration: Sendable { self.multipartBoundaryGenerator = multipartBoundaryGenerator self.xmlCoder = xmlCoder } - } diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index c6920aba..28abbdb2 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -127,7 +127,6 @@ extension Converter { convert: convertBodyCodableToJSON ) } - /// Sets an optional request body as XML in the specified header fields and returns an `HTTPBody`. /// /// - Parameters: @@ -150,7 +149,6 @@ extension Converter { convert: convertBodyCodableToXML ) } - /// Sets a required request body as XML in the specified header fields and returns an `HTTPBody`. /// /// - Parameters: @@ -321,7 +319,6 @@ extension Converter { convert: convertJSONToBodyCodable ) } - /// Retrieves the response body as XML and transforms it into a specified type. /// /// - Parameters: diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index 9f5331fa..75b0f521 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -234,7 +234,6 @@ extension Converter { convert: convertXMLToBodyCodable ) } - /// Retrieves and decodes a required XML-encoded request body and transforms it to a different type. /// /// - Parameters: @@ -389,7 +388,6 @@ extension Converter { convert: convertBodyCodableToJSON ) } - /// Sets the response body as XML data, serializing the provided value. /// /// - Parameters: @@ -399,7 +397,7 @@ extension Converter { /// - Returns: An `HTTPBody` with the response body set as XML data. /// - Throws: An error if serialization or setting the response body fails. public func setResponseBodyAsXML(_ value: T, headerFields: inout HTTPFields, contentType: String) - throws -> HTTPBody + throws -> HTTPBody { try setResponseBody( value, diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index 9188a6e8..3b871875 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -89,7 +89,8 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret case .invalidBase64String(let string): return "Invalid base64-encoded string (first 128 bytes): '\(string.prefix(128))'" case .failedToDecodeStringConvertibleValue(let string): return "Failed to decode a value of type '\(string)'." - case .missingCoderForCustomContentType(let contentType): return "Missing custom coder for content type '\(contentType)'." + case .missingCoderForCustomContentType(let contentType): + return "Missing custom coder for content type '\(contentType)'." case .unsupportedParameterStyle(name: let name, location: let location, style: let style, explode: let explode): return "Unsupported parameter style, parameter name: '\(name)', kind: \(location), style: \(style), explode: \(explode)" diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift index ce76f319..0f3bf066 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -120,7 +120,6 @@ final class Test_ClientConverterExtensions: Test_Runtime { try await XCTAssertEqualStringifiedData(body, testStructPrettyString) XCTAssertEqual(headerFields, [.contentType: "application/json", .contentLength: "23"]) } - // | client | set | request body | XML | optional | setOptionalRequestBodyAsXML | func test_setOptionalRequestBodyAsXML_codable() async throws { var headerFields: HTTPFields = [:] @@ -132,7 +131,6 @@ final class Test_ClientConverterExtensions: Test_Runtime { try await XCTAssertEqualStringifiedData(body, testStructString) XCTAssertEqual(headerFields, [.contentType: "application/xml", .contentLength: "17"]) } - // | client | set | request body | XML | required | setRequiredRequestBodyAsXML | func test_setRequiredRequestBodyAsXML_codable() async throws { var headerFields: HTTPFields = [:] @@ -230,7 +228,6 @@ final class Test_ClientConverterExtensions: Test_Runtime { ) XCTAssertEqual(value, testStruct) } - // | client | get | response body | XML | required | getResponseBodyAsXML | func test_getResponseBodyAsXML_codable() async throws { let value: TestPet = try await converter.getResponseBodyAsXML( diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift index 8fdcb303..b2305a08 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift @@ -247,7 +247,6 @@ final class Test_ServerConverterExtensions: Test_Runtime { ) XCTAssertEqual(body, testStruct) } - // | server | get | request body | XML | optional | getOptionalRequestBodyAsXML | func test_getOptionalRequestBodyAsXML_codable() async throws { let body: TestPet? = try await converter.getOptionalRequestBodyAsXML( @@ -257,7 +256,6 @@ final class Test_ServerConverterExtensions: Test_Runtime { ) XCTAssertEqual(body, testStruct) } - // | server | get | request body | XML | required | getRequiredRequestBodyAsXML | func test_getRequiredRequestBodyAsXML_codable() async throws { let body: TestPet = try await converter.getRequiredRequestBodyAsXML( @@ -338,7 +336,6 @@ final class Test_ServerConverterExtensions: Test_Runtime { try await XCTAssertEqualStringifiedData(data, testStructPrettyString) XCTAssertEqual(headers, [.contentType: "application/json", .contentLength: "23"]) } - // | server | set | response body | XML | required | setResponseBodyAsXML | func test_setResponseBodyAsXML_codable() async throws { var headers: HTTPFields = [:] diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index 0f6b1bab..03afffe4 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -27,8 +27,9 @@ class Test_Runtime: XCTestCase { var serverURL: URL { get throws { try URL(validatingOpenAPIServerURL: "/api") } } var customCoder: any CustomCoder { MockCustomCoder() } - - var configuration: Configuration { .init(multipartBoundaryGenerator: .constant, customCoders: ["application/xml": customCoder]) } + var configuration: Configuration { + .init(multipartBoundaryGenerator: .constant, customCoders: ["application/xml": customCoder]) + } var converter: Converter { .init(configuration: configuration) } @@ -225,11 +226,8 @@ struct MockMiddleware: ClientMiddleware, ServerMiddleware { } struct MockCustomCoder: CustomCoder { - func customEncode(_ value: T) throws -> Data where T : Encodable { - try JSONEncoder().encode(value) - } - - func customDecode(_ type: T.Type, from data: Data) throws -> T where T : Decodable { + func customEncode(_ value: T) throws -> Data where T: Encodable { try JSONEncoder().encode(value) } + func customDecode(_ type: T.Type, from data: Data) throws -> T where T: Decodable { try JSONDecoder().decode(T.self, from: data) } } From 72a811d6785d786683cf2af8114506fe5cce9f92 Mon Sep 17 00:00:00 2001 From: Ugo Cottin Date: Tue, 2 Apr 2024 15:42:28 +0200 Subject: [PATCH 13/17] fix lint comments --- .../OpenAPIRuntime/Conversion/Configuration.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/Configuration.swift b/Sources/OpenAPIRuntime/Conversion/Configuration.swift index 121a6215..ac524a30 100644 --- a/Sources/OpenAPIRuntime/Conversion/Configuration.swift +++ b/Sources/OpenAPIRuntime/Conversion/Configuration.swift @@ -101,15 +101,18 @@ public protocol CustomCoder: Sendable { /// Encodes the given value and returns its custom encoded representation. /// - /// - parameter value: The value to encode. - /// - returns: A new `Data` value containing the custom encoded data. + /// - Parameter value: The value to encode. + /// - Returns: A new `Data` value containing the custom encoded data. + /// - Throws: An error if encoding fails. func customEncode(_ value: T) throws -> Data /// Decodes a value of the given type from the given custom representation. /// - /// - parameter type: The type of the value to decode. - /// - parameter data: The data to decode from. - /// - returns: A value of the requested type. + /// - Parameters: + /// - type: The type of the value to decode. + /// - data: The data to decode from. + /// - Returns: A value of the requested type. + /// - Throws: An error if decoding fails. func customDecode(_ type: T.Type, from data: Data) throws -> T } From 79098ce59171627f8f9f9d9b0283e80da432b807 Mon Sep 17 00:00:00 2001 From: Ugo Cottin Date: Tue, 2 Apr 2024 15:43:09 +0200 Subject: [PATCH 14/17] Update Sources/OpenAPIRuntime/Conversion/Configuration.swift Co-authored-by: Honza Dvorsky --- Sources/OpenAPIRuntime/Conversion/Configuration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Conversion/Configuration.swift b/Sources/OpenAPIRuntime/Conversion/Configuration.swift index ac524a30..ea58def4 100644 --- a/Sources/OpenAPIRuntime/Conversion/Configuration.swift +++ b/Sources/OpenAPIRuntime/Conversion/Configuration.swift @@ -134,7 +134,7 @@ public struct Configuration: Sendable { /// - dateTranscoder: The transcoder to use when converting between date /// and string values. /// - multipartBoundaryGenerator: The generator to use when creating mutlipart bodies. - /// - xmlCoder: Custom XML coder for encoding and decoding xml bodies. + /// - xmlCoder: Custom XML coder for encoding and decoding xml bodies. Only required when using XML body payloads. public init( dateTranscoder: any DateTranscoder = .iso8601, multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random, From cff70882f8d7381c38e2611ca5ac6503b933ee19 Mon Sep 17 00:00:00 2001 From: Ugo Cottin Date: Tue, 2 Apr 2024 15:48:33 +0200 Subject: [PATCH 15/17] deprecate 2-param initializer --- .../Conversion/Configuration.swift | 1 + .../OpenAPIRuntime/Deprecated/Deprecated.swift | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/Sources/OpenAPIRuntime/Conversion/Configuration.swift b/Sources/OpenAPIRuntime/Conversion/Configuration.swift index ea58def4..f5ca02be 100644 --- a/Sources/OpenAPIRuntime/Conversion/Configuration.swift +++ b/Sources/OpenAPIRuntime/Conversion/Configuration.swift @@ -128,6 +128,7 @@ public struct Configuration: Sendable { /// Custom XML coder for encoding and decoding xml bodies. public var xmlCoder: (any CustomCoder)? + /// Creates a new configuration with the specified values. /// /// - Parameters: diff --git a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift index bf030d1c..1cfa5c99 100644 --- a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift +++ b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift @@ -22,3 +22,19 @@ extension UndocumentedPayload { self.init(headerFields: [:], body: nil) } } + +extension Configuration { + /// Creates a new configuration with the specified values. + /// + /// - Parameters: + /// - dateTranscoder: The transcoder to use when converting between date + /// and string values. + /// - multipartBoundaryGenerator: The generator to use when creating mutlipart bodies. + @available(*, deprecated, renamed: "init(dateTranscoder:multipartBoundaryGenerator:xmlCoder:)") @_disfavoredOverload + public init( + dateTranscoder: any DateTranscoder = .iso8601, + multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random + ) { + self.init(dateTranscoder: dateTranscoder, multipartBoundaryGenerator: multipartBoundaryGenerator, xmlCoder: nil) + } +} From 89d3df074151914965453ccd563c4b6f580be1c1 Mon Sep 17 00:00:00 2001 From: Ugo Cottin Date: Tue, 2 Apr 2024 15:49:09 +0200 Subject: [PATCH 16/17] remove customCoders, use single xmlCoder --- Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index 03afffe4..fe31067e 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -27,9 +27,7 @@ class Test_Runtime: XCTestCase { var serverURL: URL { get throws { try URL(validatingOpenAPIServerURL: "/api") } } var customCoder: any CustomCoder { MockCustomCoder() } - var configuration: Configuration { - .init(multipartBoundaryGenerator: .constant, customCoders: ["application/xml": customCoder]) - } + var configuration: Configuration { .init(multipartBoundaryGenerator: .constant, xmlCoder: customCoder) } var converter: Converter { .init(configuration: configuration) } From dc0bca489870ed21546499f324c0f2592ae512c9 Mon Sep 17 00:00:00 2001 From: Ugo Cottin Date: Wed, 3 Apr 2024 09:14:38 +0200 Subject: [PATCH 17/17] String associated type for RuntimeError.missingCoderForCustomContentType(contentType:) --- Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift | 4 ++-- Sources/OpenAPIRuntime/Errors/RuntimeError.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index 6799c2d7..ea07d217 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -151,7 +151,7 @@ extension Converter { /// - Throws: An error if no custom coder is present for XML coding. func convertXMLToBodyCodable(_ body: HTTPBody) async throws -> T { guard let coder = configuration.xmlCoder else { - throw RuntimeError.missingCoderForCustomContentType(contentType: OpenAPIMIMEType.xml) + throw RuntimeError.missingCoderForCustomContentType(contentType: OpenAPIMIMEType.xml.description) } let data = try await Data(collecting: body, upTo: .max) return try coder.customDecode(T.self, from: data) @@ -164,7 +164,7 @@ extension Converter { /// - Throws: An error if no custom coder is present for XML coding. func convertBodyCodableToXML(_ value: T) throws -> HTTPBody { guard let coder = configuration.xmlCoder else { - throw RuntimeError.missingCoderForCustomContentType(contentType: OpenAPIMIMEType.xml) + throw RuntimeError.missingCoderForCustomContentType(contentType: OpenAPIMIMEType.xml.description) } let data = try coder.customEncode(value) return HTTPBody(data) diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index 3b871875..f3c21db9 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -26,7 +26,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret // Data conversion case failedToDecodeStringConvertibleValue(type: String) - case missingCoderForCustomContentType(contentType: OpenAPIMIMEType) + case missingCoderForCustomContentType(contentType: String) enum ParameterLocation: String, CustomStringConvertible { case query