Skip to content

Fix encoding of OpenAPI{Object,Value}Container to allow multiple encodings in anyOf/allOf #156

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
106 changes: 66 additions & 40 deletions Sources/OpenAPIRuntime/Base/OpenAPIValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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")
)
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Sources/OpenAPIRuntime/Conversion/CodableExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down
136 changes: 136 additions & 0 deletions Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<OpenAPIObjectContainer, OpenAPIObjectContainer> = 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<OpenAPIValueContainer, OpenAPIValueContainer> = 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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -334,3 +444,29 @@ final class Test_OpenAPIValue: Test_Runtime {
)
}
}

struct MyAnyOf2<Value1: Codable & Hashable & Sendable, Value2: Codable & Hashable & Sendable>: 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)
}
}
Loading