Skip to content

Commit 57bb711

Browse files
committed
Fix encoding of OpenAPI{Object,Value}Container to allow multiple encodings in anyOf/allOf
1 parent e535c55 commit 57bb711

File tree

3 files changed

+203
-41
lines changed

3 files changed

+203
-41
lines changed

Sources/OpenAPIRuntime/Base/OpenAPIValue.swift

Lines changed: 66 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,20 @@ public struct OpenAPIValueContainer: Codable, Hashable, Sendable {
114114
self.init(validatedValue: item)
115115
} else if let item = try? container.decode(String.self) {
116116
self.init(validatedValue: item)
117-
} else if let item = try? container.decode([OpenAPIValueContainer].self) {
118-
self.init(validatedValue: item.map(\.value))
119-
} else if let item = try? container.decode([String: OpenAPIValueContainer].self) {
120-
self.init(validatedValue: item.mapValues(\.value))
117+
} else if var container = try? decoder.unkeyedContainer() {
118+
var items: [(any Sendable)?] = []
119+
if let count = container.count { items.reserveCapacity(count) }
120+
while !container.isAtEnd {
121+
let item = try container.decode(OpenAPIValueContainer.self)
122+
items.append(item.value)
123+
}
124+
self.init(validatedValue: items)
125+
} else if let container = try? decoder.container(keyedBy: StringKey.self) {
126+
let keyValuePairs = try container.allKeys.map { key -> (String, (any Sendable)?) in
127+
let item = try container.decode(OpenAPIValueContainer.self, forKey: key)
128+
return (key.stringValue, item.value)
129+
}
130+
self.init(validatedValue: Dictionary(uniqueKeysWithValues: keyValuePairs))
121131
} else {
122132
throw DecodingError.dataCorruptedError(
123133
in: container,
@@ -133,36 +143,53 @@ public struct OpenAPIValueContainer: Codable, Hashable, Sendable {
133143
/// - Parameter encoder: The encoder to which the value should be encoded.
134144
/// - Throws: An error if the encoding process encounters issues or if the value is invalid.
135145
public func encode(to encoder: any Encoder) throws {
136-
var container = encoder.singleValueContainer()
137146
guard let value = value else {
147+
var container = encoder.singleValueContainer()
138148
try container.encodeNil()
139149
return
140150
}
141151
#if canImport(Foundation)
142152
if value is NSNull {
153+
var container = encoder.singleValueContainer()
143154
try container.encodeNil()
144155
return
145156
}
146157
#if canImport(CoreFoundation)
147158
if let nsNumber = value as? NSNumber {
159+
var container = encoder.singleValueContainer()
148160
try encode(nsNumber, to: &container)
149161
return
150162
}
151163
#endif
152164
#endif
153165
switch value {
154-
case let value as Bool: try container.encode(value)
155-
case let value as Int: try container.encode(value)
156-
case let value as Double: try container.encode(value)
157-
case let value as String: try container.encode(value)
166+
case let value as Bool:
167+
var container = encoder.singleValueContainer()
168+
try container.encode(value)
169+
case let value as Int:
170+
var container = encoder.singleValueContainer()
171+
try container.encode(value)
172+
case let value as Double:
173+
var container = encoder.singleValueContainer()
174+
try container.encode(value)
175+
case let value as String:
176+
var container = encoder.singleValueContainer()
177+
try container.encode(value)
158178
case let value as [(any Sendable)?]:
159-
try container.encode(value.map(OpenAPIValueContainer.init(validatedValue:)))
179+
var container = encoder.unkeyedContainer()
180+
for item in value {
181+
let containerItem = OpenAPIValueContainer(validatedValue: item)
182+
try container.encode(containerItem)
183+
}
160184
case let value as [String: (any Sendable)?]:
161-
try container.encode(value.mapValues(OpenAPIValueContainer.init(validatedValue:)))
185+
var container = encoder.container(keyedBy: StringKey.self)
186+
for (itemKey, itemValue) in value {
187+
try container.encode(OpenAPIValueContainer(validatedValue: itemValue), forKey: StringKey(itemKey))
188+
}
162189
default:
163190
throw EncodingError.invalidValue(
164191
value,
165-
.init(codingPath: container.codingPath, debugDescription: "OpenAPIValueContainer cannot be encoded")
192+
.init(codingPath: encoder.codingPath, debugDescription: "OpenAPIValueContainer cannot be encoded")
166193
)
167194
}
168195
}
@@ -357,36 +384,29 @@ public struct OpenAPIObjectContainer: Codable, Hashable, Sendable {
357384

358385
// MARK: Decodable
359386

360-
/// Creates an `OpenAPIValueContainer` by decoding it from a single-value container in a given decoder.
361-
///
362-
/// - Parameter decoder: The decoder used to decode the container.
363-
/// - Throws: An error if the decoding process encounters an issue or if the data does not match the expected format.
387+
// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
364388
public init(from decoder: any Decoder) throws {
365-
let container = try decoder.singleValueContainer()
366-
let item = try container.decode([String: OpenAPIValueContainer].self)
367-
self.init(validatedValue: item.mapValues(\.value))
389+
let container = try decoder.container(keyedBy: StringKey.self)
390+
let keyValuePairs = try container.allKeys.map { key -> (String, (any Sendable)?) in
391+
let item = try container.decode(OpenAPIValueContainer.self, forKey: key)
392+
return (key.stringValue, item.value)
393+
}
394+
self.init(validatedValue: Dictionary(uniqueKeysWithValues: keyValuePairs))
368395
}
369396

370397
// MARK: Encodable
371398

372-
/// Encodes the `OpenAPIValueContainer` into a format that can be stored or transmitted via the given encoder.
373-
///
374-
/// - Parameter encoder: The encoder used to perform the encoding.
375-
/// - Throws: An error if the encoding process encounters an issue or if the data does not match the expected format.
399+
// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
376400
public func encode(to encoder: any Encoder) throws {
377-
var container = encoder.singleValueContainer()
378-
try container.encode(value.mapValues(OpenAPIValueContainer.init(validatedValue:)))
401+
var container = encoder.container(keyedBy: StringKey.self)
402+
for (itemKey, itemValue) in value {
403+
try container.encode(OpenAPIValueContainer(validatedValue: itemValue), forKey: StringKey(itemKey))
404+
}
379405
}
380406

381407
// MARK: Equatable
382408

383-
/// Compares two `OpenAPIObjectContainer` instances for equality by comparing their inner key-value dictionaries.
384-
///
385-
/// - Parameters:
386-
/// - lhs: The left-hand side `OpenAPIObjectContainer` to compare.
387-
/// - rhs: The right-hand side `OpenAPIObjectContainer` to compare.
388-
///
389-
/// - Returns: `true` if the `OpenAPIObjectContainer` instances are equal, `false` otherwise.
409+
// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
390410
public static func == (lhs: OpenAPIObjectContainer, rhs: OpenAPIObjectContainer) -> Bool {
391411
let lv = lhs.value
392412
let rv = rhs.value
@@ -401,9 +421,7 @@ public struct OpenAPIObjectContainer: Codable, Hashable, Sendable {
401421

402422
// MARK: Hashable
403423

404-
/// Hashes the `OpenAPIObjectContainer` instance into the provided `Hasher`.
405-
///
406-
/// - Parameter hasher: The `Hasher` into which the hash value is combined.
424+
// swift-format-ignore: AllPublicDeclarationsHaveDocumentation
407425
public func hash(into hasher: inout Hasher) {
408426
for (key, itemValue) in value {
409427
hasher.combine(key)
@@ -474,9 +492,14 @@ public struct OpenAPIArrayContainer: Codable, Hashable, Sendable {
474492
/// - Parameter decoder: The decoder to use for decoding the array of values.
475493
/// - Throws: An error if the decoding process fails or if the decoded values cannot be validated.
476494
public init(from decoder: any Decoder) throws {
477-
let container = try decoder.singleValueContainer()
478-
let item = try container.decode([OpenAPIValueContainer].self)
479-
self.init(validatedValue: item.map(\.value))
495+
var container = try decoder.unkeyedContainer()
496+
var items: [(any Sendable)?] = []
497+
if let count = container.count { items.reserveCapacity(count) }
498+
while !container.isAtEnd {
499+
let item = try container.decode(OpenAPIValueContainer.self)
500+
items.append(item.value)
501+
}
502+
self.init(validatedValue: items)
480503
}
481504

482505
// MARK: Encodable
@@ -486,8 +509,11 @@ public struct OpenAPIArrayContainer: Codable, Hashable, Sendable {
486509
/// - Parameter encoder: The encoder to use for encoding the array of values.
487510
/// - Throws: An error if the encoding process fails.
488511
public func encode(to encoder: any Encoder) throws {
489-
var container = encoder.singleValueContainer()
490-
try container.encode(value.map(OpenAPIValueContainer.init(validatedValue:)))
512+
var container = encoder.unkeyedContainer()
513+
for item in value {
514+
let containerItem = OpenAPIValueContainer(validatedValue: item)
515+
try container.encode(containerItem)
516+
}
491517
}
492518

493519
// MARK: Equatable

Sources/OpenAPIRuntime/Conversion/CodableExtensions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@
142142
}
143143

144144
/// A freeform String coding key for decoding undocumented values.
145-
private struct StringKey: CodingKey, Hashable, Comparable {
145+
internal struct StringKey: CodingKey, Hashable, Comparable {
146146

147147
var stringValue: String
148148
var intValue: Int? { Int(stringValue) }

Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,82 @@ final class Test_OpenAPIValue: Test_Runtime {
216216
XCTAssertEqual(value["keyMore"] as? [Bool], [true])
217217
}
218218

219+
func testEncoding_anyOfObjects_success() throws {
220+
let values1: [String: (any Sendable)?] = ["key": "value"]
221+
let values2: [String: (any Sendable)?] = ["keyMore": [true]]
222+
let container = MyAnyOf2(
223+
value1: try OpenAPIObjectContainer(unvalidatedValue: values1),
224+
value2: try OpenAPIObjectContainer(unvalidatedValue: values2)
225+
)
226+
let expectedString = #"""
227+
{
228+
"key" : "value",
229+
"keyMore" : [
230+
true
231+
]
232+
}
233+
"""#
234+
try _testPrettyEncoded(container, expectedJSON: expectedString)
235+
}
236+
237+
func testDecoding_anyOfObjects_success() throws {
238+
let json = #"""
239+
{
240+
"key" : "value",
241+
"keyMore" : [
242+
true
243+
]
244+
}
245+
"""#
246+
let container: MyAnyOf2<OpenAPIObjectContainer, OpenAPIObjectContainer> = try _getDecoded(json: json)
247+
let value1 = container.value1?.value
248+
XCTAssertEqual(value1?.count, 2)
249+
XCTAssertEqual(value1?["key"] as? String, "value")
250+
XCTAssertEqual(value1?["keyMore"] as? [Bool], [true])
251+
let value2 = container.value2?.value
252+
XCTAssertEqual(value2?.count, 2)
253+
XCTAssertEqual(value2?["key"] as? String, "value")
254+
XCTAssertEqual(value2?["keyMore"] as? [Bool], [true])
255+
}
256+
257+
func testEncoding_anyOfValues_success() throws {
258+
let values1: [String: (any Sendable)?] = ["key": "value"]
259+
let values2: [String: (any Sendable)?] = ["keyMore": [true]]
260+
let container = MyAnyOf2(
261+
value1: try OpenAPIValueContainer(unvalidatedValue: values1),
262+
value2: try OpenAPIValueContainer(unvalidatedValue: values2)
263+
)
264+
let expectedString = #"""
265+
{
266+
"key" : "value",
267+
"keyMore" : [
268+
true
269+
]
270+
}
271+
"""#
272+
try _testPrettyEncoded(container, expectedJSON: expectedString)
273+
}
274+
275+
func testDecoding_anyOfValues_success() throws {
276+
let json = #"""
277+
{
278+
"key" : "value",
279+
"keyMore" : [
280+
true
281+
]
282+
}
283+
"""#
284+
let container: MyAnyOf2<OpenAPIValueContainer, OpenAPIValueContainer> = try _getDecoded(json: json)
285+
let value1 = try XCTUnwrap(container.value1?.value as? [String: (any Sendable)?])
286+
XCTAssertEqual(value1.count, 2)
287+
XCTAssertEqual(value1["key"] as? String, "value")
288+
XCTAssertEqual(value1["keyMore"] as? [Bool], [true])
289+
let value2 = try XCTUnwrap(container.value2?.value as? [String: (any Sendable)?])
290+
XCTAssertEqual(value2.count, 2)
291+
XCTAssertEqual(value2["key"] as? String, "value")
292+
XCTAssertEqual(value2["keyMore"] as? [Bool], [true])
293+
}
294+
219295
func testEncoding_array_success() throws {
220296
let values: [(any Sendable)?] = ["one", ["two": 2]]
221297
let container = try OpenAPIArrayContainer(unvalidatedValue: values)
@@ -246,6 +322,40 @@ final class Test_OpenAPIValue: Test_Runtime {
246322
XCTAssertEqual(value[1] as? [String: Int], ["two": 2])
247323
}
248324

325+
func testEncoding_arrayOfObjects_success() throws {
326+
let values: [(any Sendable)?] = [["one": 1], ["two": 2]]
327+
let container = try OpenAPIArrayContainer(unvalidatedValue: values)
328+
let expectedString = #"""
329+
[
330+
{
331+
"one" : 1
332+
},
333+
{
334+
"two" : 2
335+
}
336+
]
337+
"""#
338+
try _testPrettyEncoded(container, expectedJSON: expectedString)
339+
}
340+
341+
func testDecoding_arrayOfObjects_success() throws {
342+
let json = #"""
343+
[
344+
{
345+
"one" : 1
346+
},
347+
{
348+
"two" : 2
349+
}
350+
]
351+
"""#
352+
let container: OpenAPIArrayContainer = try _getDecoded(json: json)
353+
let value = container.value
354+
XCTAssertEqual(value.count, 2)
355+
XCTAssertEqual(value[0] as? [String: Int], ["one": 1])
356+
XCTAssertEqual(value[1] as? [String: Int], ["two": 2])
357+
}
358+
249359
func testEncoding_objectNested_success() throws {
250360
struct Foo: Encodable {
251361
var bar: String
@@ -334,3 +444,29 @@ final class Test_OpenAPIValue: Test_Runtime {
334444
)
335445
}
336446
}
447+
448+
struct MyAnyOf2<Value1: Codable & Hashable & Sendable, Value2: Codable & Hashable & Sendable>: Codable, Hashable,
449+
Sendable
450+
{
451+
var value1: Value1?
452+
var value2: Value2?
453+
init(value1: Value1? = nil, value2: Value2? = nil) {
454+
self.value1 = value1
455+
self.value2 = value2
456+
}
457+
public init(from decoder: any Decoder) throws {
458+
var errors: [any Error] = []
459+
do { self.value1 = try .init(from: decoder) } catch { errors.append(error) }
460+
do { self.value2 = try .init(from: decoder) } catch { errors.append(error) }
461+
try Swift.DecodingError.verifyAtLeastOneSchemaIsNotNil(
462+
[self.value1, self.value2],
463+
type: Self.self,
464+
codingPath: decoder.codingPath,
465+
errors: errors
466+
)
467+
}
468+
public func encode(to encoder: any Encoder) throws {
469+
try self.value1?.encode(to: encoder)
470+
try self.value2?.encode(to: encoder)
471+
}
472+
}

0 commit comments

Comments
 (0)