From 9e62a21c5cd08f7d2485f4d5c8c7337dee17546c Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 30 Oct 2023 17:15:59 +0100 Subject: [PATCH] [Generator] Include partial errors in oneOf/anyOf decoding errors (#350) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Generator] Include partial errors in oneOf/anyOf decoding errors ### Motivation Fixes https://github.com/apple/swift-openapi-generator/issues/275. Depends on https://github.com/apple/swift-openapi-runtime/pull/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. https://github.com/apple/swift-openapi-generator/pull/350 --- Package.swift | 2 +- .../StructuredSwiftRepresentation.swift | 10 + .../CommonTranslations/translateCodable.swift | 96 +++++++-- .../RequestBody/translateRequestBody.swift | 80 ++++---- .../Responses/translateResponseOutcome.swift | 84 ++++---- .../ReferenceSources/Petstore/Client.swift | 138 ++++++++----- .../ReferenceSources/Petstore/Server.swift | 82 ++++---- .../ReferenceSources/Petstore/Types.swift | 106 +++++++--- .../SnippetBasedReferenceTests.swift | 186 +++++++++++++----- Tests/PetstoreConsumerTests/Test_Types.swift | 6 + 10 files changed, 521 insertions(+), 269 deletions(-) diff --git a/Package.swift b/Package.swift index d9909727..c9a92bd2 100644 --- a/Package.swift +++ b/Package.swift @@ -64,7 +64,7 @@ let package = Package( // Tests-only: Runtime library linked by generated code, and also // helps keep the runtime library new enough to work with the generated // code. - .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.5")), + .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.6")), // Build and preview docs .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), diff --git a/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift b/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift index 56b10e4f..03128e84 100644 --- a/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift +++ b/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift @@ -1435,6 +1435,16 @@ extension Expression { /// - Returns: A new expression with the `yield` keyword placed before the expression. static func `yield`(_ expression: Expression) -> Self { .unaryKeyword(kind: .yield, expression: expression) } + /// Returns a new expression that puts the provided code blocks into + /// a do/catch block. + /// - Parameter: + /// - doStatement: The code blocks in the `do` statement body. + /// - catchBody: The code blocks in the `catch` statement. + /// - Returns: The expression. + static func `do`(_ doStatement: [CodeBlock], catchBody: [CodeBlock]? = nil) -> Self { + .doStatement(.init(doStatement: doStatement, catchBody: catchBody)) + } + /// Returns a new value binding used in enums with associated values. /// /// For example: `let foo(bar)`. diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateCodable.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateCodable.swift index 5c48ad96..90eb6eb2 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateCodable.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateCodable.swift @@ -207,10 +207,15 @@ extension FileTranslator { func translateStructBlueprintAnyOfDecoder(properties: [(property: PropertyBlueprint, isKeyValuePair: Bool)]) -> Declaration { - let assignExprs: [Expression] = properties.map { (property, isKeyValuePair) in + let errorArrayDecl: Declaration = .createErrorArrayDecl() + let assignBlocks: [CodeBlock] = properties.map { (property, isKeyValuePair) in let decoderExpr: Expression = isKeyValuePair ? .initFromDecoderExpr() : .decodeFromSingleValueContainerExpr() - return .assignment(left: .identifierPattern(property.swiftSafeName), right: .optionalTry(decoderExpr)) + let assignExpr: Expression = .assignment( + left: .identifierPattern(property.swiftSafeName), + right: .try(decoderExpr) + ) + return .expression(assignExpr.wrapInDoCatchAppendArrayExpr()) } let atLeastOneNotNilCheckExpr: Expression = .try( .identifierType(TypeName.decodingError).dot("verifyAtLeastOneSchemaIsNotNil") @@ -220,9 +225,12 @@ extension FileTranslator { expression: .literal(.array(properties.map { .identifierPattern($0.property.swiftSafeName) })) ), .init(label: "type", expression: .identifierPattern("Self").dot("self")), .init(label: "codingPath", expression: .identifierPattern("decoder").dot("codingPath")), + .init(label: "errors", expression: .identifierPattern("errors")), ]) ) - return decoderInitializer(body: assignExprs.map { .expression($0) } + [.expression(atLeastOneNotNilCheckExpr)]) + return decoderInitializer( + body: [.declaration(errorArrayDecl)] + assignBlocks + [.expression(atLeastOneNotNilCheckExpr)] + ) } /// Returns a declaration of an anyOf encoder implementation. @@ -254,37 +262,51 @@ extension FileTranslator { /// - Parameter cases: The names of the cases to be decoded. /// - Returns: A `Declaration` representing the `OneOf` decoder implementation. func translateOneOfWithoutDiscriminatorDecoder(cases: [(name: String, isKeyValuePair: Bool)]) -> Declaration { + let errorArrayDecl: Declaration = .createErrorArrayDecl() let assignExprs: [Expression] = cases.map { (caseName, isKeyValuePair) in let decoderExpr: Expression = isKeyValuePair ? .initFromDecoderExpr() : .decodeFromSingleValueContainerExpr() - return .doStatement( - .init( - doStatement: [ - .expression( - .assignment( - left: .identifierPattern("self"), - right: .dot(caseName).call([.init(label: nil, expression: .try(decoderExpr))]) - ) - ), .expression(.return()), - ], - catchBody: [] - ) - ) + let body: [CodeBlock] = [ + .expression( + .assignment( + left: .identifierPattern("self"), + right: .dot(caseName).call([.init(label: nil, expression: .try(decoderExpr))]) + ) + ), .expression(.return()), + ] + return body.wrapInDoCatchAppendArrayExpr() } - - let otherExprs: [CodeBlock] = [.expression(translateOneOfDecoderThrowOnUnknownExpr())] - return decoderInitializer(body: (assignExprs).map { .expression($0) } + otherExprs) + let otherExprs: [CodeBlock] = [.expression(translateOneOfDecoderThrowOnNoCaseDecodedExpr())] + return decoderInitializer( + body: [.declaration(errorArrayDecl)] + (assignExprs).map { .expression($0) } + otherExprs + ) } + /// Returns an expression that throws an error when a oneOf discriminator + /// failed to match any known cases. + func translateOneOfDecoderThrowOnUnknownExpr(discriminatorSwiftName: String) -> Expression { + .unaryKeyword( + kind: .throw, + expression: .identifierType(TypeName.decodingError).dot("unknownOneOfDiscriminator") + .call([ + .init( + label: "discriminatorKey", + expression: .identifierPattern(Constants.Codable.codingKeysName).dot(discriminatorSwiftName) + ), .init(label: "discriminatorValue", expression: .identifierPattern("discriminator")), + .init(label: "codingPath", expression: .identifierPattern("decoder").dot("codingPath")), + ]) + ) + } /// Returns an expression that throws an error when a oneOf failed /// to match any documented cases. - func translateOneOfDecoderThrowOnUnknownExpr() -> Expression { + func translateOneOfDecoderThrowOnNoCaseDecodedExpr() -> Expression { .unaryKeyword( kind: .throw, expression: .identifierType(TypeName.decodingError).dot("failedToDecodeOneOfSchema") .call([ .init(label: "type", expression: .identifierPattern("Self").dot("self")), .init(label: "codingPath", expression: .identifierPattern("decoder").dot("codingPath")), + .init(label: "errors", expression: .identifierPattern("errors")), ]) ) } @@ -311,7 +333,9 @@ extension FileTranslator { ] ) } - let otherExprs: [CodeBlock] = [.expression(translateOneOfDecoderThrowOnUnknownExpr())] + let otherExprs: [CodeBlock] = [ + .expression(translateOneOfDecoderThrowOnUnknownExpr(discriminatorSwiftName: discriminatorName)) + ] let body: [CodeBlock] = [ .declaration(.decoderContainerOfKeysVar()), .declaration( @@ -408,6 +432,31 @@ fileprivate extension Expression { static func decodeFromSingleValueContainerExpr() -> Expression { .identifierPattern("decoder").dot("decodeFromSingleValueContainer").call([]) } + /// Returns a new expression that wraps the provided expression in + /// a do/catch block where the error is appended to an array. + /// + /// Assumes the existence of an "errors" variable in the current scope. + /// - Returns: The expression. + func wrapInDoCatchAppendArrayExpr() -> Expression { [CodeBlock.expression(self)].wrapInDoCatchAppendArrayExpr() } +} + +fileprivate extension Array where Element == CodeBlock { + /// Returns a new expression that wraps the provided code blocks in + /// a do/catch block where the error is appended to an array. + /// + /// Assumes the existence of an "errors" variable in the current scope. + /// - Returns: The expression. + func wrapInDoCatchAppendArrayExpr() -> Expression { + .do( + self, + catchBody: [ + .expression( + .identifierPattern("errors").dot("append") + .call([.init(label: nil, expression: .identifierPattern("error"))]) + ) + ] + ) + } } fileprivate extension Declaration { @@ -424,6 +473,11 @@ fileprivate extension Declaration { ) ) } + /// Creates a new declaration that creates a local array of errors. + /// - Returns: The declaration. + static func createErrorArrayDecl() -> Declaration { + .variable(kind: .var, left: "errors", type: .array(.any(.member("Error"))), right: .literal(.array([]))) + } } fileprivate extension FileTranslator { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift index f90b485b..06c183b2 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift @@ -214,29 +214,24 @@ extension ServerFileTranslator { .declaration(.variable(kind: .let, left: bodyVariableName, type: .init(requestBody.typeUsage))) ) - func makeIfBranch(typedContent: TypedSchemaContent, isFirstBranch: Bool) -> IfBranch { - let isMatchingContentTypeExpr: Expression = .identifierPattern("converter").dot("isMatchingContentType") - .call([ - .init(label: "received", expression: .identifierPattern("contentType")), - .init( - label: "expectedRaw", - expression: .literal(typedContent.content.contentType.headerValueForValidation) - ), - ]) - let condition: Expression - if isFirstBranch { - condition = .binaryOperation( - left: .binaryOperation( - left: .identifierPattern("contentType"), - operation: .equals, - right: .literal(.nil) - ), - operation: .booleanOr, - right: isMatchingContentTypeExpr - ) - } else { - condition = isMatchingContentTypeExpr - } + let typedContents = requestBody.contents + let contentTypeOptions = typedContents.map { typedContent in + typedContent.content.contentType.headerValueForValidation + } + let chosenContentTypeDecl: Declaration = .variable( + kind: .let, + left: "chosenContentType", + right: .try( + .identifierPattern("converter").dot("bestContentType") + .call([ + .init(label: "received", expression: .identifierPattern("contentType")), + .init(label: "options", expression: .literal(.array(contentTypeOptions.map { .literal($0) }))), + ]) + ) + ) + codeBlocks.append(.declaration(chosenContentTypeDecl)) + + func makeCase(typedContent: TypedSchemaContent) -> SwitchCaseDescription { let contentTypeUsage = typedContent.resolvedTypeUsage let content = typedContent.content let contentType = content.contentType @@ -264,35 +259,34 @@ extension ServerFileTranslator { } else { bodyExpr = .try(.await(converterExpr)) } + let bodyAssignExpr: Expression = .assignment(left: .identifierPattern("body"), right: bodyExpr) return .init( - condition: .try(condition), - body: [.expression(.assignment(left: .identifierPattern("body"), right: bodyExpr))] + kind: .case(.literal(typedContent.content.contentType.headerValueForValidation)), + body: [.expression(bodyAssignExpr)] ) } - let typedContents = requestBody.contents - - let primaryIfBranch = makeIfBranch(typedContent: typedContents[0], isFirstBranch: true) - let elseIfBranches = typedContents.dropFirst() - .map { typedContent in makeIfBranch(typedContent: typedContent, isFirstBranch: false) } - - codeBlocks.append( - .expression( - .ifStatement( - ifBranch: primaryIfBranch, - elseIfBranches: elseIfBranches, - elseBody: [ + let cases = typedContents.map(makeCase) + let switchExpr: Expression = .switch( + switchedExpression: .identifierPattern("chosenContentType"), + cases: cases + [ + .init( + kind: .default, + body: [ .expression( - .unaryKeyword( - kind: .throw, - expression: .identifierPattern("converter").dot("makeUnexpectedContentTypeError") - .call([.init(label: "contentType", expression: .identifierPattern("contentType"))]) - ) + .identifierPattern("preconditionFailure") + .call([ + .init( + label: nil, + expression: .literal("bestContentType chose an invalid content type.") + ) + ]) ) ] ) - ) + ] ) + codeBlocks.append(.expression(switchExpr)) return codeBlocks } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift index 3cb7d6d3..264257af 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift @@ -191,29 +191,26 @@ extension ClientFileTranslator { let bodyDecl: Declaration = .variable(kind: .let, left: "body", type: .init(bodyTypeName)) codeBlocks.append(.declaration(bodyDecl)) - func makeIfBranch(typedContent: TypedSchemaContent, isFirstBranch: Bool) -> IfBranch { - let isMatchingContentTypeExpr: Expression = .identifierPattern("converter").dot("isMatchingContentType") - .call([ - .init(label: "received", expression: .identifierPattern("contentType")), - .init( - label: "expectedRaw", - expression: .literal(typedContent.content.contentType.headerValueForValidation) - ), - ]) - let condition: Expression - if isFirstBranch { - condition = .binaryOperation( - left: .binaryOperation( - left: .identifierPattern("contentType"), - operation: .equals, - right: .literal(.nil) - ), - operation: .booleanOr, - right: isMatchingContentTypeExpr - ) - } else { - condition = isMatchingContentTypeExpr - } + let contentTypeOptions = typedContents.map { typedContent in + typedContent.content.contentType.headerValueForValidation + } + let chosenContentTypeDecl: Declaration = .variable( + kind: .let, + left: "chosenContentType", + right: .try( + .identifierPattern("converter").dot("bestContentType") + .call([ + .init(label: "received", expression: .identifierPattern("contentType")), + .init( + label: "options", + expression: .literal(.array(contentTypeOptions.map { .literal($0) })) + ), + ]) + ) + ) + codeBlocks.append(.declaration(chosenContentTypeDecl)) + + func makeCase(typedContent: TypedSchemaContent) -> SwitchCaseDescription { let contentTypeUsage = typedContent.resolvedTypeUsage let transformExpr: Expression = .closureInvocation( argumentNames: ["value"], @@ -238,36 +235,33 @@ extension ClientFileTranslator { } else { bodyExpr = .try(.await(converterExpr)) } + let bodyAssignExpr: Expression = .assignment(left: .identifierPattern("body"), right: bodyExpr) return .init( - condition: .try(condition), - body: [.expression(.assignment(left: .identifierPattern("body"), right: bodyExpr))] + kind: .case(.literal(typedContent.content.contentType.headerValueForValidation)), + body: [.expression(bodyAssignExpr)] ) } - - let primaryIfBranch = makeIfBranch(typedContent: typedContents[0], isFirstBranch: true) - let elseIfBranches = typedContents.dropFirst() - .map { typedContent in makeIfBranch(typedContent: typedContent, isFirstBranch: false) } - - codeBlocks.append( - .expression( - .ifStatement( - ifBranch: primaryIfBranch, - elseIfBranches: elseIfBranches, - elseBody: [ + let cases = typedContents.map(makeCase) + let switchExpr: Expression = .switch( + switchedExpression: .identifierPattern("chosenContentType"), + cases: cases + [ + .init( + kind: .default, + body: [ .expression( - .unaryKeyword( - kind: .throw, - expression: .identifierPattern("converter").dot("makeUnexpectedContentTypeError") - .call([ - .init(label: "contentType", expression: .identifierPattern("contentType")) - ]) - ) + .identifierPattern("preconditionFailure") + .call([ + .init( + label: nil, + expression: .literal("bestContentType chose an invalid content type.") + ) + ]) ) ] ) - ) + ] ) - + codeBlocks.append(.expression(switchExpr)) bodyVarExpr = .identifierPattern("body") } else { bodyVarExpr = nil diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift index 7c698711..7d445daf 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift @@ -117,10 +117,14 @@ public struct Client: APIProtocol { ) let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) let body: Operations.listPets.Output.Ok.Body - if try contentType == nil || converter.isMatchingContentType( + let chosenContentType = try converter.bestContentType( received: contentType, - expectedRaw: "application/json" - ) { + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": body = try await converter.getResponseBodyAsJSON( Components.Schemas.Pets.self, from: responseBody, @@ -128,8 +132,8 @@ public struct Client: APIProtocol { .json(value) } ) - } else { - throw converter.makeUnexpectedContentTypeError(contentType: contentType) + default: + preconditionFailure("bestContentType chose an invalid content type.") } return .ok(.init( headers: headers, @@ -138,10 +142,14 @@ public struct Client: APIProtocol { default: let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) let body: Operations.listPets.Output.Default.Body - if try contentType == nil || converter.isMatchingContentType( + let chosenContentType = try converter.bestContentType( received: contentType, - expectedRaw: "application/json" - ) { + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": body = try await converter.getResponseBodyAsJSON( Components.Schemas._Error.self, from: responseBody, @@ -149,8 +157,8 @@ public struct Client: APIProtocol { .json(value) } ) - } else { - throw converter.makeUnexpectedContentTypeError(contentType: contentType) + default: + preconditionFailure("bestContentType chose an invalid content type.") } return .`default`( statusCode: response.status.code, @@ -208,10 +216,14 @@ public struct Client: APIProtocol { )) let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) let body: Operations.createPet.Output.Created.Body - if try contentType == nil || converter.isMatchingContentType( + let chosenContentType = try converter.bestContentType( received: contentType, - expectedRaw: "application/json" - ) { + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": body = try await converter.getResponseBodyAsJSON( Components.Schemas.Pet.self, from: responseBody, @@ -219,8 +231,8 @@ public struct Client: APIProtocol { .json(value) } ) - } else { - throw converter.makeUnexpectedContentTypeError(contentType: contentType) + default: + preconditionFailure("bestContentType chose an invalid content type.") } return .created(.init( headers: headers, @@ -234,10 +246,14 @@ public struct Client: APIProtocol { )) let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) let body: Components.Responses.ErrorBadRequest.Body - if try contentType == nil || converter.isMatchingContentType( + let chosenContentType = try converter.bestContentType( received: contentType, - expectedRaw: "application/json" - ) { + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": body = try await converter.getResponseBodyAsJSON( Components.Responses.ErrorBadRequest.Body.jsonPayload.self, from: responseBody, @@ -245,8 +261,8 @@ public struct Client: APIProtocol { .json(value) } ) - } else { - throw converter.makeUnexpectedContentTypeError(contentType: contentType) + default: + preconditionFailure("bestContentType chose an invalid content type.") } return .clientError( statusCode: response.status.code, @@ -333,10 +349,16 @@ public struct Client: APIProtocol { case 200: let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) let body: Operations.getStats.Output.Ok.Body - if try contentType == nil || converter.isMatchingContentType( + let chosenContentType = try converter.bestContentType( received: contentType, - expectedRaw: "application/json" - ) { + options: [ + "application/json", + "text/plain", + "application/octet-stream" + ] + ) + switch chosenContentType { + case "application/json": body = try await converter.getResponseBodyAsJSON( Components.Schemas.PetStats.self, from: responseBody, @@ -344,10 +366,7 @@ public struct Client: APIProtocol { .json(value) } ) - } else if try converter.isMatchingContentType( - received: contentType, - expectedRaw: "text/plain" - ) { + case "text/plain": body = try converter.getResponseBodyAsBinary( OpenAPIRuntime.HTTPBody.self, from: responseBody, @@ -355,10 +374,7 @@ public struct Client: APIProtocol { .plainText(value) } ) - } else if try converter.isMatchingContentType( - received: contentType, - expectedRaw: "application/octet-stream" - ) { + case "application/octet-stream": body = try converter.getResponseBodyAsBinary( OpenAPIRuntime.HTTPBody.self, from: responseBody, @@ -366,8 +382,8 @@ public struct Client: APIProtocol { .binary(value) } ) - } else { - throw converter.makeUnexpectedContentTypeError(contentType: contentType) + default: + preconditionFailure("bestContentType chose an invalid content type.") } return .ok(.init(body: body)) default: @@ -506,10 +522,14 @@ public struct Client: APIProtocol { case 400: let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) let body: Operations.updatePet.Output.BadRequest.Body - if try contentType == nil || converter.isMatchingContentType( + let chosenContentType = try converter.bestContentType( received: contentType, - expectedRaw: "application/json" - ) { + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": body = try await converter.getResponseBodyAsJSON( Operations.updatePet.Output.BadRequest.Body.jsonPayload.self, from: responseBody, @@ -517,8 +537,8 @@ public struct Client: APIProtocol { .json(value) } ) - } else { - throw converter.makeUnexpectedContentTypeError(contentType: contentType) + default: + preconditionFailure("bestContentType chose an invalid content type.") } return .badRequest(.init(body: body)) default: @@ -570,10 +590,14 @@ public struct Client: APIProtocol { case 200: let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) let body: Operations.uploadAvatarForPet.Output.Ok.Body - if try contentType == nil || converter.isMatchingContentType( + let chosenContentType = try converter.bestContentType( received: contentType, - expectedRaw: "application/octet-stream" - ) { + options: [ + "application/octet-stream" + ] + ) + switch chosenContentType { + case "application/octet-stream": body = try converter.getResponseBodyAsBinary( OpenAPIRuntime.HTTPBody.self, from: responseBody, @@ -581,17 +605,21 @@ public struct Client: APIProtocol { .binary(value) } ) - } else { - throw converter.makeUnexpectedContentTypeError(contentType: contentType) + default: + preconditionFailure("bestContentType chose an invalid content type.") } return .ok(.init(body: body)) case 412: let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) let body: Operations.uploadAvatarForPet.Output.PreconditionFailed.Body - if try contentType == nil || converter.isMatchingContentType( + let chosenContentType = try converter.bestContentType( received: contentType, - expectedRaw: "application/json" - ) { + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": body = try await converter.getResponseBodyAsJSON( Swift.String.self, from: responseBody, @@ -599,17 +627,21 @@ public struct Client: APIProtocol { .json(value) } ) - } else { - throw converter.makeUnexpectedContentTypeError(contentType: contentType) + default: + preconditionFailure("bestContentType chose an invalid content type.") } return .preconditionFailed(.init(body: body)) case 500: let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) let body: Operations.uploadAvatarForPet.Output.InternalServerError.Body - if try contentType == nil || converter.isMatchingContentType( + let chosenContentType = try converter.bestContentType( received: contentType, - expectedRaw: "text/plain" - ) { + options: [ + "text/plain" + ] + ) + switch chosenContentType { + case "text/plain": body = try converter.getResponseBodyAsBinary( OpenAPIRuntime.HTTPBody.self, from: responseBody, @@ -617,8 +649,8 @@ public struct Client: APIProtocol { .plainText(value) } ) - } else { - throw converter.makeUnexpectedContentTypeError(contentType: contentType) + default: + preconditionFailure("bestContentType chose an invalid content type.") } return .internalServerError(.init(body: body)) default: diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift index d33ed532..143304b0 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift @@ -266,10 +266,14 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { ) let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) let body: Operations.createPet.Input.Body - if try contentType == nil || converter.isMatchingContentType( + let chosenContentType = try converter.bestContentType( received: contentType, - expectedRaw: "application/json" - ) { + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": body = try await converter.getRequiredRequestBodyAsJSON( Components.Schemas.CreatePetRequest.self, from: requestBody, @@ -277,8 +281,8 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { .json(value) } ) - } else { - throw converter.makeUnexpectedContentTypeError(contentType: contentType) + default: + preconditionFailure("bestContentType chose an invalid content type.") } return Operations.createPet.Input( headers: headers, @@ -359,10 +363,14 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { deserializer: { request, requestBody, metadata in let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) let body: Operations.createPetWithForm.Input.Body - if try contentType == nil || converter.isMatchingContentType( + let chosenContentType = try converter.bestContentType( received: contentType, - expectedRaw: "application/x-www-form-urlencoded" - ) { + options: [ + "application/x-www-form-urlencoded" + ] + ) + switch chosenContentType { + case "application/x-www-form-urlencoded": body = try await converter.getRequiredRequestBodyAsURLEncodedForm( Components.Schemas.CreatePetRequest.self, from: requestBody, @@ -370,8 +378,8 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { .urlEncodedForm(value) } ) - } else { - throw converter.makeUnexpectedContentTypeError(contentType: contentType) + default: + preconditionFailure("bestContentType chose an invalid content type.") } return Operations.createPetWithForm.Input(body: body) }, @@ -471,10 +479,16 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { deserializer: { request, requestBody, metadata in let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) let body: Operations.postStats.Input.Body - if try contentType == nil || converter.isMatchingContentType( + let chosenContentType = try converter.bestContentType( received: contentType, - expectedRaw: "application/json" - ) { + options: [ + "application/json", + "text/plain", + "application/octet-stream" + ] + ) + switch chosenContentType { + case "application/json": body = try await converter.getRequiredRequestBodyAsJSON( Components.Schemas.PetStats.self, from: requestBody, @@ -482,10 +496,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { .json(value) } ) - } else if try converter.isMatchingContentType( - received: contentType, - expectedRaw: "text/plain" - ) { + case "text/plain": body = try converter.getRequiredRequestBodyAsBinary( OpenAPIRuntime.HTTPBody.self, from: requestBody, @@ -493,10 +504,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { .plainText(value) } ) - } else if try converter.isMatchingContentType( - received: contentType, - expectedRaw: "application/octet-stream" - ) { + case "application/octet-stream": body = try converter.getRequiredRequestBodyAsBinary( OpenAPIRuntime.HTTPBody.self, from: requestBody, @@ -504,8 +512,8 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { .binary(value) } ) - } else { - throw converter.makeUnexpectedContentTypeError(contentType: contentType) + default: + preconditionFailure("bestContentType chose an invalid content type.") } return Operations.postStats.Input(body: body) }, @@ -579,10 +587,14 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { let headers: Operations.updatePet.Input.Headers = .init(accept: try converter.extractAcceptHeaderIfPresent(in: request.headerFields)) let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) let body: Components.RequestBodies.UpdatePetRequest? - if try contentType == nil || converter.isMatchingContentType( + let chosenContentType = try converter.bestContentType( received: contentType, - expectedRaw: "application/json" - ) { + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": body = try await converter.getOptionalRequestBodyAsJSON( Components.RequestBodies.UpdatePetRequest.jsonPayload.self, from: requestBody, @@ -590,8 +602,8 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { .json(value) } ) - } else { - throw converter.makeUnexpectedContentTypeError(contentType: contentType) + default: + preconditionFailure("bestContentType chose an invalid content type.") } return Operations.updatePet.Input( path: path, @@ -656,10 +668,14 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { let headers: Operations.uploadAvatarForPet.Input.Headers = .init(accept: try converter.extractAcceptHeaderIfPresent(in: request.headerFields)) let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) let body: Operations.uploadAvatarForPet.Input.Body - if try contentType == nil || converter.isMatchingContentType( + let chosenContentType = try converter.bestContentType( received: contentType, - expectedRaw: "application/octet-stream" - ) { + options: [ + "application/octet-stream" + ] + ) + switch chosenContentType { + case "application/octet-stream": body = try converter.getRequiredRequestBodyAsBinary( OpenAPIRuntime.HTTPBody.self, from: requestBody, @@ -667,8 +683,8 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { .binary(value) } ) - } else { - throw converter.makeUnexpectedContentTypeError(contentType: contentType) + default: + preconditionFailure("bestContentType chose an invalid content type.") } return Operations.uploadAvatarForPet.Input( path: path, diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift index 7fae4c87..34f60b55 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift @@ -273,10 +273,27 @@ public enum Components { self.value4 = value4 } public init(from decoder: any Decoder) throws { - value1 = try? decoder.decodeFromSingleValueContainer() - value2 = try? decoder.decodeFromSingleValueContainer() - value3 = try? .init(from: decoder) - value4 = try? decoder.decodeFromSingleValueContainer() + var errors: [any Error] = [] + do { + value1 = try decoder.decodeFromSingleValueContainer() + } catch { + errors.append(error) + } + do { + value2 = try decoder.decodeFromSingleValueContainer() + } catch { + errors.append(error) + } + do { + value3 = try .init(from: decoder) + } catch { + errors.append(error) + } + do { + value4 = try decoder.decodeFromSingleValueContainer() + } catch { + errors.append(error) + } try Swift.DecodingError.verifyAtLeastOneSchemaIsNotNil( [ value1, @@ -285,7 +302,8 @@ public enum Components { value4 ], type: Self.self, - codingPath: decoder.codingPath + codingPath: decoder.codingPath, + errors: errors ) } public func encode(to encoder: any Encoder) throws { @@ -306,21 +324,29 @@ public enum Components { /// - Remark: Generated from `#/components/schemas/MixedOneOf/case3`. case Pet(Components.Schemas.Pet) public init(from decoder: any Decoder) throws { + var errors: [any Error] = [] do { self = .case1(try decoder.decodeFromSingleValueContainer()) return - } catch {} + } catch { + errors.append(error) + } do { self = .PetKind(try decoder.decodeFromSingleValueContainer()) return - } catch {} + } catch { + errors.append(error) + } do { self = .Pet(try .init(from: decoder)) return - } catch {} + } catch { + errors.append(error) + } throw Swift.DecodingError.failedToDecodeOneOfSchema( type: Self.self, - codingPath: decoder.codingPath + codingPath: decoder.codingPath, + errors: errors ) } public func encode(to encoder: any Encoder) throws { @@ -668,15 +694,25 @@ public enum Components { self.value2 = value2 } public init(from decoder: any Decoder) throws { - value1 = try? .init(from: decoder) - value2 = try? .init(from: decoder) + var errors: [any Error] = [] + do { + value1 = try .init(from: decoder) + } catch { + errors.append(error) + } + do { + value2 = try .init(from: decoder) + } catch { + errors.append(error) + } try Swift.DecodingError.verifyAtLeastOneSchemaIsNotNil( [ value1, value2 ], type: Self.self, - codingPath: decoder.codingPath + codingPath: decoder.codingPath, + errors: errors ) } public func encode(to encoder: any Encoder) throws { @@ -710,25 +746,35 @@ public enum Components { /// - Remark: Generated from `#/components/schemas/OneOfAny/case4`. case case4(Components.Schemas.OneOfAny.Case4Payload) public init(from decoder: any Decoder) throws { + var errors: [any Error] = [] do { self = .case1(try decoder.decodeFromSingleValueContainer()) return - } catch {} + } catch { + errors.append(error) + } do { self = .case2(try decoder.decodeFromSingleValueContainer()) return - } catch {} + } catch { + errors.append(error) + } do { self = .CodeError(try .init(from: decoder)) return - } catch {} + } catch { + errors.append(error) + } do { self = .case4(try .init(from: decoder)) return - } catch {} + } catch { + errors.append(error) + } throw Swift.DecodingError.failedToDecodeOneOfSchema( type: Self.self, - codingPath: decoder.codingPath + codingPath: decoder.codingPath, + errors: errors ) } public func encode(to encoder: any Encoder) throws { @@ -845,8 +891,9 @@ public enum Components { case "MessagedExercise", "#/components/schemas/MessagedExercise": self = .MessagedExercise(try .init(from: decoder)) default: - throw Swift.DecodingError.failedToDecodeOneOfSchema( - type: Self.self, + throw Swift.DecodingError.unknownOneOfDiscriminator( + discriminatorKey: CodingKeys.kind, + discriminatorValue: discriminator, codingPath: decoder.codingPath ) } @@ -1184,8 +1231,9 @@ public enum Components { case "RecursivePetOneOfSecond", "#/components/schemas/RecursivePetOneOfSecond": self = .RecursivePetOneOfSecond(try .init(from: decoder)) default: - throw Swift.DecodingError.failedToDecodeOneOfSchema( - type: Self.self, + throw Swift.DecodingError.unknownOneOfDiscriminator( + discriminatorKey: CodingKeys._type, + discriminatorValue: discriminator, codingPath: decoder.codingPath ) } @@ -1254,15 +1302,25 @@ public enum Components { self.value2 = value2 } init(from decoder: any Decoder) throws { - value1 = try? .init(from: decoder) - value2 = try? decoder.decodeFromSingleValueContainer() + var errors: [any Error] = [] + do { + value1 = try .init(from: decoder) + } catch { + errors.append(error) + } + do { + value2 = try decoder.decodeFromSingleValueContainer() + } catch { + errors.append(error) + } try Swift.DecodingError.verifyAtLeastOneSchemaIsNotNil( [ value1, value2 ], type: Self.self, - codingPath: decoder.codingPath + codingPath: decoder.codingPath, + errors: errors ) } func encode(to encoder: any Encoder) throws { diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index 476ed3f8..a595532c 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -413,10 +413,27 @@ final class SnippetBasedReferenceTests: XCTestCase { self.value4 = value4 } public init(from decoder: any Decoder) throws { - value1 = try? .init(from: decoder) - value2 = try? .init(from: decoder) - value3 = try? decoder.decodeFromSingleValueContainer() - value4 = try? decoder.decodeFromSingleValueContainer() + var errors: [any Error] = [] + do { + value1 = try .init(from: decoder) + } catch { + errors.append(error) + } + do { + value2 = try .init(from: decoder) + } catch { + errors.append(error) + } + do { + value3 = try decoder.decodeFromSingleValueContainer() + } catch { + errors.append(error) + } + do { + value4 = try decoder.decodeFromSingleValueContainer() + } catch { + errors.append(error) + } try Swift.DecodingError.verifyAtLeastOneSchemaIsNotNil( [ value1, @@ -425,7 +442,8 @@ final class SnippetBasedReferenceTests: XCTestCase { value4 ], type: Self.self, - codingPath: decoder.codingPath + codingPath: decoder.codingPath, + errors: errors ) } public func encode(to encoder: any Encoder) throws { @@ -506,8 +524,9 @@ final class SnippetBasedReferenceTests: XCTestCase { case "B", "#/components/schemas/B": self = .B(try .init(from: decoder)) default: - throw Swift.DecodingError.failedToDecodeOneOfSchema( - type: Self.self, + throw Swift.DecodingError.unknownOneOfDiscriminator( + discriminatorKey: CodingKeys.which, + discriminatorValue: discriminator, codingPath: decoder.codingPath ) } @@ -610,8 +629,9 @@ final class SnippetBasedReferenceTests: XCTestCase { case "C", "#/components/schemas/C": self = .C(try .init(from: decoder)) default: - throw Swift.DecodingError.failedToDecodeOneOfSchema( - type: Self.self, + throw Swift.DecodingError.unknownOneOfDiscriminator( + discriminatorKey: CodingKeys.which, + discriminatorValue: discriminator, codingPath: decoder.codingPath ) } @@ -653,21 +673,29 @@ final class SnippetBasedReferenceTests: XCTestCase { case case2(Swift.Int) case A(Components.Schemas.A) public init(from decoder: any Decoder) throws { + var errors: [any Error] = [] do { self = .case1(try decoder.decodeFromSingleValueContainer()) return - } catch {} + } catch { + errors.append(error) + } do { self = .case2(try decoder.decodeFromSingleValueContainer()) return - } catch {} + } catch { + errors.append(error) + } do { self = .A(try .init(from: decoder)) return - } catch {} + } catch { + errors.append(error) + } throw Swift.DecodingError.failedToDecodeOneOfSchema( type: Self.self, - codingPath: decoder.codingPath + codingPath: decoder.codingPath, + errors: errors ) } public func encode(to encoder: any Encoder) throws { @@ -715,21 +743,29 @@ final class SnippetBasedReferenceTests: XCTestCase { case case2(Swift.Int) case A(Components.Schemas.A) public init(from decoder: any Decoder) throws { + var errors: [any Error] = [] do { self = .case1(try decoder.decodeFromSingleValueContainer()) return - } catch {} + } catch { + errors.append(error) + } do { self = .case2(try decoder.decodeFromSingleValueContainer()) return - } catch {} + } catch { + errors.append(error) + } do { self = .A(try .init(from: decoder)) return - } catch {} + } catch { + errors.append(error) + } throw Swift.DecodingError.failedToDecodeOneOfSchema( type: Self.self, - codingPath: decoder.codingPath + codingPath: decoder.codingPath, + errors: errors ) } public func encode(to encoder: any Encoder) throws { @@ -753,15 +789,25 @@ final class SnippetBasedReferenceTests: XCTestCase { self.value2 = value2 } public init(from decoder: any Decoder) throws { - value1 = try? .init(from: decoder) - value2 = try? .init(from: decoder) + var errors: [any Error] = [] + do { + value1 = try .init(from: decoder) + } catch { + errors.append(error) + } + do { + value2 = try .init(from: decoder) + } catch { + errors.append(error) + } try Swift.DecodingError.verifyAtLeastOneSchemaIsNotNil( [ value1, value2 ], type: Self.self, - codingPath: decoder.codingPath + codingPath: decoder.codingPath, + errors: errors ) } public func encode(to encoder: any Encoder) throws { @@ -968,15 +1014,25 @@ final class SnippetBasedReferenceTests: XCTestCase { self.value2 = value2 } public init(from decoder: any Decoder) throws { - value1 = try? decoder.decodeFromSingleValueContainer() - value2 = try? decoder.decodeFromSingleValueContainer() + var errors: [any Error] = [] + do { + value1 = try decoder.decodeFromSingleValueContainer() + } catch { + errors.append(error) + } + do { + value2 = try decoder.decodeFromSingleValueContainer() + } catch { + errors.append(error) + } try Swift.DecodingError.verifyAtLeastOneSchemaIsNotNil( [ value1, value2 ], type: Self.self, - codingPath: decoder.codingPath + codingPath: decoder.codingPath, + errors: errors ) } public func encode(to encoder: any Encoder) throws { @@ -1366,15 +1422,25 @@ final class SnippetBasedReferenceTests: XCTestCase { self.value2 = value2 } init(from decoder: any Decoder) throws { - value1 = try? .init(from: decoder) - value2 = try? decoder.decodeFromSingleValueContainer() + var errors: [any Error] = [] + do { + value1 = try .init(from: decoder) + } catch { + errors.append(error) + } + do { + value2 = try decoder.decodeFromSingleValueContainer() + } catch { + errors.append(error) + } try Swift.DecodingError.verifyAtLeastOneSchemaIsNotNil( [ value1, value2 ], type: Self.self, - codingPath: decoder.codingPath + codingPath: decoder.codingPath, + errors: errors ) } func encode(to encoder: any Encoder) throws { @@ -1405,17 +1471,23 @@ final class SnippetBasedReferenceTests: XCTestCase { case Node(Components.Schemas.Node) case case2(Swift.String) public init(from decoder: any Decoder) throws { + var errors: [any Error] = [] do { self = .Node(try .init(from: decoder)) return - } catch {} + } catch { + errors.append(error) + } do { self = .case2(try decoder.decodeFromSingleValueContainer()) return - } catch {} + } catch { + errors.append(error) + } throw Swift.DecodingError.failedToDecodeOneOfSchema( type: Self.self, - codingPath: decoder.codingPath + codingPath: decoder.codingPath, + errors: errors ) } public func encode(to encoder: any Encoder) throws { @@ -2104,10 +2176,14 @@ final class SnippetBasedReferenceTests: XCTestCase { { request, requestBody, metadata in let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) let body: Operations.get_sol_foo.Input.Body - if try contentType == nil || converter.isMatchingContentType( + let chosenContentType = try converter.bestContentType( received: contentType, - expectedRaw: "application/json" - ) { + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": body = try await converter.getRequiredRequestBodyAsJSON( Swift.String.self, from: requestBody, @@ -2115,8 +2191,8 @@ final class SnippetBasedReferenceTests: XCTestCase { .json(value) } ) - } else { - throw converter.makeUnexpectedContentTypeError(contentType: contentType) + default: + preconditionFailure("bestContentType chose an invalid content type.") } return Operations.get_sol_foo.Input(body: body) } @@ -2177,10 +2253,14 @@ final class SnippetBasedReferenceTests: XCTestCase { { request, requestBody, metadata in let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) let body: Operations.get_sol_foo.Input.Body - if try contentType == nil || converter.isMatchingContentType( + let chosenContentType = try converter.bestContentType( received: contentType, - expectedRaw: "application/json" - ) { + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": body = try await converter.getRequiredRequestBodyAsJSON( Swift.String.self, from: requestBody, @@ -2188,8 +2268,8 @@ final class SnippetBasedReferenceTests: XCTestCase { .json(value) } ) - } else { - throw converter.makeUnexpectedContentTypeError(contentType: contentType) + default: + preconditionFailure("bestContentType chose an invalid content type.") } return Operations.get_sol_foo.Input(body: body) } @@ -2252,10 +2332,14 @@ final class SnippetBasedReferenceTests: XCTestCase { { request, requestBody, metadata in let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) let body: Operations.get_sol_foo.Input.Body? - if try contentType == nil || converter.isMatchingContentType( + let chosenContentType = try converter.bestContentType( received: contentType, - expectedRaw: "application/json" - ) { + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": body = try await converter.getOptionalRequestBodyAsJSON( Swift.String.self, from: requestBody, @@ -2263,8 +2347,8 @@ final class SnippetBasedReferenceTests: XCTestCase { .json(value) } ) - } else { - throw converter.makeUnexpectedContentTypeError(contentType: contentType) + default: + preconditionFailure("bestContentType chose an invalid content type.") } return Operations.get_sol_foo.Input(body: body) } @@ -2327,10 +2411,14 @@ final class SnippetBasedReferenceTests: XCTestCase { { request, requestBody, metadata in let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) let body: Operations.get_sol_foo.Input.Body? - if try contentType == nil || converter.isMatchingContentType( + let chosenContentType = try converter.bestContentType( received: contentType, - expectedRaw: "application/json" - ) { + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": body = try await converter.getOptionalRequestBodyAsJSON( Swift.String.self, from: requestBody, @@ -2338,8 +2426,8 @@ final class SnippetBasedReferenceTests: XCTestCase { .json(value) } ) - } else { - throw converter.makeUnexpectedContentTypeError(contentType: contentType) + default: + preconditionFailure("bestContentType chose an invalid content type.") } return Operations.get_sol_foo.Input(body: body) } diff --git a/Tests/PetstoreConsumerTests/Test_Types.swift b/Tests/PetstoreConsumerTests/Test_Types.swift index 2e06767b..eb1822ab 100644 --- a/Tests/PetstoreConsumerTests/Test_Types.swift +++ b/Tests/PetstoreConsumerTests/Test_Types.swift @@ -159,6 +159,12 @@ final class Test_Types: XCTestCase { XCTAssertThrowsError( try testDecoder.decode(Components.Schemas.OneOfObjectsWithDiscriminator.self, from: Data(#"{}"#.utf8)) ) + XCTAssertThrowsError( + try testDecoder.decode( + Components.Schemas.OneOfObjectsWithDiscriminator.self, + from: Data(#"{"kind": "FooBar"}"#.utf8) + ) + ) } func testThrowingShorthandAPIs() throws { let created = Operations.createPet.Output.Created(body: .json(.init(id: 42, name: "Scruffy")))