Skip to content

Commit 9e62a21

Browse files
authored
[Generator] Include partial errors in oneOf/anyOf decoding errors (#350)
[Generator] Include partial errors in oneOf/anyOf decoding errors ### Motivation Fixes #275. Depends on apple/swift-openapi-runtime#66. ### Modifications Adapt the generator to emit code that collects individual errors during oneOf/anyOf decoding and includes them in the final error. ### Result Easier debugging of oneOf/anyOf decoding issues. ### Test Plan Adapted tests and verified that the errors look better now and include the partial errors. 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 (compatibility test) - 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. #350
1 parent 6057654 commit 9e62a21

File tree

10 files changed

+521
-269
lines changed

10 files changed

+521
-269
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ let package = Package(
6464
// Tests-only: Runtime library linked by generated code, and also
6565
// helps keep the runtime library new enough to work with the generated
6666
// code.
67-
.package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.5")),
67+
.package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.6")),
6868

6969
// Build and preview docs
7070
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),

Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,6 +1435,16 @@ extension Expression {
14351435
/// - Returns: A new expression with the `yield` keyword placed before the expression.
14361436
static func `yield`(_ expression: Expression) -> Self { .unaryKeyword(kind: .yield, expression: expression) }
14371437

1438+
/// Returns a new expression that puts the provided code blocks into
1439+
/// a do/catch block.
1440+
/// - Parameter:
1441+
/// - doStatement: The code blocks in the `do` statement body.
1442+
/// - catchBody: The code blocks in the `catch` statement.
1443+
/// - Returns: The expression.
1444+
static func `do`(_ doStatement: [CodeBlock], catchBody: [CodeBlock]? = nil) -> Self {
1445+
.doStatement(.init(doStatement: doStatement, catchBody: catchBody))
1446+
}
1447+
14381448
/// Returns a new value binding used in enums with associated values.
14391449
///
14401450
/// For example: `let foo(bar)`.

Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateCodable.swift

Lines changed: 75 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,15 @@ extension FileTranslator {
207207
func translateStructBlueprintAnyOfDecoder(properties: [(property: PropertyBlueprint, isKeyValuePair: Bool)])
208208
-> Declaration
209209
{
210-
let assignExprs: [Expression] = properties.map { (property, isKeyValuePair) in
210+
let errorArrayDecl: Declaration = .createErrorArrayDecl()
211+
let assignBlocks: [CodeBlock] = properties.map { (property, isKeyValuePair) in
211212
let decoderExpr: Expression =
212213
isKeyValuePair ? .initFromDecoderExpr() : .decodeFromSingleValueContainerExpr()
213-
return .assignment(left: .identifierPattern(property.swiftSafeName), right: .optionalTry(decoderExpr))
214+
let assignExpr: Expression = .assignment(
215+
left: .identifierPattern(property.swiftSafeName),
216+
right: .try(decoderExpr)
217+
)
218+
return .expression(assignExpr.wrapInDoCatchAppendArrayExpr())
214219
}
215220
let atLeastOneNotNilCheckExpr: Expression = .try(
216221
.identifierType(TypeName.decodingError).dot("verifyAtLeastOneSchemaIsNotNil")
@@ -220,9 +225,12 @@ extension FileTranslator {
220225
expression: .literal(.array(properties.map { .identifierPattern($0.property.swiftSafeName) }))
221226
), .init(label: "type", expression: .identifierPattern("Self").dot("self")),
222227
.init(label: "codingPath", expression: .identifierPattern("decoder").dot("codingPath")),
228+
.init(label: "errors", expression: .identifierPattern("errors")),
223229
])
224230
)
225-
return decoderInitializer(body: assignExprs.map { .expression($0) } + [.expression(atLeastOneNotNilCheckExpr)])
231+
return decoderInitializer(
232+
body: [.declaration(errorArrayDecl)] + assignBlocks + [.expression(atLeastOneNotNilCheckExpr)]
233+
)
226234
}
227235

228236
/// Returns a declaration of an anyOf encoder implementation.
@@ -254,37 +262,51 @@ extension FileTranslator {
254262
/// - Parameter cases: The names of the cases to be decoded.
255263
/// - Returns: A `Declaration` representing the `OneOf` decoder implementation.
256264
func translateOneOfWithoutDiscriminatorDecoder(cases: [(name: String, isKeyValuePair: Bool)]) -> Declaration {
265+
let errorArrayDecl: Declaration = .createErrorArrayDecl()
257266
let assignExprs: [Expression] = cases.map { (caseName, isKeyValuePair) in
258267
let decoderExpr: Expression =
259268
isKeyValuePair ? .initFromDecoderExpr() : .decodeFromSingleValueContainerExpr()
260-
return .doStatement(
261-
.init(
262-
doStatement: [
263-
.expression(
264-
.assignment(
265-
left: .identifierPattern("self"),
266-
right: .dot(caseName).call([.init(label: nil, expression: .try(decoderExpr))])
267-
)
268-
), .expression(.return()),
269-
],
270-
catchBody: []
271-
)
272-
)
269+
let body: [CodeBlock] = [
270+
.expression(
271+
.assignment(
272+
left: .identifierPattern("self"),
273+
right: .dot(caseName).call([.init(label: nil, expression: .try(decoderExpr))])
274+
)
275+
), .expression(.return()),
276+
]
277+
return body.wrapInDoCatchAppendArrayExpr()
273278
}
274-
275-
let otherExprs: [CodeBlock] = [.expression(translateOneOfDecoderThrowOnUnknownExpr())]
276-
return decoderInitializer(body: (assignExprs).map { .expression($0) } + otherExprs)
279+
let otherExprs: [CodeBlock] = [.expression(translateOneOfDecoderThrowOnNoCaseDecodedExpr())]
280+
return decoderInitializer(
281+
body: [.declaration(errorArrayDecl)] + (assignExprs).map { .expression($0) } + otherExprs
282+
)
277283
}
278284

285+
/// Returns an expression that throws an error when a oneOf discriminator
286+
/// failed to match any known cases.
287+
func translateOneOfDecoderThrowOnUnknownExpr(discriminatorSwiftName: String) -> Expression {
288+
.unaryKeyword(
289+
kind: .throw,
290+
expression: .identifierType(TypeName.decodingError).dot("unknownOneOfDiscriminator")
291+
.call([
292+
.init(
293+
label: "discriminatorKey",
294+
expression: .identifierPattern(Constants.Codable.codingKeysName).dot(discriminatorSwiftName)
295+
), .init(label: "discriminatorValue", expression: .identifierPattern("discriminator")),
296+
.init(label: "codingPath", expression: .identifierPattern("decoder").dot("codingPath")),
297+
])
298+
)
299+
}
279300
/// Returns an expression that throws an error when a oneOf failed
280301
/// to match any documented cases.
281-
func translateOneOfDecoderThrowOnUnknownExpr() -> Expression {
302+
func translateOneOfDecoderThrowOnNoCaseDecodedExpr() -> Expression {
282303
.unaryKeyword(
283304
kind: .throw,
284305
expression: .identifierType(TypeName.decodingError).dot("failedToDecodeOneOfSchema")
285306
.call([
286307
.init(label: "type", expression: .identifierPattern("Self").dot("self")),
287308
.init(label: "codingPath", expression: .identifierPattern("decoder").dot("codingPath")),
309+
.init(label: "errors", expression: .identifierPattern("errors")),
288310
])
289311
)
290312
}
@@ -311,7 +333,9 @@ extension FileTranslator {
311333
]
312334
)
313335
}
314-
let otherExprs: [CodeBlock] = [.expression(translateOneOfDecoderThrowOnUnknownExpr())]
336+
let otherExprs: [CodeBlock] = [
337+
.expression(translateOneOfDecoderThrowOnUnknownExpr(discriminatorSwiftName: discriminatorName))
338+
]
315339
let body: [CodeBlock] = [
316340
.declaration(.decoderContainerOfKeysVar()),
317341
.declaration(
@@ -408,6 +432,31 @@ fileprivate extension Expression {
408432
static func decodeFromSingleValueContainerExpr() -> Expression {
409433
.identifierPattern("decoder").dot("decodeFromSingleValueContainer").call([])
410434
}
435+
/// Returns a new expression that wraps the provided expression in
436+
/// a do/catch block where the error is appended to an array.
437+
///
438+
/// Assumes the existence of an "errors" variable in the current scope.
439+
/// - Returns: The expression.
440+
func wrapInDoCatchAppendArrayExpr() -> Expression { [CodeBlock.expression(self)].wrapInDoCatchAppendArrayExpr() }
441+
}
442+
443+
fileprivate extension Array where Element == CodeBlock {
444+
/// Returns a new expression that wraps the provided code blocks in
445+
/// a do/catch block where the error is appended to an array.
446+
///
447+
/// Assumes the existence of an "errors" variable in the current scope.
448+
/// - Returns: The expression.
449+
func wrapInDoCatchAppendArrayExpr() -> Expression {
450+
.do(
451+
self,
452+
catchBody: [
453+
.expression(
454+
.identifierPattern("errors").dot("append")
455+
.call([.init(label: nil, expression: .identifierPattern("error"))])
456+
)
457+
]
458+
)
459+
}
411460
}
412461

413462
fileprivate extension Declaration {
@@ -424,6 +473,11 @@ fileprivate extension Declaration {
424473
)
425474
)
426475
}
476+
/// Creates a new declaration that creates a local array of errors.
477+
/// - Returns: The declaration.
478+
static func createErrorArrayDecl() -> Declaration {
479+
.variable(kind: .var, left: "errors", type: .array(.any(.member("Error"))), right: .literal(.array([])))
480+
}
427481
}
428482

429483
fileprivate extension FileTranslator {

Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift

Lines changed: 37 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -214,29 +214,24 @@ extension ServerFileTranslator {
214214
.declaration(.variable(kind: .let, left: bodyVariableName, type: .init(requestBody.typeUsage)))
215215
)
216216

217-
func makeIfBranch(typedContent: TypedSchemaContent, isFirstBranch: Bool) -> IfBranch {
218-
let isMatchingContentTypeExpr: Expression = .identifierPattern("converter").dot("isMatchingContentType")
219-
.call([
220-
.init(label: "received", expression: .identifierPattern("contentType")),
221-
.init(
222-
label: "expectedRaw",
223-
expression: .literal(typedContent.content.contentType.headerValueForValidation)
224-
),
225-
])
226-
let condition: Expression
227-
if isFirstBranch {
228-
condition = .binaryOperation(
229-
left: .binaryOperation(
230-
left: .identifierPattern("contentType"),
231-
operation: .equals,
232-
right: .literal(.nil)
233-
),
234-
operation: .booleanOr,
235-
right: isMatchingContentTypeExpr
236-
)
237-
} else {
238-
condition = isMatchingContentTypeExpr
239-
}
217+
let typedContents = requestBody.contents
218+
let contentTypeOptions = typedContents.map { typedContent in
219+
typedContent.content.contentType.headerValueForValidation
220+
}
221+
let chosenContentTypeDecl: Declaration = .variable(
222+
kind: .let,
223+
left: "chosenContentType",
224+
right: .try(
225+
.identifierPattern("converter").dot("bestContentType")
226+
.call([
227+
.init(label: "received", expression: .identifierPattern("contentType")),
228+
.init(label: "options", expression: .literal(.array(contentTypeOptions.map { .literal($0) }))),
229+
])
230+
)
231+
)
232+
codeBlocks.append(.declaration(chosenContentTypeDecl))
233+
234+
func makeCase(typedContent: TypedSchemaContent) -> SwitchCaseDescription {
240235
let contentTypeUsage = typedContent.resolvedTypeUsage
241236
let content = typedContent.content
242237
let contentType = content.contentType
@@ -264,35 +259,34 @@ extension ServerFileTranslator {
264259
} else {
265260
bodyExpr = .try(.await(converterExpr))
266261
}
262+
let bodyAssignExpr: Expression = .assignment(left: .identifierPattern("body"), right: bodyExpr)
267263
return .init(
268-
condition: .try(condition),
269-
body: [.expression(.assignment(left: .identifierPattern("body"), right: bodyExpr))]
264+
kind: .case(.literal(typedContent.content.contentType.headerValueForValidation)),
265+
body: [.expression(bodyAssignExpr)]
270266
)
271267
}
272268

273-
let typedContents = requestBody.contents
274-
275-
let primaryIfBranch = makeIfBranch(typedContent: typedContents[0], isFirstBranch: true)
276-
let elseIfBranches = typedContents.dropFirst()
277-
.map { typedContent in makeIfBranch(typedContent: typedContent, isFirstBranch: false) }
278-
279-
codeBlocks.append(
280-
.expression(
281-
.ifStatement(
282-
ifBranch: primaryIfBranch,
283-
elseIfBranches: elseIfBranches,
284-
elseBody: [
269+
let cases = typedContents.map(makeCase)
270+
let switchExpr: Expression = .switch(
271+
switchedExpression: .identifierPattern("chosenContentType"),
272+
cases: cases + [
273+
.init(
274+
kind: .default,
275+
body: [
285276
.expression(
286-
.unaryKeyword(
287-
kind: .throw,
288-
expression: .identifierPattern("converter").dot("makeUnexpectedContentTypeError")
289-
.call([.init(label: "contentType", expression: .identifierPattern("contentType"))])
290-
)
277+
.identifierPattern("preconditionFailure")
278+
.call([
279+
.init(
280+
label: nil,
281+
expression: .literal("bestContentType chose an invalid content type.")
282+
)
283+
])
291284
)
292285
]
293286
)
294-
)
287+
]
295288
)
289+
codeBlocks.append(.expression(switchExpr))
296290
return codeBlocks
297291
}
298292
}

Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift

Lines changed: 39 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -191,29 +191,26 @@ extension ClientFileTranslator {
191191
let bodyDecl: Declaration = .variable(kind: .let, left: "body", type: .init(bodyTypeName))
192192
codeBlocks.append(.declaration(bodyDecl))
193193

194-
func makeIfBranch(typedContent: TypedSchemaContent, isFirstBranch: Bool) -> IfBranch {
195-
let isMatchingContentTypeExpr: Expression = .identifierPattern("converter").dot("isMatchingContentType")
196-
.call([
197-
.init(label: "received", expression: .identifierPattern("contentType")),
198-
.init(
199-
label: "expectedRaw",
200-
expression: .literal(typedContent.content.contentType.headerValueForValidation)
201-
),
202-
])
203-
let condition: Expression
204-
if isFirstBranch {
205-
condition = .binaryOperation(
206-
left: .binaryOperation(
207-
left: .identifierPattern("contentType"),
208-
operation: .equals,
209-
right: .literal(.nil)
210-
),
211-
operation: .booleanOr,
212-
right: isMatchingContentTypeExpr
213-
)
214-
} else {
215-
condition = isMatchingContentTypeExpr
216-
}
194+
let contentTypeOptions = typedContents.map { typedContent in
195+
typedContent.content.contentType.headerValueForValidation
196+
}
197+
let chosenContentTypeDecl: Declaration = .variable(
198+
kind: .let,
199+
left: "chosenContentType",
200+
right: .try(
201+
.identifierPattern("converter").dot("bestContentType")
202+
.call([
203+
.init(label: "received", expression: .identifierPattern("contentType")),
204+
.init(
205+
label: "options",
206+
expression: .literal(.array(contentTypeOptions.map { .literal($0) }))
207+
),
208+
])
209+
)
210+
)
211+
codeBlocks.append(.declaration(chosenContentTypeDecl))
212+
213+
func makeCase(typedContent: TypedSchemaContent) -> SwitchCaseDescription {
217214
let contentTypeUsage = typedContent.resolvedTypeUsage
218215
let transformExpr: Expression = .closureInvocation(
219216
argumentNames: ["value"],
@@ -238,36 +235,33 @@ extension ClientFileTranslator {
238235
} else {
239236
bodyExpr = .try(.await(converterExpr))
240237
}
238+
let bodyAssignExpr: Expression = .assignment(left: .identifierPattern("body"), right: bodyExpr)
241239
return .init(
242-
condition: .try(condition),
243-
body: [.expression(.assignment(left: .identifierPattern("body"), right: bodyExpr))]
240+
kind: .case(.literal(typedContent.content.contentType.headerValueForValidation)),
241+
body: [.expression(bodyAssignExpr)]
244242
)
245243
}
246-
247-
let primaryIfBranch = makeIfBranch(typedContent: typedContents[0], isFirstBranch: true)
248-
let elseIfBranches = typedContents.dropFirst()
249-
.map { typedContent in makeIfBranch(typedContent: typedContent, isFirstBranch: false) }
250-
251-
codeBlocks.append(
252-
.expression(
253-
.ifStatement(
254-
ifBranch: primaryIfBranch,
255-
elseIfBranches: elseIfBranches,
256-
elseBody: [
244+
let cases = typedContents.map(makeCase)
245+
let switchExpr: Expression = .switch(
246+
switchedExpression: .identifierPattern("chosenContentType"),
247+
cases: cases + [
248+
.init(
249+
kind: .default,
250+
body: [
257251
.expression(
258-
.unaryKeyword(
259-
kind: .throw,
260-
expression: .identifierPattern("converter").dot("makeUnexpectedContentTypeError")
261-
.call([
262-
.init(label: "contentType", expression: .identifierPattern("contentType"))
263-
])
264-
)
252+
.identifierPattern("preconditionFailure")
253+
.call([
254+
.init(
255+
label: nil,
256+
expression: .literal("bestContentType chose an invalid content type.")
257+
)
258+
])
265259
)
266260
]
267261
)
268-
)
262+
]
269263
)
270-
264+
codeBlocks.append(.expression(switchExpr))
271265
bodyVarExpr = .identifierPattern("body")
272266
} else {
273267
bodyVarExpr = nil

0 commit comments

Comments
 (0)