Skip to content

Commit c5ab4e8

Browse files
authored
Fix OpenAPIValueContainer serialization of nested values (#25)
Fix OpenAPIValueContainer serialization of nested values ### Motivation Credit goes to @rboyce for discovering this bug! When I saw the PR #24, I realized we didn't have any unit tests for the `OpenAPI*Container` types, which was an oversight. This PR adds those missing unit tests, and fixes the bug in casting that motivated #24. ### Modifications Adds unit tests and fixes the casting bug that expected wrapped, instead of raw stdlib values. ### Result Now all the new unit tests pass and serializing nested values in OpenAPIValueContainer and friends works correctly. ### Test Plan Most of this PR is new unit tests, verified they pass locally. Reviewed by: simonjbeaumont Builds: ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (docc test) - Build finished. ✔︎ pull request validation (integration test) - Build finished. ✔︎ pull request validation (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. ✖︎ pull request validation (api breakage) - Build finished. #25
1 parent 2080076 commit c5ab4e8

File tree

3 files changed

+217
-8
lines changed

3 files changed

+217
-8
lines changed

Sources/OpenAPIRuntime/Base/OpenAPIValue.swift

+8-8
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,9 @@ public struct OpenAPIValueContainer: Codable, Equatable, Hashable, Sendable {
139139
try container.encode(value)
140140
case let value as String:
141141
try container.encode(value)
142-
case let value as [OpenAPIValueContainer?]:
142+
case let value as [(any Sendable)?]:
143143
try container.encode(value.map(OpenAPIValueContainer.init(validatedValue:)))
144-
case let value as [String: OpenAPIValueContainer?]:
144+
case let value as [String: (any Sendable)?]:
145145
try container.encode(value.mapValues(OpenAPIValueContainer.init(validatedValue:)))
146146
default:
147147
throw EncodingError.invalidValue(
@@ -211,11 +211,11 @@ public struct OpenAPIValueContainer: Codable, Equatable, Hashable, Sendable {
211211
hasher.combine(value)
212212
case let value as String:
213213
hasher.combine(value)
214-
case let value as [any Sendable]:
214+
case let value as [(any Sendable)?]:
215215
for item in value {
216216
hasher.combine(OpenAPIValueContainer(validatedValue: item))
217217
}
218-
case let value as [String: any Sendable]:
218+
case let value as [String: (any Sendable)?]:
219219
for (key, itemValue) in value {
220220
hasher.combine(key)
221221
hasher.combine(OpenAPIValueContainer(validatedValue: itemValue))
@@ -301,7 +301,7 @@ public struct OpenAPIObjectContainer: Codable, Equatable, Hashable, Sendable {
301301
/// - Parameter unvalidatedValue: A dictionary with values of
302302
/// JSON-compatible types.
303303
/// - Throws: When the value is not supported.
304-
public init(unvalidatedValue: [String: Any?]) throws {
304+
public init(unvalidatedValue: [String: (any Sendable)?]) throws {
305305
try self.init(validatedValue: Self.tryCast(unvalidatedValue))
306306
}
307307

@@ -311,7 +311,7 @@ public struct OpenAPIObjectContainer: Codable, Equatable, Hashable, Sendable {
311311
/// - Parameter value: A dictionary with untyped values.
312312
/// - Returns: A cast dictionary if values are supported.
313313
/// - Throws: If an unsupported value is found.
314-
static func tryCast(_ value: [String: Any?]) throws -> [String: (any Sendable)?] {
314+
static func tryCast(_ value: [String: (any Sendable)?]) throws -> [String: (any Sendable)?] {
315315
return try value.mapValues(OpenAPIValueContainer.tryCast(_:))
316316
}
317317

@@ -405,7 +405,7 @@ public struct OpenAPIArrayContainer: Codable, Equatable, Hashable, Sendable {
405405
/// - Parameter unvalidatedValue: An array with values of JSON-compatible
406406
/// types.
407407
/// - Throws: When the value is not supported.
408-
public init(unvalidatedValue: [Any?]) throws {
408+
public init(unvalidatedValue: [(any Sendable)?]) throws {
409409
try self.init(validatedValue: Self.tryCast(unvalidatedValue))
410410
}
411411

@@ -414,7 +414,7 @@ public struct OpenAPIArrayContainer: Codable, Equatable, Hashable, Sendable {
414414
/// Returns the specified value cast to an array of supported values.
415415
/// - Parameter value: An array with untyped values.
416416
/// - Returns: A cast value if values are supported, nil otherwise.
417-
static func tryCast(_ value: [Any?]) throws -> [(any Sendable)?] {
417+
static func tryCast(_ value: [(any Sendable)?]) throws -> [(any Sendable)?] {
418418
return try value.map(OpenAPIValueContainer.tryCast(_:))
419419
}
420420

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
import XCTest
15+
@_spi(Generated) import OpenAPIRuntime
16+
17+
final class Test_OpenAPIValue: Test_Runtime {
18+
19+
func testValidationOnCreation() throws {
20+
_ = OpenAPIValueContainer("hello")
21+
_ = OpenAPIValueContainer(true)
22+
_ = OpenAPIValueContainer(1)
23+
_ = OpenAPIValueContainer(4.5)
24+
25+
_ = try OpenAPIValueContainer(unvalidatedValue: ["hello"])
26+
_ = try OpenAPIValueContainer(unvalidatedValue: ["hello": "world"])
27+
28+
_ = try OpenAPIObjectContainer(unvalidatedValue: ["hello": "world"])
29+
_ = try OpenAPIObjectContainer(unvalidatedValue: [
30+
"hello": ["nested": "world", "nested2": 2] as [String: any Sendable]
31+
])
32+
33+
_ = try OpenAPIArrayContainer(unvalidatedValue: ["hello"])
34+
_ = try OpenAPIArrayContainer(unvalidatedValue: ["hello", ["nestedHello", 2] as [any Sendable]])
35+
}
36+
37+
func testEncoding_container_success() throws {
38+
let values: [(any Sendable)?] = [
39+
nil,
40+
"Hello",
41+
[
42+
"key": "value",
43+
"anotherKey": [
44+
1,
45+
"two",
46+
] as [any Sendable],
47+
] as [String: any Sendable],
48+
1 as Int,
49+
2.5 as Double,
50+
[true],
51+
]
52+
let container = try OpenAPIValueContainer(unvalidatedValue: values)
53+
let expectedString = #"""
54+
[
55+
null,
56+
"Hello",
57+
{
58+
"anotherKey" : [
59+
1,
60+
"two"
61+
],
62+
"key" : "value"
63+
},
64+
1,
65+
2.5,
66+
[
67+
true
68+
]
69+
]
70+
"""#
71+
try _testPrettyEncoded(container, expectedJSON: expectedString)
72+
}
73+
74+
func testEncoding_container_failure() throws {
75+
struct Foobar: Equatable {}
76+
XCTAssertThrowsError(try OpenAPIValueContainer(unvalidatedValue: Foobar())) { error in
77+
let err = try! XCTUnwrap(error as? EncodingError)
78+
guard case let .invalidValue(value, context) = err else {
79+
XCTFail("Unexpected error")
80+
return
81+
}
82+
let typedValue = try! XCTUnwrap(value as? Foobar)
83+
XCTAssertEqual(typedValue, Foobar())
84+
XCTAssert(context.codingPath.isEmpty)
85+
XCTAssertNil(context.underlyingError)
86+
XCTAssertEqual(context.debugDescription, "Type 'Foobar' is not a supported OpenAPI value.")
87+
}
88+
}
89+
90+
func testDecoding_container_success() throws {
91+
let json = #"""
92+
[
93+
null,
94+
"Hello",
95+
{
96+
"anotherKey" : [
97+
1,
98+
"two"
99+
],
100+
"key" : "value"
101+
},
102+
1,
103+
2.5,
104+
[
105+
true
106+
]
107+
]
108+
"""#
109+
let container: OpenAPIValueContainer = try _getDecoded(json: json)
110+
let value = try XCTUnwrap(container.value)
111+
let array = try XCTUnwrap(value as? [(any Sendable)?])
112+
XCTAssertEqual(array.count, 6)
113+
XCTAssertNil(array[0])
114+
XCTAssertEqual(array[1] as? String, "Hello")
115+
let dict = try XCTUnwrap(array[2] as? [String: (any Sendable)?])
116+
XCTAssertEqual(dict.count, 2)
117+
let nestedArray = try XCTUnwrap(dict["anotherKey"] as? [(any Sendable)?])
118+
XCTAssertEqual(nestedArray.count, 2)
119+
XCTAssertEqual(nestedArray[0] as? Int, 1)
120+
XCTAssertEqual(nestedArray[1] as? String, "two")
121+
XCTAssertEqual(dict["key"] as? String, "value")
122+
XCTAssertEqual(array[3] as? Int, 1)
123+
XCTAssertEqual(array[4] as? Double, 2.5)
124+
let boolArray = try XCTUnwrap(array[5] as? [(any Sendable)?])
125+
XCTAssertEqual(boolArray.count, 1)
126+
XCTAssertEqual(boolArray[0] as? Bool, true)
127+
}
128+
129+
func testEncoding_object_success() throws {
130+
let values: [String: (any Sendable)?] = [
131+
"key": "value",
132+
"keyMore": [
133+
true
134+
],
135+
]
136+
let container = try OpenAPIObjectContainer(unvalidatedValue: values)
137+
let expectedString = #"""
138+
{
139+
"key" : "value",
140+
"keyMore" : [
141+
true
142+
]
143+
}
144+
"""#
145+
try _testPrettyEncoded(container, expectedJSON: expectedString)
146+
}
147+
148+
func testDecoding_object_success() throws {
149+
let json = #"""
150+
{
151+
"key" : "value",
152+
"keyMore" : [
153+
true
154+
]
155+
}
156+
"""#
157+
let container: OpenAPIObjectContainer = try _getDecoded(json: json)
158+
let value = container.value
159+
XCTAssertEqual(value.count, 2)
160+
XCTAssertEqual(value["key"] as? String, "value")
161+
XCTAssertEqual(value["keyMore"] as? [Bool], [true])
162+
}
163+
164+
func testEncoding_array_success() throws {
165+
let values: [(any Sendable)?] = [
166+
"one",
167+
["two": 2],
168+
]
169+
let container = try OpenAPIArrayContainer(unvalidatedValue: values)
170+
let expectedString = #"""
171+
[
172+
"one",
173+
{
174+
"two" : 2
175+
}
176+
]
177+
"""#
178+
try _testPrettyEncoded(container, expectedJSON: expectedString)
179+
}
180+
181+
func testDecoding_array_success() throws {
182+
let json = #"""
183+
[
184+
"one",
185+
{
186+
"two" : 2
187+
}
188+
]
189+
"""#
190+
let container: OpenAPIArrayContainer = try _getDecoded(json: json)
191+
let value = container.value
192+
XCTAssertEqual(value.count, 2)
193+
XCTAssertEqual(value[0] as? String, "one")
194+
XCTAssertEqual(value[1] as? [String: Int], ["two": 2])
195+
}
196+
}

Tests/OpenAPIRuntimeTests/Test_Runtime.swift

+13
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,19 @@ class Test_Runtime: XCTestCase {
9797
var testStructPrettyData: Data {
9898
Data(testStructPrettyString.utf8)
9999
}
100+
101+
func _testPrettyEncoded<Value: Encodable>(_ value: Value, expectedJSON: String) throws {
102+
let encoder = JSONEncoder()
103+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
104+
let data = try encoder.encode(value)
105+
XCTAssertEqual(String(data: data, encoding: .utf8)!, expectedJSON)
106+
}
107+
108+
func _getDecoded<Value: Decodable>(json: String) throws -> Value {
109+
let inputData = json.data(using: .utf8)!
110+
let decoder = JSONDecoder()
111+
return try decoder.decode(Value.self, from: inputData)
112+
}
100113
}
101114

102115
public func XCTAssertEqualURLString(_ lhs: URL?, _ rhs: String, file: StaticString = #file, line: UInt = #line) {

0 commit comments

Comments
 (0)