Skip to content

Commit a51b3bd

Browse files
authored
[Runtime] Include partial errors in oneOf/anyOf decoding errors (#66)
[Runtime] Include partial errors in oneOf/anyOf decoding errors ### Motivation The runtime changes to address apple/swift-openapi-generator#275. This makes debugging of decoding of oneOf/anyOf much easier, as the individual errors aren't dropped on the floor anymore. ### Modifications Added SPI that allows the generated code to collect and report partial errors when a oneOf/anyOf fails to decode (that includes trying multiple subschemas, which themselves emit errors when they're not the right match). ### Result Easier debugging of oneOf/anyOf decoding issues. ### Test Plan Tested manually as part of the generator changes, we don't generally test exact error strings. Reviewed by: simonjbeaumont Builds: ✔︎ pull request validation (5.10) - Build finished. ✔︎ 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. #66
1 parent 333d73a commit a51b3bd

File tree

3 files changed

+150
-13
lines changed

3 files changed

+150
-13
lines changed

Sources/OpenAPIRuntime/Conversion/ErrorExtensions.swift

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,19 @@ extension DecodingError {
2222
/// occurred.
2323
/// - codingPath: The coding path to the decoder that attempted to decode
2424
/// the type.
25+
/// - errors: The errors encountered when decoding individual cases.
2526
/// - Returns: A decoding error.
2627
static func failedToDecodeAnySchema(
2728
type: Any.Type,
28-
codingPath: [any CodingKey]
29+
codingPath: [any CodingKey],
30+
errors: [any Error]
2931
) -> Self {
3032
DecodingError.valueNotFound(
3133
type,
3234
DecodingError.Context.init(
3335
codingPath: codingPath,
34-
debugDescription: "The anyOf structure did not decode into any child schema."
36+
debugDescription: "The anyOf structure did not decode into any child schema.",
37+
underlyingError: MultiError(errors: errors)
3538
)
3639
)
3740
}
@@ -43,24 +46,47 @@ extension DecodingError {
4346
/// occurred.
4447
/// - codingPath: The coding path to the decoder that attempted to decode
4548
/// the type.
49+
/// - errors: The errors encountered when decoding individual cases.
4650
/// - Returns: A decoding error.
4751
@_spi(Generated)
4852
public static func failedToDecodeOneOfSchema(
4953
type: Any.Type,
50-
codingPath: [any CodingKey]
54+
codingPath: [any CodingKey],
55+
errors: [any Error]
5156
) -> Self {
5257
DecodingError.valueNotFound(
5358
type,
5459
DecodingError.Context.init(
5560
codingPath: codingPath,
56-
debugDescription: "The oneOf structure did not decode into any child schema."
61+
debugDescription: "The oneOf structure did not decode into any child schema.",
62+
underlyingError: MultiError(errors: errors)
5763
)
5864
)
5965
}
60-
}
6166

62-
@_spi(Generated)
63-
extension DecodingError {
67+
/// Returns a decoding error used by the oneOf decoder when
68+
/// the discriminator property contains an unknown schema name.
69+
/// - Parameters:
70+
/// - discriminatorKey: The discriminator coding key.
71+
/// - discriminatorValue: The unknown value of the discriminator.
72+
/// - codingPath: The coding path to the decoder that attempted to decode
73+
/// the type, with the discriminator value as the last component.
74+
/// - Returns: A decoding error.
75+
@_spi(Generated)
76+
public static func unknownOneOfDiscriminator(
77+
discriminatorKey: any CodingKey,
78+
discriminatorValue: String,
79+
codingPath: [any CodingKey]
80+
) -> Self {
81+
return DecodingError.keyNotFound(
82+
discriminatorKey,
83+
DecodingError.Context.init(
84+
codingPath: codingPath,
85+
debugDescription:
86+
"The oneOf structure does not contain the provided discriminator value '\(discriminatorValue)'."
87+
)
88+
)
89+
}
6490

6591
/// Verifies that the anyOf decoder successfully decoded at least one
6692
/// child schema, and throws an error otherwise.
@@ -70,17 +96,49 @@ extension DecodingError {
7096
/// occurred.
7197
/// - codingPath: The coding path to the decoder that attempted to decode
7298
/// the type.
99+
/// - errors: The errors encountered when decoding individual cases.
73100
/// - Throws: An error of type `DecodingError.failedToDecodeAnySchema` if none of the child schemas were successfully decoded.
101+
@_spi(Generated)
74102
public static func verifyAtLeastOneSchemaIsNotNil(
75103
_ values: [Any?],
76104
type: Any.Type,
77-
codingPath: [any CodingKey]
105+
codingPath: [any CodingKey],
106+
errors: [any Error]
78107
) throws {
79108
guard values.contains(where: { $0 != nil }) else {
80109
throw DecodingError.failedToDecodeAnySchema(
81110
type: type,
82-
codingPath: codingPath
111+
codingPath: codingPath,
112+
errors: errors
83113
)
84114
}
85115
}
86116
}
117+
118+
/// A wrapper of multiple errors, for example collected during a parallelized
119+
/// operation from the individual subtasks.
120+
struct MultiError: Swift.Error, LocalizedError, CustomStringConvertible {
121+
122+
/// The multiple underlying errors.
123+
var errors: [any Error]
124+
125+
var description: String {
126+
let combinedDescription =
127+
errors
128+
.map { error in
129+
guard let error = error as? (any PrettyStringConvertible) else {
130+
return error.localizedDescription
131+
}
132+
return error.prettyDescription
133+
}
134+
.enumerated()
135+
.map { ($0.offset + 1, $0.element) }
136+
.map { "Error \($0.0): [\($0.1)]" }
137+
.joined(separator: ", ")
138+
return "MultiError (contains \(errors.count) error\(errors.count == 1 ? "" : "s")): \(combinedDescription)"
139+
}
140+
141+
var errorDescription: String? {
142+
description
143+
}
144+
}

Sources/OpenAPIRuntime/Deprecated/Deprecated.swift

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,74 @@ extension Converter {
143143
}
144144
}
145145
}
146+
147+
extension DecodingError {
148+
/// Returns a decoding error used by the oneOf decoder when not a single
149+
/// child schema decodes the received payload.
150+
/// - Parameters:
151+
/// - type: The type representing the oneOf schema in which the decoding
152+
/// occurred.
153+
/// - codingPath: The coding path to the decoder that attempted to decode
154+
/// the type.
155+
/// - Returns: A decoding error.
156+
@_spi(Generated)
157+
@available(*, deprecated)
158+
public static func failedToDecodeOneOfSchema(
159+
type: Any.Type,
160+
codingPath: [any CodingKey]
161+
) -> Self {
162+
DecodingError.valueNotFound(
163+
type,
164+
DecodingError.Context.init(
165+
codingPath: codingPath,
166+
debugDescription: "The oneOf structure did not decode into any child schema."
167+
)
168+
)
169+
}
170+
171+
/// Returns a decoding error used by the anyOf decoder when not a single
172+
/// child schema decodes the received payload.
173+
/// - Parameters:
174+
/// - type: The type representing the anyOf schema in which the decoding
175+
/// occurred.
176+
/// - codingPath: The coding path to the decoder that attempted to decode
177+
/// the type.
178+
/// - Returns: A decoding error.
179+
@available(*, deprecated)
180+
static func failedToDecodeAnySchema(
181+
type: Any.Type,
182+
codingPath: [any CodingKey]
183+
) -> Self {
184+
DecodingError.valueNotFound(
185+
type,
186+
DecodingError.Context.init(
187+
codingPath: codingPath,
188+
debugDescription: "The anyOf structure did not decode into any child schema."
189+
)
190+
)
191+
}
192+
193+
/// Verifies that the anyOf decoder successfully decoded at least one
194+
/// child schema, and throws an error otherwise.
195+
/// - Parameters:
196+
/// - values: An array of optional values to check.
197+
/// - type: The type representing the anyOf schema in which the decoding
198+
/// occurred.
199+
/// - codingPath: The coding path to the decoder that attempted to decode
200+
/// the type.
201+
/// - Throws: An error of type `DecodingError.failedToDecodeAnySchema` if none of the child schemas were successfully decoded.
202+
@_spi(Generated)
203+
@available(*, deprecated)
204+
public static func verifyAtLeastOneSchemaIsNotNil(
205+
_ values: [Any?],
206+
type: Any.Type,
207+
codingPath: [any CodingKey]
208+
) throws {
209+
guard values.contains(where: { $0 != nil }) else {
210+
throw DecodingError.failedToDecodeAnySchema(
211+
type: type,
212+
codingPath: codingPath
213+
)
214+
}
215+
}
216+
}

Tests/OpenAPIRuntimeTests/URICoder/Test_URICodingRoundtrip.swift

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,22 +50,30 @@ final class Test_URICodingRoundtrip: Test_Runtime {
5050
self.value3 = value3
5151
}
5252
init(from decoder: any Decoder) throws {
53+
var errors: [any Error] = []
5354
do {
5455
let container = try decoder.singleValueContainer()
55-
value1 = try? container.decode(Foundation.Date.self)
56+
value1 = try container.decode(Foundation.Date.self)
57+
} catch {
58+
errors.append(error)
5659
}
5760
do {
5861
let container = try decoder.singleValueContainer()
59-
value2 = try? container.decode(SimpleEnum.self)
62+
value2 = try container.decode(SimpleEnum.self)
63+
} catch {
64+
errors.append(error)
6065
}
6166
do {
6267
let container = try decoder.singleValueContainer()
63-
value3 = try? container.decode(TrivialStruct.self)
68+
value3 = try container.decode(TrivialStruct.self)
69+
} catch {
70+
errors.append(error)
6471
}
6572
try DecodingError.verifyAtLeastOneSchemaIsNotNil(
6673
[value1, value2, value3],
6774
type: Self.self,
68-
codingPath: decoder.codingPath
75+
codingPath: decoder.codingPath,
76+
errors: errors
6977
)
7078
}
7179
func encode(to encoder: any Encoder) throws {

0 commit comments

Comments
 (0)