Skip to content

Commit 0d36255

Browse files
authored
Merge branch 'main' into feature/deep_object_style
2 parents 3e3b4e6 + 9d4a2ff commit 0d36255

14 files changed

+293
-10
lines changed

Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import Foundation
1616
/// A container for a parsed, valid MIME type.
1717
@_spi(Generated) public struct OpenAPIMIMEType: Equatable {
1818

19+
/// XML MIME type
20+
public static let xml: OpenAPIMIMEType = .init(kind: .concrete(type: "application", subtype: "xml"))
21+
1922
/// The kind of the MIME type.
2023
public enum Kind: Equatable {
2124

Sources/OpenAPIRuntime/Conversion/CodableExtensions.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@
102102
/// - Parameter additionalProperties: A container of additional properties.
103103
/// - Throws: An error if there are issues with encoding the additional properties.
104104
public func encodeAdditionalProperties(_ additionalProperties: OpenAPIObjectContainer) throws {
105-
guard !additionalProperties.value.isEmpty else { return }
106105
var container = container(keyedBy: StringKey.self)
107106
for (key, value) in additionalProperties.value {
108107
try container.encode(OpenAPIValueContainer(unvalidatedValue: value), forKey: .init(key))
@@ -116,7 +115,6 @@
116115
/// - Parameter additionalProperties: A container of additional properties.
117116
/// - Throws: An error if there are issues with encoding the additional properties.
118117
public func encodeAdditionalProperties<T: Encodable>(_ additionalProperties: [String: T]) throws {
119-
guard !additionalProperties.isEmpty else { return }
120118
var container = container(keyedBy: StringKey.self)
121119
for (key, value) in additionalProperties { try container.encode(value, forKey: .init(key)) }
122120
}

Sources/OpenAPIRuntime/Conversion/Configuration.swift

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,27 @@ extension JSONDecoder.DateDecodingStrategy {
9696
}
9797
}
9898

99+
/// A type that allows custom content type encoding and decoding.
100+
public protocol CustomCoder: Sendable {
101+
102+
/// Encodes the given value and returns its custom encoded representation.
103+
///
104+
/// - Parameter value: The value to encode.
105+
/// - Returns: A new `Data` value containing the custom encoded data.
106+
/// - Throws: An error if encoding fails.
107+
func customEncode<T: Encodable>(_ value: T) throws -> Data
108+
109+
/// Decodes a value of the given type from the given custom representation.
110+
///
111+
/// - Parameters:
112+
/// - type: The type of the value to decode.
113+
/// - data: The data to decode from.
114+
/// - Returns: A value of the requested type.
115+
/// - Throws: An error if decoding fails.
116+
func customDecode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T
117+
118+
}
119+
99120
/// A set of configuration values used by the generated client and server types.
100121
public struct Configuration: Sendable {
101122

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

129+
/// Custom XML coder for encoding and decoding xml bodies.
130+
public var xmlCoder: (any CustomCoder)?
131+
108132
/// Creates a new configuration with the specified values.
109133
///
110134
/// - Parameters:
111135
/// - dateTranscoder: The transcoder to use when converting between date
112136
/// and string values.
113137
/// - multipartBoundaryGenerator: The generator to use when creating mutlipart bodies.
138+
/// - xmlCoder: Custom XML coder for encoding and decoding xml bodies. Only required when using XML body payloads.
114139
public init(
115140
dateTranscoder: any DateTranscoder = .iso8601,
116-
multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random
141+
multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random,
142+
xmlCoder: (any CustomCoder)? = nil
117143
) {
118144
self.dateTranscoder = dateTranscoder
119145
self.multipartBoundaryGenerator = multipartBoundaryGenerator
146+
self.xmlCoder = xmlCoder
120147
}
121148
}

Sources/OpenAPIRuntime/Conversion/Converter+Client.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,50 @@ extension Converter {
127127
convert: convertBodyCodableToJSON
128128
)
129129
}
130+
/// Sets an optional request body as XML in the specified header fields and returns an `HTTPBody`.
131+
///
132+
/// - Parameters:
133+
/// - value: The optional value to be set as the request body.
134+
/// - headerFields: The header fields in which to set the content type.
135+
/// - contentType: The content type to be set in the header fields.
136+
///
137+
/// - Returns: An `HTTPBody` representing the XML-encoded request body, or `nil` if the `value` is `nil`.
138+
///
139+
/// - Throws: An error if setting the request body as XML fails.
140+
public func setOptionalRequestBodyAsXML<T: Encodable>(
141+
_ value: T?,
142+
headerFields: inout HTTPFields,
143+
contentType: String
144+
) throws -> HTTPBody? {
145+
try setOptionalRequestBody(
146+
value,
147+
headerFields: &headerFields,
148+
contentType: contentType,
149+
convert: convertBodyCodableToXML
150+
)
151+
}
152+
/// Sets a required request body as XML in the specified header fields and returns an `HTTPBody`.
153+
///
154+
/// - Parameters:
155+
/// - value: The value to be set as the request body.
156+
/// - headerFields: The header fields in which to set the content type.
157+
/// - contentType: The content type to be set in the header fields.
158+
///
159+
/// - Returns: An `HTTPBody` representing the XML-encoded request body.
160+
///
161+
/// - Throws: An error if setting the request body as XML fails.
162+
public func setRequiredRequestBodyAsXML<T: Encodable>(
163+
_ value: T,
164+
headerFields: inout HTTPFields,
165+
contentType: String
166+
) throws -> HTTPBody {
167+
try setRequiredRequestBody(
168+
value,
169+
headerFields: &headerFields,
170+
contentType: contentType,
171+
convert: convertBodyCodableToXML
172+
)
173+
}
130174

131175
/// Sets an optional request body as binary in the specified header fields and returns an `HTTPBody`.
132176
///
@@ -275,6 +319,29 @@ extension Converter {
275319
convert: convertJSONToBodyCodable
276320
)
277321
}
322+
/// Retrieves the response body as XML and transforms it into a specified type.
323+
///
324+
/// - Parameters:
325+
/// - type: The type to decode the XML into.
326+
/// - data: The HTTP body data containing the XML.
327+
/// - transform: A transformation function to apply to the decoded XML.
328+
///
329+
/// - Returns: The transformed result of type `C`.
330+
///
331+
/// - Throws: An error if retrieving or transforming the response body fails.
332+
public func getResponseBodyAsXML<T: Decodable, C>(
333+
_ type: T.Type,
334+
from data: HTTPBody?,
335+
transforming transform: (T) -> C
336+
) async throws -> C {
337+
guard let data else { throw RuntimeError.missingRequiredResponseBody }
338+
return try await getBufferingResponseBody(
339+
type,
340+
from: data,
341+
transforming: transform,
342+
convert: convertXMLToBodyCodable
343+
)
344+
}
278345

279346
/// Retrieves the response body as binary data and transforms it into a specified type.
280347
///

Sources/OpenAPIRuntime/Conversion/Converter+Common.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,12 @@ extension Converter {
6161
// The force unwrap is safe, we only get here if the array is not empty.
6262
let bestOption = evaluatedOptions.max { a, b in a.match.score < b.match.score }!
6363
let bestContentType = bestOption.contentType
64-
if case .incompatible = bestOption.match { throw RuntimeError.unexpectedContentTypeHeader(bestContentType) }
64+
if case .incompatible = bestOption.match {
65+
throw RuntimeError.unexpectedContentTypeHeader(
66+
expected: bestContentType,
67+
received: String(describing: received)
68+
)
69+
}
6570
return bestContentType
6671
}
6772

Sources/OpenAPIRuntime/Conversion/Converter+Server.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,47 @@ extension Converter {
214214
)
215215
}
216216

217+
/// Retrieves and decodes an optional XML-encoded request body and transforms it to a different type.
218+
///
219+
/// - Parameters:
220+
/// - type: The type to decode the request body into.
221+
/// - data: The HTTP request body to decode, or `nil` if the body is not present.
222+
/// - transform: A closure that transforms the decoded value to a different type.
223+
/// - Returns: The transformed value, or `nil` if the request body is not present or if decoding fails.
224+
/// - Throws: An error if there are issues decoding or transforming the request body.
225+
public func getOptionalRequestBodyAsXML<T: Decodable, C>(
226+
_ type: T.Type,
227+
from data: HTTPBody?,
228+
transforming transform: (T) -> C
229+
) async throws -> C? {
230+
try await getOptionalBufferingRequestBody(
231+
type,
232+
from: data,
233+
transforming: transform,
234+
convert: convertXMLToBodyCodable
235+
)
236+
}
237+
/// Retrieves and decodes a required XML-encoded request body and transforms it to a different type.
238+
///
239+
/// - Parameters:
240+
/// - type: The type to decode the request body into.
241+
/// - data: The HTTP request body to decode, or `nil` if the body is not present.
242+
/// - transform: A closure that transforms the decoded value to a different type.
243+
/// - Returns: The transformed value.
244+
/// - Throws: An error if the request body is not present, if decoding fails, or if there are issues transforming the request body.
245+
public func getRequiredRequestBodyAsXML<T: Decodable, C>(
246+
_ type: T.Type,
247+
from data: HTTPBody?,
248+
transforming transform: (T) -> C
249+
) async throws -> C {
250+
try await getRequiredBufferingRequestBody(
251+
type,
252+
from: data,
253+
transforming: transform,
254+
convert: convertXMLToBodyCodable
255+
)
256+
}
257+
217258
/// Retrieves and transforms an optional binary request body.
218259
///
219260
/// - Parameters:
@@ -347,6 +388,24 @@ extension Converter {
347388
convert: convertBodyCodableToJSON
348389
)
349390
}
391+
/// Sets the response body as XML data, serializing the provided value.
392+
///
393+
/// - Parameters:
394+
/// - value: The value to be serialized into the response body.
395+
/// - headerFields: The HTTP header fields to update with the new `contentType`.
396+
/// - contentType: The content type to set in the HTTP header fields.
397+
/// - Returns: An `HTTPBody` with the response body set as XML data.
398+
/// - Throws: An error if serialization or setting the response body fails.
399+
public func setResponseBodyAsXML<T: Encodable>(_ value: T, headerFields: inout HTTPFields, contentType: String)
400+
throws -> HTTPBody
401+
{
402+
try setResponseBody(
403+
value,
404+
headerFields: &headerFields,
405+
contentType: contentType,
406+
convert: convertBodyCodableToXML
407+
)
408+
}
350409

351410
/// Sets the response body as binary data.
352411
///

Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,32 @@ extension Converter {
146146
return HTTPBody(data)
147147
}
148148

149+
/// Returns a value decoded from a XML body.
150+
/// - Parameter body: The body containing the raw XML bytes.
151+
/// - Returns: A decoded value.
152+
/// - Throws: An error if decoding from the body fails.
153+
/// - Throws: An error if no custom coder is present for XML coding.
154+
func convertXMLToBodyCodable<T: Decodable>(_ body: HTTPBody) async throws -> T {
155+
guard let coder = configuration.xmlCoder else {
156+
throw RuntimeError.missingCoderForCustomContentType(contentType: OpenAPIMIMEType.xml.description)
157+
}
158+
let data = try await Data(collecting: body, upTo: .max)
159+
return try coder.customDecode(T.self, from: data)
160+
}
161+
162+
/// Returns a XML body for the provided encodable value.
163+
/// - Parameter value: The value to encode as XML.
164+
/// - Returns: The raw XML body.
165+
/// - Throws: An error if encoding to XML fails.
166+
/// - Throws: An error if no custom coder is present for XML coding.
167+
func convertBodyCodableToXML<T: Encodable>(_ value: T) throws -> HTTPBody {
168+
guard let coder = configuration.xmlCoder else {
169+
throw RuntimeError.missingCoderForCustomContentType(contentType: OpenAPIMIMEType.xml.description)
170+
}
171+
let data = try coder.customEncode(value)
172+
return HTTPBody(data)
173+
}
174+
149175
/// Returns a value decoded from a URL-encoded form body.
150176
/// - Parameter body: The body containing the raw URL-encoded form bytes.
151177
/// - Returns: A decoded value.

Sources/OpenAPIRuntime/Deprecated/Deprecated.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,19 @@ extension UndocumentedPayload {
2222
self.init(headerFields: [:], body: nil)
2323
}
2424
}
25+
26+
extension Configuration {
27+
/// Creates a new configuration with the specified values.
28+
///
29+
/// - Parameters:
30+
/// - dateTranscoder: The transcoder to use when converting between date
31+
/// and string values.
32+
/// - multipartBoundaryGenerator: The generator to use when creating mutlipart bodies.
33+
@available(*, deprecated, renamed: "init(dateTranscoder:multipartBoundaryGenerator:xmlCoder:)") @_disfavoredOverload
34+
public init(
35+
dateTranscoder: any DateTranscoder = .iso8601,
36+
multipartBoundaryGenerator: any MultipartBoundaryGenerator = .random
37+
) {
38+
self.init(dateTranscoder: dateTranscoder, multipartBoundaryGenerator: multipartBoundaryGenerator, xmlCoder: nil)
39+
}
40+
}

Sources/OpenAPIRuntime/Errors/RuntimeError.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret
2626

2727
// Data conversion
2828
case failedToDecodeStringConvertibleValue(type: String)
29+
case missingCoderForCustomContentType(contentType: String)
2930

3031
enum ParameterLocation: String, CustomStringConvertible {
3132
case query
@@ -36,7 +37,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret
3637

3738
// Headers
3839
case missingRequiredHeaderField(String)
39-
case unexpectedContentTypeHeader(String)
40+
case unexpectedContentTypeHeader(expected: String, received: String)
4041
case unexpectedAcceptHeader(String)
4142
case malformedAcceptHeader(String)
4243
case missingOrMalformedContentDispositionName
@@ -88,11 +89,14 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret
8889
case .invalidBase64String(let string):
8990
return "Invalid base64-encoded string (first 128 bytes): '\(string.prefix(128))'"
9091
case .failedToDecodeStringConvertibleValue(let string): return "Failed to decode a value of type '\(string)'."
92+
case .missingCoderForCustomContentType(let contentType):
93+
return "Missing custom coder for content type '\(contentType)'."
9194
case .unsupportedParameterStyle(name: let name, location: let location, style: let style, explode: let explode):
9295
return
9396
"Unsupported parameter style, parameter name: '\(name)', kind: \(location), style: \(style), explode: \(explode)"
9497
case .missingRequiredHeaderField(let name): return "The required header field named '\(name)' is missing."
95-
case .unexpectedContentTypeHeader(let contentType): return "Unexpected Content-Type header: \(contentType)"
98+
case .unexpectedContentTypeHeader(expected: let expected, received: let received):
99+
return "Unexpected content type, expected: \(expected), received: \(received)"
96100
case .unexpectedAcceptHeader(let accept): return "Unexpected Accept header: \(accept)"
97101
case .malformedAcceptHeader(let accept): return "Malformed Accept header: \(accept)"
98102
case .missingOrMalformedContentDispositionName:

Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,28 @@ final class Test_ClientConverterExtensions: Test_Runtime {
120120
try await XCTAssertEqualStringifiedData(body, testStructPrettyString)
121121
XCTAssertEqual(headerFields, [.contentType: "application/json", .contentLength: "23"])
122122
}
123+
// | client | set | request body | XML | optional | setOptionalRequestBodyAsXML |
124+
func test_setOptionalRequestBodyAsXML_codable() async throws {
125+
var headerFields: HTTPFields = [:]
126+
let body = try converter.setOptionalRequestBodyAsXML(
127+
testStruct,
128+
headerFields: &headerFields,
129+
contentType: "application/xml"
130+
)
131+
try await XCTAssertEqualStringifiedData(body, testStructString)
132+
XCTAssertEqual(headerFields, [.contentType: "application/xml", .contentLength: "17"])
133+
}
134+
// | client | set | request body | XML | required | setRequiredRequestBodyAsXML |
135+
func test_setRequiredRequestBodyAsXML_codable() async throws {
136+
var headerFields: HTTPFields = [:]
137+
let body = try converter.setRequiredRequestBodyAsXML(
138+
testStruct,
139+
headerFields: &headerFields,
140+
contentType: "application/xml"
141+
)
142+
try await XCTAssertEqualStringifiedData(body, testStructString)
143+
XCTAssertEqual(headerFields, [.contentType: "application/xml", .contentLength: "17"])
144+
}
123145

124146
// | client | set | request body | urlEncodedForm | codable | optional | setRequiredRequestBodyAsURLEncodedForm |
125147
func test_setOptionalRequestBodyAsURLEncodedForm_codable() async throws {
@@ -206,6 +228,15 @@ final class Test_ClientConverterExtensions: Test_Runtime {
206228
)
207229
XCTAssertEqual(value, testStruct)
208230
}
231+
// | client | get | response body | XML | required | getResponseBodyAsXML |
232+
func test_getResponseBodyAsXML_codable() async throws {
233+
let value: TestPet = try await converter.getResponseBodyAsXML(
234+
TestPet.self,
235+
from: .init(testStructData),
236+
transforming: { $0 }
237+
)
238+
XCTAssertEqual(value, testStruct)
239+
}
209240

210241
// | client | get | response body | binary | required | getResponseBodyAsBinary |
211242
func test_getResponseBodyAsBinary_data() async throws {

0 commit comments

Comments
 (0)