Skip to content

Commit f4f5963

Browse files
authored
[Runtime] Fix nested coding (#50)
[Runtime] Fix nested coding ### Motivation Fixes apple/swift-openapi-generator#263. ### Modifications Makes URIEncoder/URIDecoder able to handle custom Codable types in a single value container. For more details check out the associated generator [PR](apple/swift-openapi-generator#271). ### Result More Codable types can be handled. ### Test Plan Updated unit tests. Reviewed by: glbrntt Builds: ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (api breakage) - 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. #50
1 parent 9b003cc commit f4f5963

File tree

6 files changed

+152
-29
lines changed

6 files changed

+152
-29
lines changed

Sources/OpenAPIRuntime/Conversion/CodableExtensions.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ extension Decoder {
9090
return .init(uniqueKeysWithValues: keyValuePairs)
9191
}
9292

93+
/// Returns the decoded value by using a single value container.
94+
/// - Parameter type: The type to decode.
95+
/// - Returns: The decoded value.
96+
public func decodeFromSingleValueContainer<T: Decodable>(
97+
_ type: T.Type = T.self
98+
) throws -> T {
99+
let container = try singleValueContainer()
100+
return try container.decode(T.self)
101+
}
102+
93103
// MARK: - Private
94104

95105
/// Returns the keys in the given decoder that are not present
@@ -146,6 +156,29 @@ extension Encoder {
146156
try container.encode(value, forKey: .init(key))
147157
}
148158
}
159+
160+
/// Encodes the value into the encoder using a single value container.
161+
/// - Parameter value: The value to encode.
162+
public func encodeToSingleValueContainer<T: Encodable>(
163+
_ value: T
164+
) throws {
165+
var container = singleValueContainer()
166+
try container.encode(value)
167+
}
168+
169+
/// Encodes the first non-nil value from the provided array into
170+
/// the encoder using a single value container.
171+
/// - Parameter values: An array of optional values.
172+
public func encodeFirstNonNilValueToSingleValueContainer(
173+
_ values: [(any Encodable)?]
174+
) throws {
175+
for value in values {
176+
if let value {
177+
try encodeToSingleValueContainer(value)
178+
return
179+
}
180+
}
181+
}
149182
}
150183

151184
/// A freeform String coding key for decoding undocumented values.

Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder+Single.swift

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,27 @@ import Foundation
1717
/// A single value container used by `URIValueFromNodeDecoder`.
1818
struct URISingleValueDecodingContainer {
1919

20-
/// The coder used to serialize Date values.
21-
let dateTranscoder: any DateTranscoder
22-
23-
/// The coding path of the container.
24-
let codingPath: [any CodingKey]
25-
26-
/// The underlying value.
27-
let value: URIParsedValue
20+
/// The associated decoder.
21+
let decoder: URIValueFromNodeDecoder
2822
}
2923

3024
extension URISingleValueDecodingContainer {
3125

26+
/// The underlying value as a single value.
27+
var value: URIParsedValue {
28+
get throws {
29+
try decoder.currentElementAsSingleValue()
30+
}
31+
}
32+
3233
/// Returns the value found in the underlying node converted to
3334
/// the provided type.
3435
/// - Returns: The converted value found.
3536
/// - Throws: An error if the conversion failed.
3637
private func _decodeBinaryFloatingPoint<T: BinaryFloatingPoint>(
3738
_: T.Type = T.self
3839
) throws -> T {
39-
guard let double = Double(value) else {
40+
guard let double = try Double(value) else {
4041
throw DecodingError.typeMismatch(
4142
T.self,
4243
.init(
@@ -55,7 +56,7 @@ extension URISingleValueDecodingContainer {
5556
private func _decodeFixedWidthInteger<T: FixedWidthInteger>(
5657
_: T.Type = T.self
5758
) throws -> T {
58-
guard let parsedValue = T(value) else {
59+
guard let parsedValue = try T(value) else {
5960
throw DecodingError.typeMismatch(
6061
T.self,
6162
.init(
@@ -74,7 +75,7 @@ extension URISingleValueDecodingContainer {
7475
private func _decodeLosslessStringConvertible<T: LosslessStringConvertible>(
7576
_: T.Type = T.self
7677
) throws -> T {
77-
guard let parsedValue = T(String(value)) else {
78+
guard let parsedValue = try T(String(value)) else {
7879
throw DecodingError.typeMismatch(
7980
T.self,
8081
.init(
@@ -89,6 +90,10 @@ extension URISingleValueDecodingContainer {
8990

9091
extension URISingleValueDecodingContainer: SingleValueDecodingContainer {
9192

93+
var codingPath: [any CodingKey] {
94+
decoder.codingPath
95+
}
96+
9297
func decodeNil() -> Bool {
9398
false
9499
}
@@ -98,7 +103,7 @@ extension URISingleValueDecodingContainer: SingleValueDecodingContainer {
98103
}
99104

100105
func decode(_ type: String.Type) throws -> String {
101-
String(value)
106+
try String(value)
102107
}
103108

104109
func decode(_ type: Double.Type) throws -> Double {
@@ -180,9 +185,9 @@ extension URISingleValueDecodingContainer: SingleValueDecodingContainer {
180185
case is UInt64.Type:
181186
return try decode(UInt64.self) as! T
182187
case is Date.Type:
183-
return try dateTranscoder.decode(String(value)) as! T
188+
return try decoder.dateTranscoder.decode(String(value)) as! T
184189
default:
185-
throw URIValueFromNodeDecoder.GeneralError.unsupportedType(T.self)
190+
return try T.init(from: decoder)
186191
}
187192
}
188193
}

Sources/OpenAPIRuntime/URICoder/Decoding/URIValueFromNodeDecoder.swift

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,6 @@ extension URIValueFromNodeDecoder {
9898
/// A decoder error.
9999
enum GeneralError: Swift.Error {
100100

101-
/// The decoder does not support the provided type.
102-
case unsupportedType(Any.Type)
103-
104101
/// The decoder was asked to create a nested container.
105102
case nestedContainersNotSupported
106103

@@ -274,7 +271,7 @@ extension URIValueFromNodeDecoder {
274271
/// Extracts the node at the top of the coding stack and tries to treat it
275272
/// as a primitive value.
276273
/// - Returns: The value if it can be treated as a primitive value.
277-
private func currentElementAsSingleValue() throws -> URIParsedValue {
274+
func currentElementAsSingleValue() throws -> URIParsedValue {
278275
try nodeAsSingleValue(currentElement)
279276
}
280277

@@ -368,11 +365,6 @@ extension URIValueFromNodeDecoder: Decoder {
368365
}
369366

370367
func singleValueContainer() throws -> any SingleValueDecodingContainer {
371-
let value = try currentElementAsSingleValue()
372-
return URISingleValueDecodingContainer(
373-
dateTranscoder: dateTranscoder,
374-
codingPath: codingPath,
375-
value: value
376-
)
368+
return URISingleValueDecodingContainer(decoder: self)
377369
}
378370
}

Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder+Single.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ extension URISingleValueEncodingContainer: SingleValueEncodingContainer {
144144
case let value as Date:
145145
try _setValue(.date(value))
146146
default:
147-
throw URIValueToNodeEncoder.GeneralError.nestedValueInSingleValueContainer
147+
try value.encode(to: encoder)
148148
}
149149
}
150150
}

Sources/OpenAPIRuntime/URICoder/Encoding/URIValueToNodeEncoder.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@ final class URIValueToNodeEncoder {
4040

4141
/// The encoder set a value for an index out of range of the container.
4242
case integerOutOfRange
43-
44-
/// The encoder tried to treat
45-
case nestedValueInSingleValueContainer
4643
}
4744

4845
/// The stack of nested values within the root node.

Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414
import XCTest
15-
@testable import OpenAPIRuntime
15+
@_spi(Generated)@testable import OpenAPIRuntime
16+
#if os(Linux)
17+
@preconcurrency import Foundation
18+
#endif
1619

1720
final class Test_URICodingRoundtrip: Test_Runtime {
1821

@@ -27,12 +30,60 @@ final class Test_URICodingRoundtrip: Test_Runtime {
2730
var maybeFoo: String?
2831
}
2932

33+
struct TrivialStruct: Codable, Equatable {
34+
var foo: String
35+
}
36+
3037
enum SimpleEnum: String, Codable, Equatable {
3138
case red
3239
case green
3340
case blue
3441
}
3542

43+
struct AnyOf: Codable, Equatable, Sendable {
44+
var value1: Foundation.Date?
45+
var value2: SimpleEnum?
46+
var value3: TrivialStruct?
47+
init(value1: Foundation.Date? = nil, value2: SimpleEnum? = nil, value3: TrivialStruct? = nil) {
48+
self.value1 = value1
49+
self.value2 = value2
50+
self.value3 = value3
51+
}
52+
init(from decoder: any Decoder) throws {
53+
do {
54+
let container = try decoder.singleValueContainer()
55+
value1 = try? container.decode(Foundation.Date.self)
56+
}
57+
do {
58+
let container = try decoder.singleValueContainer()
59+
value2 = try? container.decode(SimpleEnum.self)
60+
}
61+
do {
62+
let container = try decoder.singleValueContainer()
63+
value3 = try? container.decode(TrivialStruct.self)
64+
}
65+
try DecodingError.verifyAtLeastOneSchemaIsNotNil(
66+
[value1, value2, value3],
67+
type: Self.self,
68+
codingPath: decoder.codingPath
69+
)
70+
}
71+
func encode(to encoder: any Encoder) throws {
72+
if let value1 {
73+
var container = encoder.singleValueContainer()
74+
try container.encode(value1)
75+
}
76+
if let value2 {
77+
var container = encoder.singleValueContainer()
78+
try container.encode(value2)
79+
}
80+
if let value3 {
81+
var container = encoder.singleValueContainer()
82+
try container.encode(value3)
83+
}
84+
}
85+
}
86+
3687
// An empty string.
3788
try _test(
3889
"",
@@ -210,6 +261,51 @@ final class Test_URICodingRoundtrip: Test_Runtime {
210261
)
211262
)
212263

264+
// A struct with a custom Codable implementation that forwards
265+
// decoding to nested values.
266+
try _test(
267+
AnyOf(
268+
value1: Date(timeIntervalSince1970: 1_674_036_251)
269+
),
270+
key: "root",
271+
.init(
272+
formExplode: "root=2023-01-18T10%3A04%3A11Z",
273+
formUnexplode: "root=2023-01-18T10%3A04%3A11Z",
274+
simpleExplode: "2023-01-18T10%3A04%3A11Z",
275+
simpleUnexplode: "2023-01-18T10%3A04%3A11Z",
276+
formDataExplode: "root=2023-01-18T10%3A04%3A11Z",
277+
formDataUnexplode: "root=2023-01-18T10%3A04%3A11Z"
278+
)
279+
)
280+
try _test(
281+
AnyOf(
282+
value2: .green
283+
),
284+
key: "root",
285+
.init(
286+
formExplode: "root=green",
287+
formUnexplode: "root=green",
288+
simpleExplode: "green",
289+
simpleUnexplode: "green",
290+
formDataExplode: "root=green",
291+
formDataUnexplode: "root=green"
292+
)
293+
)
294+
try _test(
295+
AnyOf(
296+
value3: .init(foo: "bar")
297+
),
298+
key: "root",
299+
.init(
300+
formExplode: "foo=bar",
301+
formUnexplode: "root=foo,bar",
302+
simpleExplode: "foo=bar",
303+
simpleUnexplode: "foo,bar",
304+
formDataExplode: "foo=bar",
305+
formDataUnexplode: "root=foo,bar"
306+
)
307+
)
308+
213309
// An empty struct.
214310
struct EmptyStruct: Codable, Equatable {}
215311
try _test(

0 commit comments

Comments
 (0)