Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for application/xml body #102

Merged
merged 18 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
29 changes: 28 additions & 1 deletion Sources/OpenAPIRuntime/Conversion/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,27 @@ 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.
/// - Throws: An error if encoding fails.
func customEncode<T: Encodable>(_ value: T) throws -> Data
ugocottin marked this conversation as resolved.
Show resolved Hide resolved

/// Decodes a value of the given type from the given custom representation.
///
/// - 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<T: Decodable>(_ 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 {

Expand All @@ -105,17 +126,23 @@ public struct Configuration: Sendable {
/// The generator to use when creating mutlipart bodies.
public var multipartBoundaryGenerator: any MultipartBoundaryGenerator

/// Custom XML coder for encoding and decoding xml bodies.
public var xmlCoder: (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.
/// - 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
multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random,
xmlCoder: (any CustomCoder)? = nil
ugocottin marked this conversation as resolved.
Show resolved Hide resolved
) {
self.dateTranscoder = dateTranscoder
self.multipartBoundaryGenerator = multipartBoundaryGenerator
self.xmlCoder = xmlCoder
}
}
67 changes: 67 additions & 0 deletions Sources/OpenAPIRuntime/Conversion/Converter+Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,50 @@ 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<T: Encodable>(
_ 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<T: Encodable>(
_ 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`.
///
Expand Down Expand Up @@ -275,6 +319,29 @@ 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<T: Decodable, C>(
_ 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.
///
Expand Down
59 changes: 59 additions & 0 deletions Sources/OpenAPIRuntime/Conversion/Converter+Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,47 @@ 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<T: Decodable, C>(
_ 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<T: Decodable, C>(
_ 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:
Expand Down Expand Up @@ -347,6 +388,24 @@ 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<T: Encodable>(_ 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.
///
Expand Down
26 changes: 26 additions & 0 deletions Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,32 @@ 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.
/// - Throws: An error if no custom coder is present for XML coding.
func convertXMLToBodyCodable<T: Decodable>(_ body: HTTPBody) async throws -> T {
guard let coder = configuration.xmlCoder else {
throw RuntimeError.missingCoderForCustomContentType(contentType: OpenAPIMIMEType.xml.description)
}
let data = try await Data(collecting: body, upTo: .max)
return try coder.customDecode(T.self, from: data)
}

/// 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<T: Encodable>(_ value: T) throws -> HTTPBody {
guard let coder = configuration.xmlCoder else {
throw RuntimeError.missingCoderForCustomContentType(contentType: OpenAPIMIMEType.xml.description)
}
let data = try coder.customEncode(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.
Expand Down
16 changes: 16 additions & 0 deletions Sources/OpenAPIRuntime/Deprecated/Deprecated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
3 changes: 3 additions & 0 deletions Sources/OpenAPIRuntime/Errors/RuntimeError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -88,6 +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 .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)"
Expand Down
31 changes: 31 additions & 0 deletions Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,28 @@ 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 |
czechboy0 marked this conversation as resolved.
Show resolved Hide resolved
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 {
Expand Down Expand Up @@ -206,6 +228,15 @@ 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 {
Expand Down
29 changes: 29 additions & 0 deletions Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,24 @@ 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 {
Expand Down Expand Up @@ -318,6 +336,17 @@ 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 {
Expand Down
10 changes: 9 additions & 1 deletion Tests/OpenAPIRuntimeTests/Test_Runtime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ 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, xmlCoder: customCoder) }

var converter: Converter { .init(configuration: configuration) }

Expand Down Expand Up @@ -222,6 +223,13 @@ struct MockMiddleware: ClientMiddleware, ServerMiddleware {
}
}

struct MockCustomCoder: CustomCoder {
func customEncode<T>(_ value: T) throws -> Data where T: Encodable { try JSONEncoder().encode(value) }
func customDecode<T>(_ 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:
Expand Down