diff --git a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift index ed14a00..83da78c 100644 --- a/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift +++ b/Sources/OpenAPIRuntime/Base/OpenAPIValue.swift @@ -114,10 +114,20 @@ public struct OpenAPIValueContainer: Codable, Hashable, Sendable { self.init(validatedValue: item) } else if let item = try? container.decode(String.self) { self.init(validatedValue: item) - } else if let item = try? container.decode([OpenAPIValueContainer].self) { - self.init(validatedValue: item.map(\.value)) - } else if let item = try? container.decode([String: OpenAPIValueContainer].self) { - self.init(validatedValue: item.mapValues(\.value)) + } else if var container = try? decoder.unkeyedContainer() { + var items: [(any Sendable)?] = [] + if let count = container.count { items.reserveCapacity(count) } + while !container.isAtEnd { + let item = try container.decode(OpenAPIValueContainer.self) + items.append(item.value) + } + self.init(validatedValue: items) + } else if let container = try? decoder.container(keyedBy: StringKey.self) { + let keyValuePairs = try container.allKeys.map { key -> (String, (any Sendable)?) in + let item = try container.decode(OpenAPIValueContainer.self, forKey: key) + return (key.stringValue, item.value) + } + self.init(validatedValue: Dictionary(uniqueKeysWithValues: keyValuePairs)) } else { throw DecodingError.dataCorruptedError( in: container, @@ -133,36 +143,53 @@ public struct OpenAPIValueContainer: Codable, Hashable, Sendable { /// - Parameter encoder: The encoder to which the value should be encoded. /// - Throws: An error if the encoding process encounters issues or if the value is invalid. public func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() guard let value = value else { + var container = encoder.singleValueContainer() try container.encodeNil() return } #if canImport(Foundation) if value is NSNull { + var container = encoder.singleValueContainer() try container.encodeNil() return } #if canImport(CoreFoundation) if let nsNumber = value as? NSNumber { + var container = encoder.singleValueContainer() try encode(nsNumber, to: &container) return } #endif #endif switch value { - case let value as Bool: try container.encode(value) - case let value as Int: try container.encode(value) - case let value as Double: try container.encode(value) - case let value as String: try container.encode(value) + case let value as Bool: + var container = encoder.singleValueContainer() + try container.encode(value) + case let value as Int: + var container = encoder.singleValueContainer() + try container.encode(value) + case let value as Double: + var container = encoder.singleValueContainer() + try container.encode(value) + case let value as String: + var container = encoder.singleValueContainer() + try container.encode(value) case let value as [(any Sendable)?]: - try container.encode(value.map(OpenAPIValueContainer.init(validatedValue:))) + var container = encoder.unkeyedContainer() + for item in value { + let containerItem = OpenAPIValueContainer(validatedValue: item) + try container.encode(containerItem) + } case let value as [String: (any Sendable)?]: - try container.encode(value.mapValues(OpenAPIValueContainer.init(validatedValue:))) + var container = encoder.container(keyedBy: StringKey.self) + for (itemKey, itemValue) in value { + try container.encode(OpenAPIValueContainer(validatedValue: itemValue), forKey: StringKey(itemKey)) + } default: throw EncodingError.invalidValue( value, - .init(codingPath: container.codingPath, debugDescription: "OpenAPIValueContainer cannot be encoded") + .init(codingPath: encoder.codingPath, debugDescription: "OpenAPIValueContainer cannot be encoded") ) } } @@ -357,36 +384,29 @@ public struct OpenAPIObjectContainer: Codable, Hashable, Sendable { // MARK: Decodable - /// Creates an `OpenAPIValueContainer` by decoding it from a single-value container in a given decoder. - /// - /// - Parameter decoder: The decoder used to decode the container. - /// - Throws: An error if the decoding process encounters an issue or if the data does not match the expected format. + // swift-format-ignore: AllPublicDeclarationsHaveDocumentation public init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - let item = try container.decode([String: OpenAPIValueContainer].self) - self.init(validatedValue: item.mapValues(\.value)) + let container = try decoder.container(keyedBy: StringKey.self) + let keyValuePairs = try container.allKeys.map { key -> (String, (any Sendable)?) in + let item = try container.decode(OpenAPIValueContainer.self, forKey: key) + return (key.stringValue, item.value) + } + self.init(validatedValue: Dictionary(uniqueKeysWithValues: keyValuePairs)) } // MARK: Encodable - /// Encodes the `OpenAPIValueContainer` into a format that can be stored or transmitted via the given encoder. - /// - /// - Parameter encoder: The encoder used to perform the encoding. - /// - Throws: An error if the encoding process encounters an issue or if the data does not match the expected format. + // swift-format-ignore: AllPublicDeclarationsHaveDocumentation public func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(value.mapValues(OpenAPIValueContainer.init(validatedValue:))) + var container = encoder.container(keyedBy: StringKey.self) + for (itemKey, itemValue) in value { + try container.encode(OpenAPIValueContainer(validatedValue: itemValue), forKey: StringKey(itemKey)) + } } // MARK: Equatable - /// Compares two `OpenAPIObjectContainer` instances for equality by comparing their inner key-value dictionaries. - /// - /// - Parameters: - /// - lhs: The left-hand side `OpenAPIObjectContainer` to compare. - /// - rhs: The right-hand side `OpenAPIObjectContainer` to compare. - /// - /// - Returns: `true` if the `OpenAPIObjectContainer` instances are equal, `false` otherwise. + // swift-format-ignore: AllPublicDeclarationsHaveDocumentation public static func == (lhs: OpenAPIObjectContainer, rhs: OpenAPIObjectContainer) -> Bool { let lv = lhs.value let rv = rhs.value @@ -401,9 +421,7 @@ public struct OpenAPIObjectContainer: Codable, Hashable, Sendable { // MARK: Hashable - /// Hashes the `OpenAPIObjectContainer` instance into the provided `Hasher`. - /// - /// - Parameter hasher: The `Hasher` into which the hash value is combined. + // swift-format-ignore: AllPublicDeclarationsHaveDocumentation public func hash(into hasher: inout Hasher) { for (key, itemValue) in value { hasher.combine(key) @@ -474,9 +492,14 @@ public struct OpenAPIArrayContainer: Codable, Hashable, Sendable { /// - Parameter decoder: The decoder to use for decoding the array of values. /// - Throws: An error if the decoding process fails or if the decoded values cannot be validated. public init(from decoder: any Decoder) throws { - let container = try decoder.singleValueContainer() - let item = try container.decode([OpenAPIValueContainer].self) - self.init(validatedValue: item.map(\.value)) + var container = try decoder.unkeyedContainer() + var items: [(any Sendable)?] = [] + if let count = container.count { items.reserveCapacity(count) } + while !container.isAtEnd { + let item = try container.decode(OpenAPIValueContainer.self) + items.append(item.value) + } + self.init(validatedValue: items) } // MARK: Encodable @@ -486,8 +509,11 @@ public struct OpenAPIArrayContainer: Codable, Hashable, Sendable { /// - Parameter encoder: The encoder to use for encoding the array of values. /// - Throws: An error if the encoding process fails. public func encode(to encoder: any Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(value.map(OpenAPIValueContainer.init(validatedValue:))) + var container = encoder.unkeyedContainer() + for item in value { + let containerItem = OpenAPIValueContainer(validatedValue: item) + try container.encode(containerItem) + } } // MARK: Equatable diff --git a/Sources/OpenAPIRuntime/Conversion/CodableExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CodableExtensions.swift index 6e9f5ed..e91954a 100644 --- a/Sources/OpenAPIRuntime/Conversion/CodableExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CodableExtensions.swift @@ -142,7 +142,7 @@ } /// A freeform String coding key for decoding undocumented values. -private struct StringKey: CodingKey, Hashable, Comparable { +internal struct StringKey: CodingKey, Hashable, Comparable { var stringValue: String var intValue: Int? { Int(stringValue) } diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift index 7146a26..f2591ff 100644 --- a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift +++ b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift @@ -216,6 +216,82 @@ final class Test_OpenAPIValue: Test_Runtime { XCTAssertEqual(value["keyMore"] as? [Bool], [true]) } + func testEncoding_anyOfObjects_success() throws { + let values1: [String: (any Sendable)?] = ["key": "value"] + let values2: [String: (any Sendable)?] = ["keyMore": [true]] + let container = MyAnyOf2( + value1: try OpenAPIObjectContainer(unvalidatedValue: values1), + value2: try OpenAPIObjectContainer(unvalidatedValue: values2) + ) + let expectedString = #""" + { + "key" : "value", + "keyMore" : [ + true + ] + } + """# + try _testPrettyEncoded(container, expectedJSON: expectedString) + } + + func testDecoding_anyOfObjects_success() throws { + let json = #""" + { + "key" : "value", + "keyMore" : [ + true + ] + } + """# + let container: MyAnyOf2 = try _getDecoded(json: json) + let value1 = container.value1?.value + XCTAssertEqual(value1?.count, 2) + XCTAssertEqual(value1?["key"] as? String, "value") + XCTAssertEqual(value1?["keyMore"] as? [Bool], [true]) + let value2 = container.value2?.value + XCTAssertEqual(value2?.count, 2) + XCTAssertEqual(value2?["key"] as? String, "value") + XCTAssertEqual(value2?["keyMore"] as? [Bool], [true]) + } + + func testEncoding_anyOfValues_success() throws { + let values1: [String: (any Sendable)?] = ["key": "value"] + let values2: [String: (any Sendable)?] = ["keyMore": [true]] + let container = MyAnyOf2( + value1: try OpenAPIValueContainer(unvalidatedValue: values1), + value2: try OpenAPIValueContainer(unvalidatedValue: values2) + ) + let expectedString = #""" + { + "key" : "value", + "keyMore" : [ + true + ] + } + """# + try _testPrettyEncoded(container, expectedJSON: expectedString) + } + + func testDecoding_anyOfValues_success() throws { + let json = #""" + { + "key" : "value", + "keyMore" : [ + true + ] + } + """# + let container: MyAnyOf2 = try _getDecoded(json: json) + let value1 = try XCTUnwrap(container.value1?.value as? [String: (any Sendable)?]) + XCTAssertEqual(value1.count, 2) + XCTAssertEqual(value1["key"] as? String, "value") + XCTAssertEqual(value1["keyMore"] as? [Bool], [true]) + let value2 = try XCTUnwrap(container.value2?.value as? [String: (any Sendable)?]) + XCTAssertEqual(value2.count, 2) + XCTAssertEqual(value2["key"] as? String, "value") + XCTAssertEqual(value2["keyMore"] as? [Bool], [true]) + } + func testEncoding_array_success() throws { let values: [(any Sendable)?] = ["one", ["two": 2]] let container = try OpenAPIArrayContainer(unvalidatedValue: values) @@ -246,6 +322,40 @@ final class Test_OpenAPIValue: Test_Runtime { XCTAssertEqual(value[1] as? [String: Int], ["two": 2]) } + func testEncoding_arrayOfObjects_success() throws { + let values: [(any Sendable)?] = [["one": 1], ["two": 2]] + let container = try OpenAPIArrayContainer(unvalidatedValue: values) + let expectedString = #""" + [ + { + "one" : 1 + }, + { + "two" : 2 + } + ] + """# + try _testPrettyEncoded(container, expectedJSON: expectedString) + } + + func testDecoding_arrayOfObjects_success() throws { + let json = #""" + [ + { + "one" : 1 + }, + { + "two" : 2 + } + ] + """# + let container: OpenAPIArrayContainer = try _getDecoded(json: json) + let value = container.value + XCTAssertEqual(value.count, 2) + XCTAssertEqual(value[0] as? [String: Int], ["one": 1]) + XCTAssertEqual(value[1] as? [String: Int], ["two": 2]) + } + func testEncoding_objectNested_success() throws { struct Foo: Encodable { var bar: String @@ -334,3 +444,29 @@ final class Test_OpenAPIValue: Test_Runtime { ) } } + +struct MyAnyOf2: Codable, Hashable, + Sendable +{ + var value1: Value1? + var value2: Value2? + init(value1: Value1? = nil, value2: Value2? = nil) { + self.value1 = value1 + self.value2 = value2 + } + public init(from decoder: any Decoder) throws { + var errors: [any Error] = [] + do { self.value1 = try .init(from: decoder) } catch { errors.append(error) } + do { self.value2 = try .init(from: decoder) } catch { errors.append(error) } + try Swift.DecodingError.verifyAtLeastOneSchemaIsNotNil( + [self.value1, self.value2], + type: Self.self, + codingPath: decoder.codingPath, + errors: errors + ) + } + public func encode(to encoder: any Encoder) throws { + try self.value1?.encode(to: encoder) + try self.value2?.encode(to: encoder) + } +}