From cb41d40414ed569aa238a33f37b31b637c709bbf Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 4 Aug 2023 09:52:23 +0200 Subject: [PATCH] [Generator] Multiple content types support (#146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Generator] Multiple content types support Depends on https://github.com/apple/swift-openapi-runtime/pull/29 ### Motivation Up until now, when an OpenAPI document contained more than one content type in either a request or a response body, we would only generate code for now, and ignored the other ones. However, that was a temporary workaround until we could add proper support for multiple content types, which is what this PR is about. Addresses #6 and #7, except for the "Accept header" propagation, which will be addressed separately and have its own PR and even a proposal. That's one of the reasons this feature is landing disabled, hidden behind the `multipleContentTypes` feature flag. A tip for reviewing this change: first check out how the test OpenAPI document and the reference files changed, that shows the details of how we encode and decode multiple content types. I also added comments in the code for easier review. ### Modifications - Main: all supported content types found in a `content` map in request and response bodies are now generated in the `Body` enum. - Added a method `supportedTypedContents` that returns all supported content types, used now instead of the `bestTypedContent` method we used before. What is a "supported" content type? One that has a schema that doesn't return `false` to `isSchemaSupported`. (This is a pre-existing concept.) - Updated the logic for generating request and response bodies to use the new method, both on client and server. - Updated content type -> Swift enum case name logic, but that will go through a full proposal, so please hold feedback for the proposal, this is just a first pass based on some collected data of most commonly used content types. - ### Open Questions - What to do in an operation with multiple request/response content types when _no_ `content-type` header is provided? Previously, with up to 1 content type support, we still went ahead and tried to parse it. But here, we don't know which content to parse it as, so we just try it with the first in the list (we respect the order in the YAML dictionary, so what the user put first will be what we try to use when no `content-type` header is provided). If an invalid `content-type` value is provided, we throw an error (pre-existing behavior). Q: Is choosing the first reasonable? What would be better? Note that, unfortunately, many servers don't send `content-type` today. ### Result When an adopter provides multiple content types in their OpenAPI document, we generate one case in the Body enum for each! However, the new functionality is disabled by default, has to be explicitly requested by enabling the `multipleContentTypes` feature flag either on the CLI or in the config file. ### Test Plan Added `setStats` and `getStats` operations that employ multiple content types, one in a request body, the other in a response body. Added unit tests for it in `PetstoreConsumerTests` to verify it all works correctly at runtime. Deleted `Tests/OpenAPIGeneratorCoreTests/Translator/RequestBody/Test_translateRequestBody.swift` as snippet tests do this job better, and I didn't want to spend the time updating the test. Adds `PetstoreConsumerTests_FF_MultipleContentTypes`, a variant of `PetstoreConsumerTests` for when the `multipleContentTypes` feature flag is enabled, so we test both how the existing logic handles multiple content types (it picks one), and the new logic (it generates code for all supported ones it found). ## TODOs - [x] rename `IfConditionPair` to `IfBranch` - [x] break out the first `if` from the subsequence `else if`s in `IfStatement`, to 3 properties: first if, then an array of else ifs, then an optional else; to avoid creating invalid code - [x] create an SPI type `ContentType` to avoid repeatedly parsing the received content type Reviewed by: gjcairo, 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. https://github.com/apple/swift-openapi-generator/pull/146 --- .swift-format | 2 +- Package.swift | 14 +- .../StructuredSwiftRepresentation.swift | 69 + .../Renderer/TextBasedRenderer.swift | 24 + .../Translator/Content/ContentInspector.swift | 165 +- .../Translator/Content/ContentSwiftName.swift | 20 +- .../Translator/Content/ContentType.swift | 21 +- .../RequestBody/TypedRequestBody.swift | 17 +- .../RequestBody/translateRequestBody.swift | 241 ++- .../Responses/acceptHeaderContentTypes.swift | 7 +- .../Responses/translateResponse.swift | 7 +- .../Responses/translateResponseOutcome.swift | 149 +- .../translateServerMethod.swift | 48 +- .../StructureHelpers.swift | 5 + .../Content/Test_ContentSwiftName.swift | 20 +- .../Test_translateRequestBody.swift | 67 - .../FileBasedReferenceTests.swift | 17 +- .../Resources/Docs/petstore.yaml | 32 + .../ReferenceSources/Petstore/Client.swift | 229 ++- .../ReferenceSources/Petstore/Server.swift | 167 +- .../ReferenceSources/Petstore/Types.swift | 191 ++ .../Client.swift | 549 ++++++ .../Server.swift | 613 +++++++ .../Types.swift | 1601 +++++++++++++++++ .../SnippetBasedReferenceTests.swift | 120 +- Tests/PetstoreConsumerTests/TestClient.swift | 22 + Tests/PetstoreConsumerTests/TestServer.swift | 18 + Tests/PetstoreConsumerTests/Test_Client.swift | 110 ++ Tests/PetstoreConsumerTests/Test_Server.swift | 157 +- .../Generated | 1 + .../TestClient.swift | 112 ++ .../TestServer.swift | 115 ++ .../Test_Client.swift | 152 ++ .../Test_Server.swift | 155 ++ .../Test_Types.swift | 233 +++ scripts/check-license-headers.sh | 1 + 36 files changed, 5137 insertions(+), 334 deletions(-) delete mode 100644 Tests/OpenAPIGeneratorCoreTests/Translator/RequestBody/Test_translateRequestBody.swift create mode 100644 Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Client.swift create mode 100644 Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Server.swift create mode 100644 Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Types.swift create mode 120000 Tests/PetstoreConsumerTestsFFMultipleContentTypes/Generated create mode 100644 Tests/PetstoreConsumerTestsFFMultipleContentTypes/TestClient.swift create mode 100644 Tests/PetstoreConsumerTestsFFMultipleContentTypes/TestServer.swift create mode 100644 Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift create mode 100644 Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift create mode 100644 Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Types.swift diff --git a/.swift-format b/.swift-format index e6a70edf..efbc73c4 100644 --- a/.swift-format +++ b/.swift-format @@ -43,7 +43,7 @@ "OnlyOneTrailingClosureArgument" : true, "OrderedImports" : false, "ReturnVoidInsteadOfEmptyTuple" : true, - "UseEarlyExits" : true, + "UseEarlyExits" : false, "UseLetInEveryBoundCaseVariable" : false, "UseShorthandTypeNames" : true, "UseSingleLinePropertyGetter" : false, diff --git a/Package.swift b/Package.swift index d2798fda..76f840bb 100644 --- a/Package.swift +++ b/Package.swift @@ -78,7 +78,7 @@ let package = Package( ), // Tests-only: Runtime library linked by generated code - .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.1.6")), + .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.1.7")), // Build and preview docs .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), @@ -143,6 +143,18 @@ let package = Package( swiftSettings: swiftSettings ), + // PetstoreConsumerTestsFFMultipleContentTypes + // Builds and tests the reference code from GeneratorReferenceTests + // to ensure it actually works correctly at runtime. + // Enabled feature flag: multipleContentTypes + .testTarget( + name: "PetstoreConsumerTestsFFMultipleContentTypes", + dependencies: [ + "PetstoreConsumerTestCore" + ], + swiftSettings: swiftSettings + ), + // Generator CLI .executableTarget( name: "swift-openapi-generator", diff --git a/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift b/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift index 29989ae3..78aaf013 100644 --- a/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift +++ b/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift @@ -664,6 +664,41 @@ struct SwitchDescription: Equatable, Codable { var cases: [SwitchCaseDescription] } +/// A description of an if branch and the corresponding code block. +/// +/// For example: in `if foo { bar }`, the condition pair represents +/// `foo` + `bar`. +struct IfBranch: Equatable, Codable { + + /// The expressions evaluated by the if statement and their corresponding + /// body blocks. If more than one is provided, an `else if` branch is added. + /// + /// For example, in `if foo { bar }`, `condition` is `foo`. + var condition: Expression + + /// The body executed if the `condition` evaluates to true. + /// + /// For example, in `if foo { bar }`, `body` is `bar`. + var body: [CodeBlock] +} + +/// A description of an if[[/elseif]/else] statement expression. +/// +/// For example: `if foo { } else if bar { } else { }`. +struct IfStatementDescription: Equatable, Codable { + + /// The primary `if` branch. + var ifBranch: IfBranch + + /// Additional `else if` branches. + var elseIfBranches: [IfBranch] + + /// The body of an else block. + /// + /// No `else` statement is added when `elseBody` is nil. + var elseBody: [CodeBlock]? +} + /// A description of a do statement. /// /// For example: `do { try foo() } catch { return bar }`. @@ -709,6 +744,9 @@ enum KeywordKind: Equatable, Codable { /// The await keyword. case `await` + + /// The throw keyword. + case `throw` } /// A description of an expression that places a keyword before an expression. @@ -751,8 +789,14 @@ enum BinaryOperator: String, Equatable, Codable { /// The += operator, adds and then assigns another value. case plusEquals = "+=" + /// The == operator, checks equality between two values. + case equals = "==" + /// The ... operator, creates an end-inclusive range between two numbers. case rangeInclusive = "..." + + /// The || operator, used between two Boolean values. + case booleanOr = "||" } /// A description of a binary operation expression. @@ -832,6 +876,11 @@ indirect enum Expression: Equatable, Codable { /// For example: `switch foo {`. case `switch`(SwitchDescription) + /// An if statement, with optional else if's and an else statement attached. + /// + /// For example: `if foo { bar } else if baz { boo } else { bam }`. + case ifStatement(IfStatementDescription) + /// A do statement. /// /// For example: `do { try foo() } catch { return bar }`. @@ -1202,6 +1251,26 @@ extension Expression { ) } + /// Returns an if statement, with optional else if's and an else + /// statement attached. + /// - Parameters: + /// - ifBranch: The primary `if` branch. + /// - elseIfBranches: Additional `else if` branches. + /// - elseBody: The body of an else block. + static func ifStatement( + ifBranch: IfBranch, + elseIfBranches: [IfBranch] = [], + elseBody: [CodeBlock]? = nil + ) -> Self { + .ifStatement( + .init( + ifBranch: ifBranch, + elseIfBranches: elseIfBranches, + elseBody: elseBody + ) + ) + } + /// Returns a new function call expression. /// /// For example `foo(bar: 42)`. diff --git a/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift b/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift index 3fe4699e..b004dd12 100644 --- a/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift +++ b/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift @@ -175,6 +175,26 @@ struct TextBasedRenderer: RendererProtocol { return lines.joinedLines() } + /// Renders the specified if statement. + func renderedIf(_ ifDesc: IfStatementDescription) -> String { + var lines: [String] = [] + let ifBranch = ifDesc.ifBranch + lines.append("if \(renderedExpression(ifBranch.condition)) {") + lines.append(renderedCodeBlocks(ifBranch.body)) + lines.append("}") + for branch in ifDesc.elseIfBranches { + lines.append("else if \(renderedExpression(branch.condition)) {") + lines.append(renderedCodeBlocks(branch.body)) + lines.append("}") + } + if let elseBody = ifDesc.elseBody { + lines.append("else {") + lines.append(renderedCodeBlocks(elseBody)) + lines.append("}") + } + return lines.joinedLines() + } + /// Renders the specified switch expression. func renderedDoStatement(_ description: DoStatementDescription) -> String { var lines: [String] = ["do {"] @@ -201,6 +221,8 @@ struct TextBasedRenderer: RendererProtocol { return "try\(hasPostfixQuestionMark ? "?" : "")" case .await: return "await" + case .throw: + return "throw" } } @@ -268,6 +290,8 @@ struct TextBasedRenderer: RendererProtocol { return renderedAssignment(assignment) case .switch(let switchDesc): return renderedSwitch(switchDesc) + case .ifStatement(let ifDesc): + return renderedIf(ifDesc) case .doStatement(let doStmt): return renderedDoStatement(doStmt) case .valueBinding(let valueBinding): diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentInspector.swift b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentInspector.swift index 823f8408..64871010 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentInspector.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentInspector.swift @@ -61,6 +61,78 @@ extension FileTranslator { return .init(content: content, typeUsage: associatedType) } + /// Extract the supported content types. + /// - Parameters: + /// - map: The content map from the OpenAPI document. + /// - excludeBinary: A Boolean value controlling whether binary content + /// type should be skipped, for example used when encoding headers. + /// - parent: The parent type of the chosen typed schema. + /// - Returns: The supported content type + schema + type names. + func supportedTypedContents( + _ map: OpenAPI.Content.Map, + excludeBinary: Bool = false, + inParent parent: TypeName + ) throws -> [TypedSchemaContent] { + let contents = supportedContents( + map, + excludeBinary: excludeBinary, + foundIn: parent.description + ) + return try contents.compactMap { content in + guard + try validateSchemaIsSupported( + content.schema, + foundIn: parent.description + ) + else { + return nil + } + let identifier = contentSwiftName(content.contentType) + let associatedType = try typeAssigner.typeUsage( + usingNamingHint: identifier, + withSchema: content.schema, + inParent: parent + ) + return .init(content: content, typeUsage: associatedType) + } + } + + /// Extract the supported content types. + /// - Parameters: + /// - contents: The content map from the OpenAPI document. + /// - excludeBinary: A Boolean value controlling whether binary content + /// type should be skipped, for example used when encoding headers. + /// - foundIn: The location where this content is parsed. + /// - Returns: the detected content type + schema, nil if no supported + /// schema found or if empty. + func supportedContents( + _ contents: OpenAPI.Content.Map, + excludeBinary: Bool = false, + foundIn: String + ) -> [SchemaContent] { + guard !contents.isEmpty else { + return [] + } + guard config.featureFlags.contains(.multipleContentTypes) else { + return bestSingleContent( + contents, + excludeBinary: excludeBinary, + foundIn: foundIn + ) + .flatMap { [$0] } ?? [] + } + return + contents + .compactMap { key, value in + parseContentIfSupported( + contentKey: key, + contentValue: value, + excludeBinary: excludeBinary, + foundIn: foundIn + "/\(key.rawValue)" + ) + } + } + /// While we only support a single content at a time, choose the best one. /// /// Priority: @@ -72,6 +144,7 @@ extension FileTranslator { /// - map: The content map from the OpenAPI document. /// - excludeBinary: A Boolean value controlling whether binary content /// type should be skipped, for example used when encoding headers. + /// - foundIn: The location where this content is parsed. /// - Returns: the detected content type + schema, nil if no supported /// schema found or if empty. func bestSingleContent( @@ -88,8 +161,81 @@ extension FileTranslator { foundIn: foundIn ) } + let chosenContent: (SchemaContent, OpenAPI.Content)? if let (contentKey, contentValue) = map.first(where: { $0.key.isJSON }), let contentType = ContentType(contentKey.typeAndSubtype) + { + chosenContent = ( + .init( + contentType: contentType, + schema: contentValue.schema + ), + contentValue + ) + } else if let (contentKey, contentValue) = map.first(where: { $0.key.isText }), + let contentType = ContentType(contentKey.typeAndSubtype) + { + chosenContent = ( + .init( + contentType: contentType, + schema: .b(.string) + ), + contentValue + ) + } else if !excludeBinary, + let (contentKey, contentValue) = map.first(where: { $0.key.isBinary }), + let contentType = ContentType(contentKey.typeAndSubtype) + { + chosenContent = ( + .init( + contentType: contentType, + schema: .b(.string(format: .binary)) + ), + contentValue + ) + } else { + diagnostics.emitUnsupported( + "Unsupported content", + foundIn: foundIn + ) + chosenContent = nil + } + if let chosenContent { + let rawMIMEType = chosenContent.0.contentType.rawMIMEType + if rawMIMEType.hasPrefix("multipart/") || rawMIMEType.contains("application/x-www-form-urlencoded") { + diagnostics.emitUnsupportedIfNotNil( + chosenContent.1.encoding, + "Custom encoding for JSON content", + foundIn: "\(foundIn), content \(rawMIMEType)" + ) + } + } + return chosenContent?.0 + } + + /// Returns a wrapped version of the provided content if supported, returns + /// nil otherwise. + /// + /// Priority of checking for known MIME types: + /// 1. JSON + /// 2. text + /// 3. binary + /// + /// - Parameters: + /// - contentKey: The content key from the OpenAPI document. + /// - contentValue: The content value from the OpenAPI document. + /// - excludeBinary: A Boolean value controlling whether binary content + /// type should be skipped, for example used when encoding headers. + /// - foundIn: The location where this content is parsed. + /// - Returns: The detected content type + schema, nil if unsupported. + func parseContentIfSupported( + contentKey: OpenAPI.ContentType, + contentValue: OpenAPI.Content, + excludeBinary: Bool = false, + foundIn: String + ) -> SchemaContent? { + if contentKey.isJSON, + let contentType = ContentType(contentKey.typeAndSubtype) { diagnostics.emitUnsupportedIfNotNil( contentValue.encoding, @@ -100,27 +246,28 @@ extension FileTranslator { contentType: contentType, schema: contentValue.schema ) - } else if let (contentKey, _) = map.first(where: { $0.key.isText }), + } + if contentKey.isText, let contentType = ContentType(contentKey.typeAndSubtype) { return .init( contentType: contentType, schema: .b(.string) ) - } else if !excludeBinary, - let (contentKey, _) = map.first(where: { $0.key.isBinary }), + } + if !excludeBinary, + contentKey.isBinary, let contentType = ContentType(contentKey.typeAndSubtype) { return .init( contentType: contentType, schema: .b(.string(format: .binary)) ) - } else { - diagnostics.emitUnsupported( - "Unsupported content", - foundIn: foundIn - ) - return nil } + diagnostics.emitUnsupported( + "Unsupported content", + foundIn: foundIn + ) + return nil } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentSwiftName.swift b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentSwiftName.swift index 5efbb552..8e446597 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentSwiftName.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentSwiftName.swift @@ -21,7 +21,25 @@ extension FileTranslator { /// - Parameter contentType: The content type for which to compute the name. func contentSwiftName(_ contentType: ContentType) -> String { if config.featureFlags.contains(.multipleContentTypes) { - return "unsupported" + let rawMIMEType = contentType.rawMIMEType + switch rawMIMEType { + case "application/json": + return "json" + case "application/x-www-form-urlencoded": + return "form" + case "multipart/form-data": + return "multipart" + case "text/plain": + return "text" + case "*/*": + return "any" + case "application/xml": + return "xml" + case "application/octet-stream": + return "binary" + default: + return swiftSafeName(for: rawMIMEType) + } } else { switch contentType { case .json: diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift index c67f20a1..eea7c10b 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift @@ -42,6 +42,14 @@ enum ContentType: Hashable { self = .binary(rawValue) } + /// Returns the original raw MIME type. + var rawMIMEType: String { + switch self { + case .json(let string), .text(let string), .binary(let string): + return string + } + } + /// The header value used when sending a content-type header. var headerValueForSending: String { switch self { @@ -107,23 +115,10 @@ enum ContentType: Hashable { } return false } - - /// Returns a new content type representing an octet stream. - static var octetStream: Self { - .binary("application/octet-stream") - } - - /// Returns a new content type representing JSON. - static var applicationJSON: Self { - .json("application/json") - } } extension OpenAPI.ContentType { - /// Returns a new content type representing an octet stream. - static let octetStream: Self = .other(ContentType.octetStream.headerValueForValidation) - /// A Boolean value that indicates whether the content type /// is a type of JSON. var isJSON: Bool { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/TypedRequestBody.swift b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/TypedRequestBody.swift index 84517116..e61abe24 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/TypedRequestBody.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/TypedRequestBody.swift @@ -25,8 +25,8 @@ struct TypedRequestBody { /// A Boolean value indicating whether the response is inlined. var isInlined: Bool - /// The validated content. - var content: TypedSchemaContent + /// The validated contents. + var contents: [TypedSchemaContent] } extension FileTranslator { @@ -95,12 +95,11 @@ extension FileTranslator { isInlined = true } - guard - let content = try bestSingleTypedContent( - request.content, - inParent: typeName - ) - else { + let contents = try supportedTypedContents( + request.content, + inParent: typeName + ) + if contents.isEmpty { return nil } @@ -109,7 +108,7 @@ extension FileTranslator { request: request, typeUsage: usage, isInlined: isInlined, - content: content + contents: contents ) } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift index c9a57f41..d77a4125 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift @@ -16,23 +16,18 @@ import OpenAPIKit30 extension TypesFileTranslator { /// Returns a list of declarations that define a Swift type for - /// the request body content and type name. + /// the request body content. /// - Parameters: - /// - typeName: The type name to declare the request body type under. - /// - requestBody: The request body to declare. - /// - Returns: A list of declarations; empty list if the request body is + /// - content: The typed schema content to declare. + /// - Returns: A list of declarations; empty list if the content is /// unsupported. func translateRequestBodyContentInTypes( - requestBody: TypedRequestBody + _ content: TypedSchemaContent ) throws -> [Declaration] { - let content = requestBody.content let decl = try translateSchema( typeName: content.resolvedTypeUsage.typeName, schema: content.content.schema, - overrides: .init( - isOptional: !requestBody.request.required, - userDescription: requestBody.request.description - ) + overrides: .none ) return decl } @@ -45,28 +40,30 @@ extension TypesFileTranslator { /// - Parameter requestBody: The request body to declare. /// - Returns: A list of declarations; empty if the request body is /// unsupported. - func requestBodyContentCase( + func requestBodyContentCases( for requestBody: TypedRequestBody ) throws -> [Declaration] { var bodyMembers: [Declaration] = [] - let content = requestBody.content - if TypeMatcher.isInlinable(content.content.schema) { - let inlineTypeDecls = try translateRequestBodyContentInTypes( - requestBody: requestBody + let contents = requestBody.contents + for content in contents { + if TypeMatcher.isInlinable(content.content.schema) { + let inlineTypeDecls = try translateRequestBodyContentInTypes( + content + ) + bodyMembers.append(contentsOf: inlineTypeDecls) + } + let identifier = contentSwiftName(content.content.contentType) + let associatedType = content.resolvedTypeUsage + let contentCase: Declaration = .enumCase( + .init( + name: identifier, + kind: .nameWithAssociatedValues([ + .init(type: associatedType.fullyQualifiedNonOptionalSwiftName) + ]) + ) ) - bodyMembers.append(contentsOf: inlineTypeDecls) + bodyMembers.append(contentCase) } - let identifier = contentSwiftName(content.content.contentType) - let associatedType = content.resolvedTypeUsage - let contentCase: Declaration = .enumCase( - .init( - name: identifier, - kind: .nameWithAssociatedValues([ - .init(type: associatedType.fullyQualifiedNonOptionalSwiftName) - ]) - ) - ) - bodyMembers.append(contentCase) return bodyMembers } @@ -134,7 +131,7 @@ extension TypesFileTranslator { requestBody: TypedRequestBody ) throws -> Declaration { let type = requestBody.typeUsage.typeName - let members = try requestBodyContentCase(for: requestBody) + let members = try requestBodyContentCases(for: requestBody) return translateRequestBodyInTypes( typeName: type, members: members @@ -164,7 +161,7 @@ extension TypesFileTranslator { extension ClientFileTranslator { /// Returns an expression that extracts the specified request body from - /// a property on an Input value to a request. + /// a property on an Input value and sets it on a request. /// - Parameters: /// - requestBody: The request body to extract. /// - requestVariableName: The name of the request variable. @@ -175,7 +172,7 @@ extension ClientFileTranslator { requestVariableName: String, inputVariableName: String ) throws -> Expression { - let contents = [requestBody.content] + let contents = requestBody.contents var cases: [SwitchCaseDescription] = contents.map { typedContent in let content = typedContent.content let contentType = content.contentType @@ -244,73 +241,153 @@ extension ServerFileTranslator { /// - inputTypeName: The type of the Input. /// - Returns: A variable declaration. func translateRequestBodyInServer( - _ requestBody: TypedRequestBody?, + _ requestBody: TypedRequestBody, requestVariableName: String, bodyVariableName: String, inputTypeName: TypeName - ) throws -> Declaration { - guard let requestBody else { - let bodyTypeUsage = - inputTypeName.appending( - swiftComponent: Constants.Operation.Body.typeName - ) - .asUsage - return .variable( - kind: .let, - left: bodyVariableName, - type: bodyTypeUsage.asOptional.fullyQualifiedSwiftName, - right: .literal(.nil) - ) - } + ) throws -> [CodeBlock] { + var codeBlocks: [CodeBlock] = [] - let bodyTypeUsage = requestBody.typeUsage - let typedContent = requestBody.content - let contentTypeUsage = typedContent.resolvedTypeUsage - let content = typedContent.content - let contentType = content.contentType - let contentTypeIdentifier = contentSwiftName(contentType) - let codingStrategyName = contentType.codingStrategy.runtimeName let isOptional = !requestBody.request.required - let transformExpr: Expression = .closureInvocation( - argumentNames: ["value"], - body: [ - .expression( - .dot(contentTypeIdentifier) - .call([ - .init(label: nil, expression: .identifier("value")) - ]) + let contentTypeDecl: Declaration = .variable( + kind: .let, + left: "contentType", + right: .identifier("converter") + .dot("extractContentTypeIfPresent") + .call([ + .init( + label: "in", + expression: .identifier(requestVariableName) + .dot("headerFields") + ) + ]) + ) + codeBlocks.append(.declaration(contentTypeDecl)) + codeBlocks.append( + .declaration( + .variable( + kind: .let, + left: bodyVariableName, + type: requestBody.typeUsage.fullyQualifiedSwiftName ) - ] + ) ) - let initExpr: Expression = .try( - .identifier("converter") - .dot("get\(isOptional ? "Optional" : "Required")RequestBodyAs\(codingStrategyName)") + + func makeIfBranch(typedContent: TypedSchemaContent, isFirstBranch: Bool) -> IfBranch { + let isMatchingContentTypeExpr: Expression = .identifier("converter") + .dot("isMatchingContentType") .call([ .init( - label: nil, - expression: - .identifier( - contentTypeUsage - .fullyQualifiedNonOptionalSwiftName - ) - .dot("self") + label: "received", + expression: .identifier("contentType") ), .init( - label: "from", - expression: .identifier(requestVariableName).dot("body") + label: "expectedRaw", + expression: .literal( + typedContent + .content + .contentType + .headerValueForValidation + ) ), - .init(label: "transforming", expression: transformExpr), ]) + let condition: Expression + if isFirstBranch { + condition = .binaryOperation( + left: .binaryOperation( + left: .identifier("contentType"), + operation: .equals, + right: .literal(.nil) + ), + operation: .booleanOr, + right: isMatchingContentTypeExpr + ) + } else { + condition = isMatchingContentTypeExpr + } + let contentTypeUsage = typedContent.resolvedTypeUsage + let content = typedContent.content + let contentType = content.contentType + let codingStrategyName = contentType.codingStrategy.runtimeName + let transformExpr: Expression = .closureInvocation( + argumentNames: ["value"], + body: [ + .expression( + .dot(contentSwiftName(typedContent.content.contentType)) + .call([ + .init(label: nil, expression: .identifier("value")) + ]) + ) + ] + ) + let bodyExpr: Expression = .try( + .identifier("converter") + .dot("get\(isOptional ? "Optional" : "Required")RequestBodyAs\(codingStrategyName)") + .call([ + .init( + label: nil, + expression: .identifier(contentTypeUsage.fullyQualifiedSwiftName).dot("self") + ), + .init(label: "from", expression: .identifier(requestVariableName).dot("body")), + .init( + label: "transforming", + expression: transformExpr + ), + ]) + ) + return .init( + condition: .try(condition), + body: [ + .expression( + .assignment( + left: .identifier("body"), + right: bodyExpr + ) + ) + ] + ) + } + + let typedContents = requestBody.contents + + let primaryIfBranch = makeIfBranch( + typedContent: typedContents[0], + isFirstBranch: true ) - return .variable( - kind: .let, - left: bodyVariableName, - type: - bodyTypeUsage - .withOptional(isOptional) - .fullyQualifiedSwiftName, - right: initExpr + let elseIfBranches = + typedContents + .dropFirst() + .map { typedContent in + makeIfBranch( + typedContent: typedContent, + isFirstBranch: false + ) + } + + codeBlocks.append( + .expression( + .ifStatement( + ifBranch: primaryIfBranch, + elseIfBranches: elseIfBranches, + elseBody: [ + .expression( + .unaryKeyword( + kind: .throw, + expression: .identifier("converter") + .dot("makeUnexpectedContentTypeError") + .call([ + .init( + label: "contentType", + expression: .identifier("contentType") + ) + ]) + ) + ) + ] + ) + ) ) + return codeBlocks } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/acceptHeaderContentTypes.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/acceptHeaderContentTypes.swift index d7802f77..8c6c3f88 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/acceptHeaderContentTypes.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/acceptHeaderContentTypes.swift @@ -29,12 +29,9 @@ extension FileTranslator { try description .operation .responseOutcomes - .compactMap { outcome in + .flatMap { outcome in let response = try outcome.response.resolve(in: components) - return bestSingleContent( - response.content, - foundIn: description.operationID - ) + return supportedContents(response.content, foundIn: description.operationID) } .map { content in content.contentType diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponse.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponse.swift index a412380a..60c73831 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponse.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponse.swift @@ -60,11 +60,12 @@ extension TypesFileTranslator { let bodyTypeName = typeName.appending( swiftComponent: Constants.Operation.Body.typeName ) - var bodyCases: [Declaration] = [] - if let typedContent = try bestSingleTypedContent( + let typedContents = try supportedTypedContents( response.content, inParent: bodyTypeName - ) { + ) + var bodyCases: [Declaration] = [] + for typedContent in typedContents { let identifier = contentSwiftName(typedContent.content.contentType) let associatedType = typedContent.resolvedTypeUsage if TypeMatcher.isInlinable(typedContent.content.schema), let inlineType = typedContent.typeUsage { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift index 2af82bb3..f446fcc5 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift @@ -142,17 +142,45 @@ extension ClientFileTranslator { codeBlocks.append(.declaration(headersVarDecl)) let headersVarExpr: Expression = .identifier("headers") - let bodyVarExpr: Expression - if let typedContent = try bestSingleTypedContent( + let typedContents = try supportedTypedContents( typedResponse.response.content, inParent: bodyTypeName - ) { - let validateContentTypeExpr: Expression = .try( - .identifier("converter").dot("validateContentTypeIfPresent") + ) + let bodyVarExpr: Expression + if !typedContents.isEmpty { + + let contentTypeDecl: Declaration = .variable( + kind: .let, + left: "contentType", + right: .identifier("converter") + .dot("extractContentTypeIfPresent") + .call([ + .init( + label: "in", + expression: .identifier("response") + .dot("headerFields") + ) + ]) + ) + codeBlocks.append(.declaration(contentTypeDecl)) + + let bodyDecl: Declaration = .variable( + kind: .let, + left: "body", + type: bodyTypeName.fullyQualifiedSwiftName + ) + codeBlocks.append(.declaration(bodyDecl)) + + func makeIfBranch(typedContent: TypedSchemaContent, isFirstBranch: Bool) -> IfBranch { + let isMatchingContentTypeExpr: Expression = .identifier("converter") + .dot("isMatchingContentType") .call([ - .init(label: "in", expression: .identifier("response").dot("headerFields")), .init( - label: "substring", + label: "received", + expression: .identifier("contentType") + ), + .init( + label: "expectedRaw", expression: .literal( typedContent .content @@ -161,26 +189,33 @@ extension ClientFileTranslator { ) ), ]) - ) - codeBlocks.append(.expression(validateContentTypeExpr)) - - let contentTypeUsage = typedContent.resolvedTypeUsage - let transformExpr: Expression = .closureInvocation( - argumentNames: ["value"], - body: [ - .expression( - .dot(contentSwiftName(typedContent.content.contentType)) - .call([ - .init(label: nil, expression: .identifier("value")) - ]) + let condition: Expression + if isFirstBranch { + condition = .binaryOperation( + left: .binaryOperation( + left: .identifier("contentType"), + operation: .equals, + right: .literal(.nil) + ), + operation: .booleanOr, + right: isMatchingContentTypeExpr ) - ] - ) - let bodyDecl: Declaration = .variable( - kind: .let, - left: "body", - type: bodyTypeName.fullyQualifiedSwiftName, - right: .try( + } else { + condition = isMatchingContentTypeExpr + } + let contentTypeUsage = typedContent.resolvedTypeUsage + let transformExpr: Expression = .closureInvocation( + argumentNames: ["value"], + body: [ + .expression( + .dot(contentSwiftName(typedContent.content.contentType)) + .call([ + .init(label: nil, expression: .identifier("value")) + ]) + ) + ] + ) + let bodyExpr: Expression = .try( .identifier("converter") .dot("getResponseBodyAs\(typedContent.content.contentType.codingStrategy.runtimeName)") .call([ @@ -195,8 +230,57 @@ extension ClientFileTranslator { ), ]) ) + return .init( + condition: .try(condition), + body: [ + .expression( + .assignment( + left: .identifier("body"), + right: bodyExpr + ) + ) + ] + ) + } + + let primaryIfBranch = makeIfBranch( + typedContent: typedContents[0], + isFirstBranch: true ) - codeBlocks.append(.declaration(bodyDecl)) + let elseIfBranches = + typedContents + .dropFirst() + .map { typedContent in + makeIfBranch( + typedContent: typedContent, + isFirstBranch: false + ) + } + + codeBlocks.append( + .expression( + .ifStatement( + ifBranch: primaryIfBranch, + elseIfBranches: elseIfBranches, + elseBody: [ + .expression( + .unaryKeyword( + kind: .throw, + expression: .identifier("converter") + .dot("makeUnexpectedContentTypeError") + .call([ + .init( + label: "contentType", + expression: .identifier("contentType") + ) + ]) + ) + ) + ] + ) + ) + ) + bodyVarExpr = .identifier("body") } else { bodyVarExpr = .literal(.nil) @@ -305,13 +389,10 @@ extension ServerFileTranslator { } codeBlocks.append(contentsOf: headerExprs.map { .expression($0) }) - let typedContents = [ - try bestSingleTypedContent( - typedResponse.response.content, - inParent: bodyTypeName - ) - ] - .compactMap { $0 } + let typedContents = try supportedTypedContents( + typedResponse.response.content, + inParent: bodyTypeName + ) if !typedContents.isEmpty { let switchContentCases: [SwitchCaseDescription] = typedContents.map { typedContent in diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift index e0f03575..c3afd31b 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift @@ -24,26 +24,6 @@ extension ServerFileTranslator { var closureBody: [CodeBlock] = [] let typedRequestBody = try typedRequestBody(in: operation) - - if let headerValueForValidation = typedRequestBody? - .content - .content - .contentType - .headerValueForValidation - { - let validateContentTypeExpr: Expression = .try( - .identifier("converter").dot("validateContentTypeIfPresent") - .call([ - .init(label: "in", expression: .identifier("request").dot("headerFields")), - .init( - label: "substring", - expression: .literal(headerValueForValidation) - ), - ]) - ) - closureBody.append(.expression(validateContentTypeExpr)) - } - let inputTypeName = operation.inputTypeName func locationSpecificInputDecl( @@ -69,7 +49,7 @@ extension ServerFileTranslator { ) } - var inputMemberDecls = try [ + var inputMemberCodeBlocks = try [ ( .path, operation.allPathParameters @@ -88,14 +68,21 @@ extension ServerFileTranslator { ), ] .map(locationSpecificInputDecl(locatedIn:fromParameters:)) + .map(CodeBlock.declaration) - let bodyDecl = try translateRequestBodyInServer( - typedRequestBody, - requestVariableName: "request", - bodyVariableName: "body", - inputTypeName: inputTypeName - ) - inputMemberDecls.append(bodyDecl) + let requestBodyExpr: Expression + if let typedRequestBody { + let bodyCodeBlocks = try translateRequestBodyInServer( + typedRequestBody, + requestVariableName: "request", + bodyVariableName: "body", + inputTypeName: inputTypeName + ) + inputMemberCodeBlocks.append(contentsOf: bodyCodeBlocks) + requestBodyExpr = .identifier("body") + } else { + requestBodyExpr = .literal(.nil) + } func functionArgumentForLocation( _ location: OpenAPI.Parameter.Context.Location @@ -113,14 +100,13 @@ extension ServerFileTranslator { functionArgumentForLocation(.query), functionArgumentForLocation(.header), functionArgumentForLocation(.cookie), - .init(label: "body", expression: .identifier("body")), + .init(label: "body", expression: requestBodyExpr), ]) ) closureBody.append( - contentsOf: inputMemberDecls.map(CodeBlock.declaration) + [.expression(returnExpr)] + contentsOf: inputMemberCodeBlocks + [.expression(returnExpr)] ) - return .closureInvocation( argumentNames: ["request", "metadata"], body: closureBody diff --git a/Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift b/Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift index 7974e1ff..8e663f07 100644 --- a/Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift +++ b/Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift @@ -50,6 +50,7 @@ enum ExprKind: String, Equatable, CustomStringConvertible { case functionCall case assignment case `switch` + case `if` case doStatement case valueBinding case unaryKeyword @@ -176,6 +177,8 @@ extension KeywordKind { return hasPostfixQuestionMark ? "try?" : "try" case .await: return "await" + case .throw: + return "throw" } } } @@ -206,6 +209,8 @@ extension Expression { return .init(name: value.left.info.name, kind: .assignment) case .`switch`(let value): return .init(name: value.switchedExpression.info.name, kind: .switch) + case .ifStatement(_): + return .init(name: nil, kind: .if) case .doStatement(_): return .init(name: nil, kind: .doStatement) case .valueBinding(let value): diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/Content/Test_ContentSwiftName.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/Content/Test_ContentSwiftName.swift index 5671ad61..2ea04fba 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/Content/Test_ContentSwiftName.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/Content/Test_ContentSwiftName.swift @@ -33,18 +33,18 @@ final class Test_ContentSwiftName: Test_Core { try _testIdentifiers(cases: cases, nameMaker: nameMaker) } - func testProposed() throws { + func testProposed_multipleContentTypes() throws { let nameMaker = makeTranslator(featureFlags: [.multipleContentTypes]).contentSwiftName let cases: [(String, String)] = [ - ("application/json", "unsupported"), - ("application/x-www-form-urlencoded", "unsupported"), - ("multipart/form-data", "unsupported"), - ("text/plain", "unsupported"), - ("*/*", "unsupported"), - ("application/xml", "unsupported"), - ("application/octet-stream", "unsupported"), - ("application/myformat+json", "unsupported"), - ("foo/bar", "unsupported"), + ("application/json", "json"), + ("application/x-www-form-urlencoded", "form"), + ("multipart/form-data", "multipart"), + ("text/plain", "text"), + ("*/*", "any"), + ("application/xml", "xml"), + ("application/octet-stream", "binary"), + ("application/myformat+json", "application_myformat_json"), + ("foo/bar", "foo_bar"), ] try _testIdentifiers(cases: cases, nameMaker: nameMaker) } diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/RequestBody/Test_translateRequestBody.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/RequestBody/Test_translateRequestBody.swift deleted file mode 100644 index 90711f92..00000000 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/RequestBody/Test_translateRequestBody.swift +++ /dev/null @@ -1,67 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftOpenAPIGenerator open source project -// -// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// -import XCTest -import OpenAPIKit30 -@testable import _OpenAPIGeneratorCore - -final class Test_translateRequestBody_Types: Test_Core { - func testRequestBodyContentCase() throws { - let translator = makeTypesTranslator() - - let bodyTypeName = TypeName( - swiftKeyPath: ["Body"] - ) - - let contentTypeName = bodyTypeName.appending( - swiftComponent: "Foo" - ) - let contentTypeUsage = contentTypeName.asUsage - - let expected: [(TypeUsage, String)] = [ - // Required - (contentTypeUsage, "Body.Foo"), - - // Optional - (contentTypeUsage.asOptional, "Body.Foo"), - ] - - for (typeUsage, associatedType) in expected { - let decls = try translator.requestBodyContentCase( - for: .init( - request: .init(content: [ - .json: .init(schema: .string) - ]), - typeUsage: bodyTypeName.asUsage, - isInlined: false, - content: .init( - content: .init( - contentType: .applicationJSON, - schema: .schema(.string) - ), - typeUsage: typeUsage - ) - ) - ) - XCTAssertEqual(decls.count, 1) - let decl = decls[0] - let expected: Declaration = .enumCase( - name: "json", - kind: .nameWithAssociatedValues([ - .init(type: associatedType) - ]) - ) - XCTAssertEqualCodable(decl, expected) - } - } -} diff --git a/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift index 11e033ca..d69515a2 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift @@ -45,7 +45,22 @@ class FileBasedReferenceTests: XCTestCase { } func testPetstore() throws { - try _test(referenceProject: .init(name: .petstore)) + try _test( + referenceProject: .init(name: .petstore), + ignoredDiagnosticMessages: [ + #"Feature "Multiple content types" is not supported, skipping"# + ] + ) + } + + func testPetstoreFFMultipleContentTypes() throws { + try _test( + referenceProject: .init( + name: .petstore, + customDirectoryName: "Petstore_FF_MultipleContentTypes" + ), + featureFlags: [.multipleContentTypes] + ) } // MARK: - Private diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml b/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml index a367d7f5..85b25524 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml @@ -113,6 +113,31 @@ paths: $ref: '#/components/schemas/Pet' '400': $ref: '#/components/responses/ErrorBadRequest' + /pets/stats: + get: + operationId: getStats + responses: + '200': + description: A successful response. + content: + application/json: + schema: + $ref: '#/components/schemas/PetStats' + text/plain: {} + application/octet-stream: {} + post: + operationId: postStats + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PetStats' + text/plain: {} + application/octet-stream: {} + responses: + '202': + description: Accepted data. /probe/: post: operationId: probe @@ -389,6 +414,13 @@ components: message: type: string deprecated: true + PetStats: + type: object + properties: + count: + type: integer + required: + - count responses: ErrorBadRequest: description: Bad request diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift index 60d3b3c4..363e2337 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift @@ -97,29 +97,45 @@ public struct Client: APIProtocol { as: Components.Headers.TracingHeader.self ) ) - try converter.validateContentTypeIfPresent( - in: response.headerFields, - substring: "application/json" + let contentType = converter.extractContentTypeIfPresent( + in: response.headerFields ) - let body: Operations.listPets.Output.Ok.Body = - try converter.getResponseBodyAsJSON( + let body: Operations.listPets.Output.Ok.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getResponseBodyAsJSON( Components.Schemas.Pets.self, from: response.body, transforming: { value in .json(value) } ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } return .ok(.init(headers: headers, body: body)) default: let headers: Operations.listPets.Output.Default.Headers = .init() - try converter.validateContentTypeIfPresent( - in: response.headerFields, - substring: "application/json" + let contentType = converter.extractContentTypeIfPresent( + in: response.headerFields ) - let body: Operations.listPets.Output.Default.Body = - try converter.getResponseBodyAsJSON( + let body: Operations.listPets.Output.Default.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getResponseBodyAsJSON( Components.Schemas._Error.self, from: response.body, transforming: { value in .json(value) } ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } return .`default`( statusCode: response.statusCode, .init(headers: headers, body: body) @@ -172,16 +188,24 @@ public struct Client: APIProtocol { as: Components.Schemas.CodeError.self ) ) - try converter.validateContentTypeIfPresent( - in: response.headerFields, - substring: "application/json" + let contentType = converter.extractContentTypeIfPresent( + in: response.headerFields ) - let body: Operations.createPet.Output.Created.Body = - try converter.getResponseBodyAsJSON( + let body: Operations.createPet.Output.Created.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getResponseBodyAsJSON( Components.Schemas.Pet.self, from: response.body, transforming: { value in .json(value) } ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } return .created(.init(headers: headers, body: body)) case 400: let headers: Components.Responses.ErrorBadRequest.Headers = .init( @@ -191,22 +215,115 @@ public struct Client: APIProtocol { as: Swift.String.self ) ) - try converter.validateContentTypeIfPresent( - in: response.headerFields, - substring: "application/json" + let contentType = converter.extractContentTypeIfPresent( + in: response.headerFields ) - let body: Components.Responses.ErrorBadRequest.Body = - try converter.getResponseBodyAsJSON( + let body: Components.Responses.ErrorBadRequest.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getResponseBodyAsJSON( Components.Responses.ErrorBadRequest.Body.jsonPayload.self, from: response.body, transforming: { value in .json(value) } ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } return .badRequest(.init(headers: headers, body: body)) default: return .undocumented(statusCode: response.statusCode, .init()) } } ) } + /// - Remark: HTTP `GET /pets/stats`. + /// - Remark: Generated from `#/paths//pets/stats/get(getStats)`. + public func getStats(_ input: Operations.getStats.Input) async throws + -> Operations.getStats.Output + { + try await client.send( + input: input, + forOperation: Operations.getStats.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/pets/stats", + parameters: [] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .get) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.getStats.Output.Ok.Headers = .init() + let contentType = converter.extractContentTypeIfPresent( + in: response.headerFields + ) + let body: Operations.getStats.Output.Ok.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getResponseBodyAsJSON( + Components.Schemas.PetStats.self, + from: response.body, + transforming: { value in .json(value) } + ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } + return .ok(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// - Remark: HTTP `POST /pets/stats`. + /// - Remark: Generated from `#/paths//pets/stats/post(postStats)`. + public func postStats(_ input: Operations.postStats.Input) async throws + -> Operations.postStats.Output + { + try await client.send( + input: input, + forOperation: Operations.postStats.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/pets/stats", + parameters: [] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + switch input.body { + case let .json(value): + request.body = try converter.setRequiredRequestBodyAsJSON( + value, + headerFields: &request.headerFields, + contentType: "application/json; charset=utf-8" + ) + } + return request + }, + deserializer: { response in + switch response.statusCode { + case 202: + let headers: Operations.postStats.Output.Accepted.Headers = .init() + return .accepted(.init(headers: headers, body: nil)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } /// - Remark: HTTP `POST /probe/`. /// - Remark: Generated from `#/paths//probe//post(probe)`. @available(*, deprecated) public func probe(_ input: Operations.probe.Input) async throws @@ -271,16 +388,24 @@ public struct Client: APIProtocol { return .noContent(.init(headers: headers, body: nil)) case 400: let headers: Operations.updatePet.Output.BadRequest.Headers = .init() - try converter.validateContentTypeIfPresent( - in: response.headerFields, - substring: "application/json" + let contentType = converter.extractContentTypeIfPresent( + in: response.headerFields ) - let body: Operations.updatePet.Output.BadRequest.Body = - try converter.getResponseBodyAsJSON( + let body: Operations.updatePet.Output.BadRequest.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getResponseBodyAsJSON( Operations.updatePet.Output.BadRequest.Body.jsonPayload.self, from: response.body, transforming: { value in .json(value) } ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } return .badRequest(.init(headers: headers, body: body)) default: return .undocumented(statusCode: response.statusCode, .init()) } @@ -323,44 +448,68 @@ public struct Client: APIProtocol { switch response.statusCode { case 200: let headers: Operations.uploadAvatarForPet.Output.Ok.Headers = .init() - try converter.validateContentTypeIfPresent( - in: response.headerFields, - substring: "application/octet-stream" + let contentType = converter.extractContentTypeIfPresent( + in: response.headerFields ) - let body: Operations.uploadAvatarForPet.Output.Ok.Body = - try converter.getResponseBodyAsBinary( + let body: Operations.uploadAvatarForPet.Output.Ok.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/octet-stream" + ) + { + body = try converter.getResponseBodyAsBinary( Foundation.Data.self, from: response.body, transforming: { value in .binary(value) } ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } return .ok(.init(headers: headers, body: body)) case 412: let headers: Operations.uploadAvatarForPet.Output.PreconditionFailed.Headers = .init() - try converter.validateContentTypeIfPresent( - in: response.headerFields, - substring: "application/json" + let contentType = converter.extractContentTypeIfPresent( + in: response.headerFields ) - let body: Operations.uploadAvatarForPet.Output.PreconditionFailed.Body = - try converter.getResponseBodyAsJSON( + let body: Operations.uploadAvatarForPet.Output.PreconditionFailed.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getResponseBodyAsJSON( Swift.String.self, from: response.body, transforming: { value in .json(value) } ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } return .preconditionFailed(.init(headers: headers, body: body)) case 500: let headers: Operations.uploadAvatarForPet.Output.InternalServerError.Headers = .init() - try converter.validateContentTypeIfPresent( - in: response.headerFields, - substring: "text/plain" + let contentType = converter.extractContentTypeIfPresent( + in: response.headerFields ) - let body: Operations.uploadAvatarForPet.Output.InternalServerError.Body = - try converter.getResponseBodyAsText( + let body: Operations.uploadAvatarForPet.Output.InternalServerError.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "text/plain" + ) + { + body = try converter.getResponseBodyAsText( Swift.String.self, from: response.body, transforming: { value in .text(value) } ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } return .internalServerError(.init(headers: headers, body: body)) default: return .undocumented(statusCode: response.statusCode, .init()) } diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift index fc3dc9a7..cd60e0cd 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift @@ -37,6 +37,18 @@ extension APIProtocol { path: server.apiPathComponentsWithServerPrefix(["pets"]), queryItemNames: [] ) + try transport.register( + { try await server.getStats(request: $0, metadata: $1) }, + method: .get, + path: server.apiPathComponentsWithServerPrefix(["pets", "stats"]), + queryItemNames: [] + ) + try transport.register( + { try await server.postStats(request: $0, metadata: $1) }, + method: .post, + path: server.apiPathComponentsWithServerPrefix(["pets", "stats"]), + queryItemNames: [] + ) try transport.register( { try await server.probe(request: $0, metadata: $1) }, method: .post, @@ -102,13 +114,12 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { ) ) let cookies: Operations.listPets.Input.Cookies = .init() - let body: Operations.listPets.Input.Body? = nil return Operations.listPets.Input( path: path, query: query, headers: headers, cookies: cookies, - body: body + body: nil ) }, serializer: { output, request in @@ -171,12 +182,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { with: metadata, forOperation: Operations.createPet.id, using: { APIHandler.createPet($0) }, - deserializer: { request, metadata in - try converter.validateContentTypeIfPresent( - in: request.headerFields, - substring: "application/json" - ) - let path: Operations.createPet.Input.Path = .init() + deserializer: { request, metadata in let path: Operations.createPet.Input.Path = .init() let query: Operations.createPet.Input.Query = .init() let headers: Operations.createPet.Input.Headers = .init( X_Extra_Arguments: try converter.getOptionalHeaderFieldAsJSON( @@ -186,12 +192,22 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { ) ) let cookies: Operations.createPet.Input.Cookies = .init() - let body: Operations.createPet.Input.Body = - try converter.getRequiredRequestBodyAsJSON( + let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) + let body: Operations.createPet.Input.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getRequiredRequestBodyAsJSON( Components.Schemas.CreatePetRequest.self, from: request.body, transforming: { value in .json(value) } ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } return Operations.createPet.Input( path: path, query: query, @@ -251,6 +267,98 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { } ) } + /// - Remark: HTTP `GET /pets/stats`. + /// - Remark: Generated from `#/paths//pets/stats/get(getStats)`. + func getStats(request: Request, metadata: ServerRequestMetadata) async throws -> Response { + try await handle( + request: request, + with: metadata, + forOperation: Operations.getStats.id, + using: { APIHandler.getStats($0) }, + deserializer: { request, metadata in let path: Operations.getStats.Input.Path = .init() + let query: Operations.getStats.Input.Query = .init() + let headers: Operations.getStats.Input.Headers = .init() + let cookies: Operations.getStats.Input.Cookies = .init() + return Operations.getStats.Input( + path: path, + query: query, + headers: headers, + cookies: cookies, + body: nil + ) + }, + serializer: { output, request in + switch output { + case let .ok(value): + suppressUnusedWarning(value) + var response = Response(statusCode: 200) + suppressMutabilityWarning(&response) + switch value.body { + case let .json(value): + try converter.validateAcceptIfPresent( + "application/json", + in: request.headerFields + ) + response.body = try converter.setResponseBodyAsJSON( + value, + headerFields: &response.headerFields, + contentType: "application/json; charset=utf-8" + ) + } + return response + case let .undocumented(statusCode, _): return .init(statusCode: statusCode) + } + } + ) + } + /// - Remark: HTTP `POST /pets/stats`. + /// - Remark: Generated from `#/paths//pets/stats/post(postStats)`. + func postStats(request: Request, metadata: ServerRequestMetadata) async throws -> Response { + try await handle( + request: request, + with: metadata, + forOperation: Operations.postStats.id, + using: { APIHandler.postStats($0) }, + deserializer: { request, metadata in let path: Operations.postStats.Input.Path = .init() + let query: Operations.postStats.Input.Query = .init() + let headers: Operations.postStats.Input.Headers = .init() + let cookies: Operations.postStats.Input.Cookies = .init() + let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) + let body: Operations.postStats.Input.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getRequiredRequestBodyAsJSON( + Components.Schemas.PetStats.self, + from: request.body, + transforming: { value in .json(value) } + ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } + return Operations.postStats.Input( + path: path, + query: query, + headers: headers, + cookies: cookies, + body: body + ) + }, + serializer: { output, request in + switch output { + case let .accepted(value): + suppressUnusedWarning(value) + var response = Response(statusCode: 202) + suppressMutabilityWarning(&response) + return response + case let .undocumented(statusCode, _): return .init(statusCode: statusCode) + } + } + ) + } /// - Remark: HTTP `POST /probe/`. /// - Remark: Generated from `#/paths//probe//post(probe)`. @available(*, deprecated) func probe(request: Request, metadata: ServerRequestMetadata) @@ -265,13 +373,12 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { let query: Operations.probe.Input.Query = .init() let headers: Operations.probe.Input.Headers = .init() let cookies: Operations.probe.Input.Cookies = .init() - let body: Operations.probe.Input.Body? = nil return Operations.probe.Input( path: path, query: query, headers: headers, cookies: cookies, - body: body + body: nil ) }, serializer: { output, request in @@ -297,10 +404,6 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { forOperation: Operations.updatePet.id, using: { APIHandler.updatePet($0) }, deserializer: { request, metadata in - try converter.validateContentTypeIfPresent( - in: request.headerFields, - substring: "application/json" - ) let path: Operations.updatePet.Input.Path = .init( petId: try converter.getPathParameterAsText( in: metadata.pathParameters, @@ -311,12 +414,22 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { let query: Operations.updatePet.Input.Query = .init() let headers: Operations.updatePet.Input.Headers = .init() let cookies: Operations.updatePet.Input.Cookies = .init() - let body: Components.RequestBodies.UpdatePetRequest? = - try converter.getOptionalRequestBodyAsJSON( + let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) + let body: Components.RequestBodies.UpdatePetRequest? + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getOptionalRequestBodyAsJSON( Components.RequestBodies.UpdatePetRequest.jsonPayload.self, from: request.body, transforming: { value in .json(value) } ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } return Operations.updatePet.Input( path: path, query: query, @@ -367,10 +480,6 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { forOperation: Operations.uploadAvatarForPet.id, using: { APIHandler.uploadAvatarForPet($0) }, deserializer: { request, metadata in - try converter.validateContentTypeIfPresent( - in: request.headerFields, - substring: "application/octet-stream" - ) let path: Operations.uploadAvatarForPet.Input.Path = .init( petId: try converter.getPathParameterAsText( in: metadata.pathParameters, @@ -381,12 +490,22 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { let query: Operations.uploadAvatarForPet.Input.Query = .init() let headers: Operations.uploadAvatarForPet.Input.Headers = .init() let cookies: Operations.uploadAvatarForPet.Input.Cookies = .init() - let body: Operations.uploadAvatarForPet.Input.Body = - try converter.getRequiredRequestBodyAsBinary( + let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) + let body: Operations.uploadAvatarForPet.Input.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/octet-stream" + ) + { + body = try converter.getRequiredRequestBodyAsBinary( Foundation.Data.self, from: request.body, transforming: { value in .binary(value) } ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } return Operations.uploadAvatarForPet.Input( path: path, query: query, diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift index e8cadeb1..aaedc0cd 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift @@ -20,6 +20,12 @@ public protocol APIProtocol: Sendable { /// - Remark: HTTP `POST /pets`. /// - Remark: Generated from `#/paths//pets/post(createPet)`. func createPet(_ input: Operations.createPet.Input) async throws -> Operations.createPet.Output + /// - Remark: HTTP `GET /pets/stats`. + /// - Remark: Generated from `#/paths//pets/stats/get(getStats)`. + func getStats(_ input: Operations.getStats.Input) async throws -> Operations.getStats.Output + /// - Remark: HTTP `POST /pets/stats`. + /// - Remark: Generated from `#/paths//pets/stats/post(postStats)`. + func postStats(_ input: Operations.postStats.Input) async throws -> Operations.postStats.Output /// - Remark: HTTP `POST /probe/`. /// - Remark: Generated from `#/paths//probe//post(probe)`. @available(*, deprecated) func probe(_ input: Operations.probe.Input) async throws @@ -601,6 +607,17 @@ public enum Components { public init(message: Swift.String? = nil) { self.message = message } public enum CodingKeys: String, CodingKey { case message } } + /// - Remark: Generated from `#/components/schemas/PetStats`. + public struct PetStats: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/PetStats/count`. + public var count: Swift.Int + /// Creates a new `PetStats`. + /// + /// - Parameters: + /// - count: + public init(count: Swift.Int) { self.count = count } + public enum CodingKeys: String, CodingKey { case count } + } } /// Types generated from the `#/components/parameters` section of the OpenAPI document. public enum Parameters { @@ -1033,6 +1050,180 @@ public enum Operations { case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) } } + /// - Remark: HTTP `GET /pets/stats`. + /// - Remark: Generated from `#/paths//pets/stats/get(getStats)`. + public enum getStats { + public static let id: String = "getStats" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + /// Creates a new `Path`. + public init() {} + } + public var path: Operations.getStats.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.getStats.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.getStats.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.getStats.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.getStats.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.getStats.Input.Path = .init(), + query: Operations.getStats.Input.Query = .init(), + headers: Operations.getStats.Input.Headers = .init(), + cookies: Operations.getStats.Input.Cookies = .init(), + body: Operations.getStats.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.getStats.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.PetStats) + } + /// Received HTTP response body + public var body: Operations.getStats.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.getStats.Output.Ok.Headers = .init(), + body: Operations.getStats.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// A successful response. + /// + /// - Remark: Generated from `#/paths//pets/stats/get(getStats)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.getStats.Output.Ok) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// - Remark: HTTP `POST /pets/stats`. + /// - Remark: Generated from `#/paths//pets/stats/post(postStats)`. + public enum postStats { + public static let id: String = "postStats" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + /// Creates a new `Path`. + public init() {} + } + public var path: Operations.postStats.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.postStats.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.postStats.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.postStats.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.PetStats) + } + public var body: Operations.postStats.Input.Body + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.postStats.Input.Path = .init(), + query: Operations.postStats.Input.Query = .init(), + headers: Operations.postStats.Input.Headers = .init(), + cookies: Operations.postStats.Input.Cookies = .init(), + body: Operations.postStats.Input.Body + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Accepted: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.postStats.Output.Accepted.Headers + @frozen public enum Body: Sendable, Equatable, Hashable {} + /// Received HTTP response body + public var body: Operations.postStats.Output.Accepted.Body? + /// Creates a new `Accepted`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.postStats.Output.Accepted.Headers = .init(), + body: Operations.postStats.Output.Accepted.Body? = nil + ) { + self.headers = headers + self.body = body + } + } + /// Accepted data. + /// + /// - Remark: Generated from `#/paths//pets/stats/post(postStats)/responses/202`. + /// + /// HTTP response code: `202 accepted`. + case accepted(Operations.postStats.Output.Accepted) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } /// - Remark: HTTP `POST /probe/`. /// - Remark: Generated from `#/paths//probe//post(probe)`. public enum probe { diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Client.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Client.swift new file mode 100644 index 00000000..2231d78c --- /dev/null +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Client.swift @@ -0,0 +1,549 @@ +// Generated by swift-openapi-generator, do not modify. +@_spi(Generated) import OpenAPIRuntime +#if os(Linux) +@preconcurrency import Foundation +#else +import Foundation +#endif +/// Service for managing pet metadata. +/// +/// Because why not. +public struct Client: APIProtocol { + /// The underlying HTTP client. + private let client: UniversalClient + /// Creates a new client. + /// - Parameters: + /// - serverURL: The server URL that the client connects to. Any server + /// URLs defined in the OpenAPI document are available as static methods + /// on the ``Servers`` type. + /// - configuration: A set of configuration values for the client. + /// - transport: A transport that performs HTTP operations. + /// - middlewares: A list of middlewares to call before the transport. + public init( + serverURL: URL, + configuration: Configuration = .init(), + transport: any ClientTransport, + middlewares: [any ClientMiddleware] = [] + ) { + self.client = .init( + serverURL: serverURL, + configuration: configuration, + transport: transport, + middlewares: middlewares + ) + } + private var converter: Converter { client.converter } + /// List all pets + /// + /// You can fetch + /// all the pets here + /// + /// - Remark: HTTP `GET /pets`. + /// - Remark: Generated from `#/paths//pets/get(listPets)`. + public func listPets(_ input: Operations.listPets.Input) async throws + -> Operations.listPets.Output + { + try await client.send( + input: input, + forOperation: Operations.listPets.id, + serializer: { input in + let path = try converter.renderedRequestPath(template: "/pets", parameters: []) + var request: OpenAPIRuntime.Request = .init(path: path, method: .get) + suppressMutabilityWarning(&request) + try converter.setQueryItemAsText( + in: &request, + name: "limit", + value: input.query.limit + ) + try converter.setQueryItemAsText( + in: &request, + name: "habitat", + value: input.query.habitat + ) + try converter.setQueryItemAsText( + in: &request, + name: "feeds", + value: input.query.feeds + ) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "My-Request-UUID", + value: input.headers.My_Request_UUID + ) + try converter.setQueryItemAsText( + in: &request, + name: "since", + value: input.query.since + ) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.listPets.Output.Ok.Headers = .init( + My_Response_UUID: try converter.getRequiredHeaderFieldAsText( + in: response.headerFields, + name: "My-Response-UUID", + as: Swift.String.self + ), + My_Tracing_Header: try converter.getOptionalHeaderFieldAsText( + in: response.headerFields, + name: "My-Tracing-Header", + as: Components.Headers.TracingHeader.self + ) + ) + let contentType = converter.extractContentTypeIfPresent( + in: response.headerFields + ) + let body: Operations.listPets.Output.Ok.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getResponseBodyAsJSON( + Components.Schemas.Pets.self, + from: response.body, + transforming: { value in .json(value) } + ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } + return .ok(.init(headers: headers, body: body)) + default: + let headers: Operations.listPets.Output.Default.Headers = .init() + let contentType = converter.extractContentTypeIfPresent( + in: response.headerFields + ) + let body: Operations.listPets.Output.Default.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } + return .`default`( + statusCode: response.statusCode, + .init(headers: headers, body: body) + ) + } + } + ) + } + /// Create a pet + /// + /// - Remark: HTTP `POST /pets`. + /// - Remark: Generated from `#/paths//pets/post(createPet)`. + public func createPet(_ input: Operations.createPet.Input) async throws + -> Operations.createPet.Output + { + try await client.send( + input: input, + forOperation: Operations.createPet.id, + serializer: { input in + let path = try converter.renderedRequestPath(template: "/pets", parameters: []) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsJSON( + in: &request.headerFields, + name: "X-Extra-Arguments", + value: input.headers.X_Extra_Arguments + ) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + switch input.body { + case let .json(value): + request.body = try converter.setRequiredRequestBodyAsJSON( + value, + headerFields: &request.headerFields, + contentType: "application/json; charset=utf-8" + ) + } + return request + }, + deserializer: { response in + switch response.statusCode { + case 201: + let headers: Operations.createPet.Output.Created.Headers = .init( + X_Extra_Arguments: try converter.getOptionalHeaderFieldAsJSON( + in: response.headerFields, + name: "X-Extra-Arguments", + as: Components.Schemas.CodeError.self + ) + ) + let contentType = converter.extractContentTypeIfPresent( + in: response.headerFields + ) + let body: Operations.createPet.Output.Created.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getResponseBodyAsJSON( + Components.Schemas.Pet.self, + from: response.body, + transforming: { value in .json(value) } + ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } + return .created(.init(headers: headers, body: body)) + case 400: + let headers: Components.Responses.ErrorBadRequest.Headers = .init( + X_Reason: try converter.getOptionalHeaderFieldAsText( + in: response.headerFields, + name: "X-Reason", + as: Swift.String.self + ) + ) + let contentType = converter.extractContentTypeIfPresent( + in: response.headerFields + ) + let body: Components.Responses.ErrorBadRequest.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getResponseBodyAsJSON( + Components.Responses.ErrorBadRequest.Body.jsonPayload.self, + from: response.body, + transforming: { value in .json(value) } + ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } + return .badRequest(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// - Remark: HTTP `GET /pets/stats`. + /// - Remark: Generated from `#/paths//pets/stats/get(getStats)`. + public func getStats(_ input: Operations.getStats.Input) async throws + -> Operations.getStats.Output + { + try await client.send( + input: input, + forOperation: Operations.getStats.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/pets/stats", + parameters: [] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .get) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json, text/plain, application/octet-stream" + ) + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.getStats.Output.Ok.Headers = .init() + let contentType = converter.extractContentTypeIfPresent( + in: response.headerFields + ) + let body: Operations.getStats.Output.Ok.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getResponseBodyAsJSON( + Components.Schemas.PetStats.self, + from: response.body, + transforming: { value in .json(value) } + ) + } else if try converter.isMatchingContentType( + received: contentType, + expectedRaw: "text/plain" + ) { + body = try converter.getResponseBodyAsText( + Swift.String.self, + from: response.body, + transforming: { value in .text(value) } + ) + } else if try converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/octet-stream" + ) { + body = try converter.getResponseBodyAsBinary( + Foundation.Data.self, + from: response.body, + transforming: { value in .binary(value) } + ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } + return .ok(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// - Remark: HTTP `POST /pets/stats`. + /// - Remark: Generated from `#/paths//pets/stats/post(postStats)`. + public func postStats(_ input: Operations.postStats.Input) async throws + -> Operations.postStats.Output + { + try await client.send( + input: input, + forOperation: Operations.postStats.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/pets/stats", + parameters: [] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + switch input.body { + case let .json(value): + request.body = try converter.setRequiredRequestBodyAsJSON( + value, + headerFields: &request.headerFields, + contentType: "application/json; charset=utf-8" + ) + case let .text(value): + request.body = try converter.setRequiredRequestBodyAsText( + value, + headerFields: &request.headerFields, + contentType: "text/plain" + ) + case let .binary(value): + request.body = try converter.setRequiredRequestBodyAsBinary( + value, + headerFields: &request.headerFields, + contentType: "application/octet-stream" + ) + } + return request + }, + deserializer: { response in + switch response.statusCode { + case 202: + let headers: Operations.postStats.Output.Accepted.Headers = .init() + return .accepted(.init(headers: headers, body: nil)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// - Remark: HTTP `POST /probe/`. + /// - Remark: Generated from `#/paths//probe//post(probe)`. + @available(*, deprecated) public func probe(_ input: Operations.probe.Input) async throws + -> Operations.probe.Output + { + try await client.send( + input: input, + forOperation: Operations.probe.id, + serializer: { input in + let path = try converter.renderedRequestPath(template: "/probe/", parameters: []) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) + suppressMutabilityWarning(&request) + return request + }, + deserializer: { response in + switch response.statusCode { + case 204: + let headers: Operations.probe.Output.NoContent.Headers = .init() + return .noContent(.init(headers: headers, body: nil)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Update just a specific property of an existing pet. Nothing is updated if no request body is provided. + /// + /// - Remark: HTTP `PATCH /pets/{petId}`. + /// - Remark: Generated from `#/paths//pets/{petId}/patch(updatePet)`. + public func updatePet(_ input: Operations.updatePet.Input) async throws + -> Operations.updatePet.Output + { + try await client.send( + input: input, + forOperation: Operations.updatePet.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/pets/{}", + parameters: [input.path.petId] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .patch) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/json" + ) + switch input.body { + case .none: request.body = nil + case let .json(value): + request.body = try converter.setOptionalRequestBodyAsJSON( + value, + headerFields: &request.headerFields, + contentType: "application/json; charset=utf-8" + ) + } + return request + }, + deserializer: { response in + switch response.statusCode { + case 204: + let headers: Operations.updatePet.Output.NoContent.Headers = .init() + return .noContent(.init(headers: headers, body: nil)) + case 400: + let headers: Operations.updatePet.Output.BadRequest.Headers = .init() + let contentType = converter.extractContentTypeIfPresent( + in: response.headerFields + ) + let body: Operations.updatePet.Output.BadRequest.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getResponseBodyAsJSON( + Operations.updatePet.Output.BadRequest.Body.jsonPayload.self, + from: response.body, + transforming: { value in .json(value) } + ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } + return .badRequest(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } + /// Upload an avatar + /// + /// - Remark: HTTP `PUT /pets/{petId}/avatar`. + /// - Remark: Generated from `#/paths//pets/{petId}/avatar/put(uploadAvatarForPet)`. + public func uploadAvatarForPet(_ input: Operations.uploadAvatarForPet.Input) async throws + -> Operations.uploadAvatarForPet.Output + { + try await client.send( + input: input, + forOperation: Operations.uploadAvatarForPet.id, + serializer: { input in + let path = try converter.renderedRequestPath( + template: "/pets/{}/avatar", + parameters: [input.path.petId] + ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .put) + suppressMutabilityWarning(&request) + try converter.setHeaderFieldAsText( + in: &request.headerFields, + name: "accept", + value: "application/octet-stream, application/json, text/plain" + ) + switch input.body { + case let .binary(value): + request.body = try converter.setRequiredRequestBodyAsBinary( + value, + headerFields: &request.headerFields, + contentType: "application/octet-stream" + ) + } + return request + }, + deserializer: { response in + switch response.statusCode { + case 200: + let headers: Operations.uploadAvatarForPet.Output.Ok.Headers = .init() + let contentType = converter.extractContentTypeIfPresent( + in: response.headerFields + ) + let body: Operations.uploadAvatarForPet.Output.Ok.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/octet-stream" + ) + { + body = try converter.getResponseBodyAsBinary( + Foundation.Data.self, + from: response.body, + transforming: { value in .binary(value) } + ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } + return .ok(.init(headers: headers, body: body)) + case 412: + let headers: Operations.uploadAvatarForPet.Output.PreconditionFailed.Headers = + .init() + let contentType = converter.extractContentTypeIfPresent( + in: response.headerFields + ) + let body: Operations.uploadAvatarForPet.Output.PreconditionFailed.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getResponseBodyAsJSON( + Swift.String.self, + from: response.body, + transforming: { value in .json(value) } + ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } + return .preconditionFailed(.init(headers: headers, body: body)) + case 500: + let headers: Operations.uploadAvatarForPet.Output.InternalServerError.Headers = + .init() + let contentType = converter.extractContentTypeIfPresent( + in: response.headerFields + ) + let body: Operations.uploadAvatarForPet.Output.InternalServerError.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "text/plain" + ) + { + body = try converter.getResponseBodyAsText( + Swift.String.self, + from: response.body, + transforming: { value in .text(value) } + ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } + return .internalServerError(.init(headers: headers, body: body)) + default: return .undocumented(statusCode: response.statusCode, .init()) + } + } + ) + } +} diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Server.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Server.swift new file mode 100644 index 00000000..1a3753f8 --- /dev/null +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Server.swift @@ -0,0 +1,613 @@ +// Generated by swift-openapi-generator, do not modify. +@_spi(Generated) import OpenAPIRuntime +#if os(Linux) +@preconcurrency import Foundation +#else +import Foundation +#endif +extension APIProtocol { + /// Registers each operation handler with the provided transport. + /// - Parameters: + /// - transport: A transport to which to register the operation handlers. + /// - serverURL: A URL used to determine the path prefix for registered + /// request handlers. + /// - configuration: A set of configuration values for the server. + /// - middlewares: A list of middlewares to call before the handler. + public func registerHandlers( + on transport: any ServerTransport, + serverURL: URL = .defaultOpenAPIServerURL, + configuration: Configuration = .init(), + middlewares: [any ServerMiddleware] = [] + ) throws { + let server = UniversalServer( + serverURL: serverURL, + handler: self, + configuration: configuration, + middlewares: middlewares + ) + try transport.register( + { try await server.listPets(request: $0, metadata: $1) }, + method: .get, + path: server.apiPathComponentsWithServerPrefix(["pets"]), + queryItemNames: ["limit", "habitat", "feeds", "since"] + ) + try transport.register( + { try await server.createPet(request: $0, metadata: $1) }, + method: .post, + path: server.apiPathComponentsWithServerPrefix(["pets"]), + queryItemNames: [] + ) + try transport.register( + { try await server.getStats(request: $0, metadata: $1) }, + method: .get, + path: server.apiPathComponentsWithServerPrefix(["pets", "stats"]), + queryItemNames: [] + ) + try transport.register( + { try await server.postStats(request: $0, metadata: $1) }, + method: .post, + path: server.apiPathComponentsWithServerPrefix(["pets", "stats"]), + queryItemNames: [] + ) + try transport.register( + { try await server.probe(request: $0, metadata: $1) }, + method: .post, + path: server.apiPathComponentsWithServerPrefix(["probe"]), + queryItemNames: [] + ) + try transport.register( + { try await server.updatePet(request: $0, metadata: $1) }, + method: .patch, + path: server.apiPathComponentsWithServerPrefix(["pets", ":petId"]), + queryItemNames: [] + ) + try transport.register( + { try await server.uploadAvatarForPet(request: $0, metadata: $1) }, + method: .put, + path: server.apiPathComponentsWithServerPrefix(["pets", ":petId", "avatar"]), + queryItemNames: [] + ) + } +} +fileprivate extension UniversalServer where APIHandler: APIProtocol { + /// List all pets + /// + /// You can fetch + /// all the pets here + /// + /// - Remark: HTTP `GET /pets`. + /// - Remark: Generated from `#/paths//pets/get(listPets)`. + func listPets(request: Request, metadata: ServerRequestMetadata) async throws -> Response { + try await handle( + request: request, + with: metadata, + forOperation: Operations.listPets.id, + using: { APIHandler.listPets($0) }, + deserializer: { request, metadata in let path: Operations.listPets.Input.Path = .init() + let query: Operations.listPets.Input.Query = .init( + limit: try converter.getOptionalQueryItemAsText( + in: metadata.queryParameters, + name: "limit", + as: Swift.Int32.self + ), + habitat: try converter.getOptionalQueryItemAsText( + in: metadata.queryParameters, + name: "habitat", + as: Operations.listPets.Input.Query.habitatPayload.self + ), + feeds: try converter.getOptionalQueryItemAsText( + in: metadata.queryParameters, + name: "feeds", + as: Operations.listPets.Input.Query.feedsPayload.self + ), + since: try converter.getOptionalQueryItemAsText( + in: metadata.queryParameters, + name: "since", + as: Components.Parameters.query_born_since.self + ) + ) + let headers: Operations.listPets.Input.Headers = .init( + My_Request_UUID: try converter.getOptionalHeaderFieldAsText( + in: request.headerFields, + name: "My-Request-UUID", + as: Swift.String.self + ) + ) + let cookies: Operations.listPets.Input.Cookies = .init() + return Operations.listPets.Input( + path: path, + query: query, + headers: headers, + cookies: cookies, + body: nil + ) + }, + serializer: { output, request in + switch output { + case let .ok(value): + suppressUnusedWarning(value) + var response = Response(statusCode: 200) + suppressMutabilityWarning(&response) + try converter.setHeaderFieldAsText( + in: &response.headerFields, + name: "My-Response-UUID", + value: value.headers.My_Response_UUID + ) + try converter.setHeaderFieldAsText( + in: &response.headerFields, + name: "My-Tracing-Header", + value: value.headers.My_Tracing_Header + ) + switch value.body { + case let .json(value): + try converter.validateAcceptIfPresent( + "application/json", + in: request.headerFields + ) + response.body = try converter.setResponseBodyAsJSON( + value, + headerFields: &response.headerFields, + contentType: "application/json; charset=utf-8" + ) + } + return response + case let .`default`(statusCode, value): + suppressUnusedWarning(value) + var response = Response(statusCode: statusCode) + suppressMutabilityWarning(&response) + switch value.body { + case let .json(value): + try converter.validateAcceptIfPresent( + "application/json", + in: request.headerFields + ) + response.body = try converter.setResponseBodyAsJSON( + value, + headerFields: &response.headerFields, + contentType: "application/json; charset=utf-8" + ) + } + return response + } + } + ) + } + /// Create a pet + /// + /// - Remark: HTTP `POST /pets`. + /// - Remark: Generated from `#/paths//pets/post(createPet)`. + func createPet(request: Request, metadata: ServerRequestMetadata) async throws -> Response { + try await handle( + request: request, + with: metadata, + forOperation: Operations.createPet.id, + using: { APIHandler.createPet($0) }, + deserializer: { request, metadata in let path: Operations.createPet.Input.Path = .init() + let query: Operations.createPet.Input.Query = .init() + let headers: Operations.createPet.Input.Headers = .init( + X_Extra_Arguments: try converter.getOptionalHeaderFieldAsJSON( + in: request.headerFields, + name: "X-Extra-Arguments", + as: Components.Schemas.CodeError.self + ) + ) + let cookies: Operations.createPet.Input.Cookies = .init() + let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) + let body: Operations.createPet.Input.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getRequiredRequestBodyAsJSON( + Components.Schemas.CreatePetRequest.self, + from: request.body, + transforming: { value in .json(value) } + ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } + return Operations.createPet.Input( + path: path, + query: query, + headers: headers, + cookies: cookies, + body: body + ) + }, + serializer: { output, request in + switch output { + case let .created(value): + suppressUnusedWarning(value) + var response = Response(statusCode: 201) + suppressMutabilityWarning(&response) + try converter.setHeaderFieldAsJSON( + in: &response.headerFields, + name: "X-Extra-Arguments", + value: value.headers.X_Extra_Arguments + ) + switch value.body { + case let .json(value): + try converter.validateAcceptIfPresent( + "application/json", + in: request.headerFields + ) + response.body = try converter.setResponseBodyAsJSON( + value, + headerFields: &response.headerFields, + contentType: "application/json; charset=utf-8" + ) + } + return response + case let .badRequest(value): + suppressUnusedWarning(value) + var response = Response(statusCode: 400) + suppressMutabilityWarning(&response) + try converter.setHeaderFieldAsText( + in: &response.headerFields, + name: "X-Reason", + value: value.headers.X_Reason + ) + switch value.body { + case let .json(value): + try converter.validateAcceptIfPresent( + "application/json", + in: request.headerFields + ) + response.body = try converter.setResponseBodyAsJSON( + value, + headerFields: &response.headerFields, + contentType: "application/json; charset=utf-8" + ) + } + return response + case let .undocumented(statusCode, _): return .init(statusCode: statusCode) + } + } + ) + } + /// - Remark: HTTP `GET /pets/stats`. + /// - Remark: Generated from `#/paths//pets/stats/get(getStats)`. + func getStats(request: Request, metadata: ServerRequestMetadata) async throws -> Response { + try await handle( + request: request, + with: metadata, + forOperation: Operations.getStats.id, + using: { APIHandler.getStats($0) }, + deserializer: { request, metadata in let path: Operations.getStats.Input.Path = .init() + let query: Operations.getStats.Input.Query = .init() + let headers: Operations.getStats.Input.Headers = .init() + let cookies: Operations.getStats.Input.Cookies = .init() + return Operations.getStats.Input( + path: path, + query: query, + headers: headers, + cookies: cookies, + body: nil + ) + }, + serializer: { output, request in + switch output { + case let .ok(value): + suppressUnusedWarning(value) + var response = Response(statusCode: 200) + suppressMutabilityWarning(&response) + switch value.body { + case let .json(value): + try converter.validateAcceptIfPresent( + "application/json", + in: request.headerFields + ) + response.body = try converter.setResponseBodyAsJSON( + value, + headerFields: &response.headerFields, + contentType: "application/json; charset=utf-8" + ) + case let .text(value): + try converter.validateAcceptIfPresent( + "text/plain", + in: request.headerFields + ) + response.body = try converter.setResponseBodyAsText( + value, + headerFields: &response.headerFields, + contentType: "text/plain" + ) + case let .binary(value): + try converter.validateAcceptIfPresent( + "application/octet-stream", + in: request.headerFields + ) + response.body = try converter.setResponseBodyAsBinary( + value, + headerFields: &response.headerFields, + contentType: "application/octet-stream" + ) + } + return response + case let .undocumented(statusCode, _): return .init(statusCode: statusCode) + } + } + ) + } + /// - Remark: HTTP `POST /pets/stats`. + /// - Remark: Generated from `#/paths//pets/stats/post(postStats)`. + func postStats(request: Request, metadata: ServerRequestMetadata) async throws -> Response { + try await handle( + request: request, + with: metadata, + forOperation: Operations.postStats.id, + using: { APIHandler.postStats($0) }, + deserializer: { request, metadata in let path: Operations.postStats.Input.Path = .init() + let query: Operations.postStats.Input.Query = .init() + let headers: Operations.postStats.Input.Headers = .init() + let cookies: Operations.postStats.Input.Cookies = .init() + let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) + let body: Operations.postStats.Input.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getRequiredRequestBodyAsJSON( + Components.Schemas.PetStats.self, + from: request.body, + transforming: { value in .json(value) } + ) + } else if try converter.isMatchingContentType( + received: contentType, + expectedRaw: "text/plain" + ) { + body = try converter.getRequiredRequestBodyAsText( + Swift.String.self, + from: request.body, + transforming: { value in .text(value) } + ) + } else if try converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/octet-stream" + ) { + body = try converter.getRequiredRequestBodyAsBinary( + Foundation.Data.self, + from: request.body, + transforming: { value in .binary(value) } + ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } + return Operations.postStats.Input( + path: path, + query: query, + headers: headers, + cookies: cookies, + body: body + ) + }, + serializer: { output, request in + switch output { + case let .accepted(value): + suppressUnusedWarning(value) + var response = Response(statusCode: 202) + suppressMutabilityWarning(&response) + return response + case let .undocumented(statusCode, _): return .init(statusCode: statusCode) + } + } + ) + } + /// - Remark: HTTP `POST /probe/`. + /// - Remark: Generated from `#/paths//probe//post(probe)`. + @available(*, deprecated) func probe(request: Request, metadata: ServerRequestMetadata) + async throws -> Response + { + try await handle( + request: request, + with: metadata, + forOperation: Operations.probe.id, + using: { APIHandler.probe($0) }, + deserializer: { request, metadata in let path: Operations.probe.Input.Path = .init() + let query: Operations.probe.Input.Query = .init() + let headers: Operations.probe.Input.Headers = .init() + let cookies: Operations.probe.Input.Cookies = .init() + return Operations.probe.Input( + path: path, + query: query, + headers: headers, + cookies: cookies, + body: nil + ) + }, + serializer: { output, request in + switch output { + case let .noContent(value): + suppressUnusedWarning(value) + var response = Response(statusCode: 204) + suppressMutabilityWarning(&response) + return response + case let .undocumented(statusCode, _): return .init(statusCode: statusCode) + } + } + ) + } + /// Update just a specific property of an existing pet. Nothing is updated if no request body is provided. + /// + /// - Remark: HTTP `PATCH /pets/{petId}`. + /// - Remark: Generated from `#/paths//pets/{petId}/patch(updatePet)`. + func updatePet(request: Request, metadata: ServerRequestMetadata) async throws -> Response { + try await handle( + request: request, + with: metadata, + forOperation: Operations.updatePet.id, + using: { APIHandler.updatePet($0) }, + deserializer: { request, metadata in + let path: Operations.updatePet.Input.Path = .init( + petId: try converter.getPathParameterAsText( + in: metadata.pathParameters, + name: "petId", + as: Swift.Int64.self + ) + ) + let query: Operations.updatePet.Input.Query = .init() + let headers: Operations.updatePet.Input.Headers = .init() + let cookies: Operations.updatePet.Input.Cookies = .init() + let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) + let body: Components.RequestBodies.UpdatePetRequest? + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/json" + ) + { + body = try converter.getOptionalRequestBodyAsJSON( + Components.RequestBodies.UpdatePetRequest.jsonPayload.self, + from: request.body, + transforming: { value in .json(value) } + ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } + return Operations.updatePet.Input( + path: path, + query: query, + headers: headers, + cookies: cookies, + body: body + ) + }, + serializer: { output, request in + switch output { + case let .noContent(value): + suppressUnusedWarning(value) + var response = Response(statusCode: 204) + suppressMutabilityWarning(&response) + return response + case let .badRequest(value): + suppressUnusedWarning(value) + var response = Response(statusCode: 400) + suppressMutabilityWarning(&response) + switch value.body { + case let .json(value): + try converter.validateAcceptIfPresent( + "application/json", + in: request.headerFields + ) + response.body = try converter.setResponseBodyAsJSON( + value, + headerFields: &response.headerFields, + contentType: "application/json; charset=utf-8" + ) + } + return response + case let .undocumented(statusCode, _): return .init(statusCode: statusCode) + } + } + ) + } + /// Upload an avatar + /// + /// - Remark: HTTP `PUT /pets/{petId}/avatar`. + /// - Remark: Generated from `#/paths//pets/{petId}/avatar/put(uploadAvatarForPet)`. + func uploadAvatarForPet(request: Request, metadata: ServerRequestMetadata) async throws + -> Response + { + try await handle( + request: request, + with: metadata, + forOperation: Operations.uploadAvatarForPet.id, + using: { APIHandler.uploadAvatarForPet($0) }, + deserializer: { request, metadata in + let path: Operations.uploadAvatarForPet.Input.Path = .init( + petId: try converter.getPathParameterAsText( + in: metadata.pathParameters, + name: "petId", + as: Components.Parameters.path_petId.self + ) + ) + let query: Operations.uploadAvatarForPet.Input.Query = .init() + let headers: Operations.uploadAvatarForPet.Input.Headers = .init() + let cookies: Operations.uploadAvatarForPet.Input.Cookies = .init() + let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) + let body: Operations.uploadAvatarForPet.Input.Body + if try contentType == nil + || converter.isMatchingContentType( + received: contentType, + expectedRaw: "application/octet-stream" + ) + { + body = try converter.getRequiredRequestBodyAsBinary( + Foundation.Data.self, + from: request.body, + transforming: { value in .binary(value) } + ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } + return Operations.uploadAvatarForPet.Input( + path: path, + query: query, + headers: headers, + cookies: cookies, + body: body + ) + }, + serializer: { output, request in + switch output { + case let .ok(value): + suppressUnusedWarning(value) + var response = Response(statusCode: 200) + suppressMutabilityWarning(&response) + switch value.body { + case let .binary(value): + try converter.validateAcceptIfPresent( + "application/octet-stream", + in: request.headerFields + ) + response.body = try converter.setResponseBodyAsBinary( + value, + headerFields: &response.headerFields, + contentType: "application/octet-stream" + ) + } + return response + case let .preconditionFailed(value): + suppressUnusedWarning(value) + var response = Response(statusCode: 412) + suppressMutabilityWarning(&response) + switch value.body { + case let .json(value): + try converter.validateAcceptIfPresent( + "application/json", + in: request.headerFields + ) + response.body = try converter.setResponseBodyAsJSON( + value, + headerFields: &response.headerFields, + contentType: "application/json; charset=utf-8" + ) + } + return response + case let .internalServerError(value): + suppressUnusedWarning(value) + var response = Response(statusCode: 500) + suppressMutabilityWarning(&response) + switch value.body { + case let .text(value): + try converter.validateAcceptIfPresent( + "text/plain", + in: request.headerFields + ) + response.body = try converter.setResponseBodyAsText( + value, + headerFields: &response.headerFields, + contentType: "text/plain" + ) + } + return response + case let .undocumented(statusCode, _): return .init(statusCode: statusCode) + } + } + ) + } +} diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Types.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Types.swift new file mode 100644 index 00000000..b8e1de93 --- /dev/null +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Types.swift @@ -0,0 +1,1601 @@ +// Generated by swift-openapi-generator, do not modify. +@_spi(Generated) import OpenAPIRuntime +#if os(Linux) +@preconcurrency import Foundation +#else +import Foundation +#endif +/// A type that performs HTTP operations defined by the OpenAPI document. +public protocol APIProtocol: Sendable { + /// List all pets + /// + /// You can fetch + /// all the pets here + /// + /// - Remark: HTTP `GET /pets`. + /// - Remark: Generated from `#/paths//pets/get(listPets)`. + func listPets(_ input: Operations.listPets.Input) async throws -> Operations.listPets.Output + /// Create a pet + /// + /// - Remark: HTTP `POST /pets`. + /// - Remark: Generated from `#/paths//pets/post(createPet)`. + func createPet(_ input: Operations.createPet.Input) async throws -> Operations.createPet.Output + /// - Remark: HTTP `GET /pets/stats`. + /// - Remark: Generated from `#/paths//pets/stats/get(getStats)`. + func getStats(_ input: Operations.getStats.Input) async throws -> Operations.getStats.Output + /// - Remark: HTTP `POST /pets/stats`. + /// - Remark: Generated from `#/paths//pets/stats/post(postStats)`. + func postStats(_ input: Operations.postStats.Input) async throws -> Operations.postStats.Output + /// - Remark: HTTP `POST /probe/`. + /// - Remark: Generated from `#/paths//probe//post(probe)`. + @available(*, deprecated) func probe(_ input: Operations.probe.Input) async throws + -> Operations.probe.Output + /// Update just a specific property of an existing pet. Nothing is updated if no request body is provided. + /// + /// - Remark: HTTP `PATCH /pets/{petId}`. + /// - Remark: Generated from `#/paths//pets/{petId}/patch(updatePet)`. + func updatePet(_ input: Operations.updatePet.Input) async throws -> Operations.updatePet.Output + /// Upload an avatar + /// + /// - Remark: HTTP `PUT /pets/{petId}/avatar`. + /// - Remark: Generated from `#/paths//pets/{petId}/avatar/put(uploadAvatarForPet)`. + func uploadAvatarForPet(_ input: Operations.uploadAvatarForPet.Input) async throws + -> Operations.uploadAvatarForPet.Output +} +/// Server URLs defined in the OpenAPI document. +public enum Servers { + /// Example Petstore implementation service + public static func server1() throws -> URL { + try URL(validatingOpenAPIServerURL: "https://example.com/api") + } + public static func server2() throws -> URL { try URL(validatingOpenAPIServerURL: "/api") } +} +/// Types generated from the components section of the OpenAPI document. +public enum Components { + /// Types generated from the `#/components/schemas` section of the OpenAPI document. + public enum Schemas { + /// Pet metadata + /// + /// - Remark: Generated from `#/components/schemas/Pet`. + public struct Pet: Codable, Equatable, Hashable, Sendable { + /// Pet id + /// + /// - Remark: Generated from `#/components/schemas/Pet/id`. + public var id: Swift.Int64 + /// Pet name + /// + /// - Remark: Generated from `#/components/schemas/Pet/name`. + public var name: Swift.String + /// - Remark: Generated from `#/components/schemas/Pet/tag`. + public var tag: Swift.String? + /// - Remark: Generated from `#/components/schemas/Pet/kind`. + public var kind: Components.Schemas.PetKind? + /// Creates a new `Pet`. + /// + /// - Parameters: + /// - id: Pet id + /// - name: Pet name + /// - tag: + /// - kind: + public init( + id: Swift.Int64, + name: Swift.String, + tag: Swift.String? = nil, + kind: Components.Schemas.PetKind? = nil + ) { + self.id = id + self.name = name + self.tag = tag + self.kind = kind + } + public enum CodingKeys: String, CodingKey { + case id + case name + case tag + case kind + } + } + /// Kind of pet + /// + /// - Remark: Generated from `#/components/schemas/PetKind`. + @frozen + public enum PetKind: RawRepresentable, Codable, Equatable, Hashable, Sendable, + _AutoLosslessStringConvertible, CaseIterable + { + case cat + case dog + case ELEPHANT + case BIG_ELEPHANT_1 + case _nake + case _public + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "cat": self = .cat + case "dog": self = .dog + case "ELEPHANT": self = .ELEPHANT + case "BIG_ELEPHANT_1": self = .BIG_ELEPHANT_1 + case "$nake": self = ._nake + case "public": self = ._public + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .cat: return "cat" + case .dog: return "dog" + case .ELEPHANT: return "ELEPHANT" + case .BIG_ELEPHANT_1: return "BIG_ELEPHANT_1" + case ._nake: return "$nake" + case ._public: return "public" + } + } + public static var allCases: [PetKind] { + [.cat, .dog, .ELEPHANT, .BIG_ELEPHANT_1, ._nake, ._public] + } + } + /// - Remark: Generated from `#/components/schemas/CreatePetRequest`. + public struct CreatePetRequest: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/CreatePetRequest/name`. + public var name: Swift.String + /// - Remark: Generated from `#/components/schemas/CreatePetRequest/kind`. + public var kind: Components.Schemas.PetKind? + /// - Remark: Generated from `#/components/schemas/CreatePetRequest/tag`. + public var tag: Swift.String? + /// Creates a new `CreatePetRequest`. + /// + /// - Parameters: + /// - name: + /// - kind: + /// - tag: + public init( + name: Swift.String, + kind: Components.Schemas.PetKind? = nil, + tag: Swift.String? = nil + ) { + self.name = name + self.kind = kind + self.tag = tag + } + public enum CodingKeys: String, CodingKey { + case name + case kind + case tag + } + } + /// - Remark: Generated from `#/components/schemas/Pets`. + public typealias Pets = [Components.Schemas.Pet] + /// - Remark: Generated from `#/components/schemas/Error`. + public struct _Error: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/Error/code`. + public var code: Swift.Int32 + /// - Remark: Generated from `#/components/schemas/Error/me$sage`. + public var me_sage: Swift.String + /// Extra information about the error. + /// + /// - Remark: Generated from `#/components/schemas/Error/extraInfo`. + public struct extraInfoPayload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/Error/extraInfo/value1`. + public var value1: Components.Schemas.ExtraInfo + /// Creates a new `extraInfoPayload`. + /// + /// - Parameters: + /// - value1: + public init(value1: Components.Schemas.ExtraInfo) { self.value1 = value1 } + public init(from decoder: any Decoder) throws { value1 = try .init(from: decoder) } + public func encode(to encoder: any Encoder) throws { + try value1.encode(to: encoder) + } + } + /// Extra information about the error. + /// + /// - Remark: Generated from `#/components/schemas/Error/extraInfo`. + public var extraInfo: Components.Schemas._Error.extraInfoPayload? + /// Custom user-provided key-value pairs. + /// + /// - Remark: Generated from `#/components/schemas/Error/userData`. + public var userData: OpenAPIRuntime.OpenAPIObjectContainer? + /// Creates a new `_Error`. + /// + /// - Parameters: + /// - code: + /// - me_sage: + /// - extraInfo: Extra information about the error. + /// - userData: Custom user-provided key-value pairs. + public init( + code: Swift.Int32, + me_sage: Swift.String, + extraInfo: Components.Schemas._Error.extraInfoPayload? = nil, + userData: OpenAPIRuntime.OpenAPIObjectContainer? = nil + ) { + self.code = code + self.me_sage = me_sage + self.extraInfo = extraInfo + self.userData = userData + } + public enum CodingKeys: String, CodingKey { + case code + case me_sage = "me$sage" + case extraInfo + case userData + } + } + /// - Remark: Generated from `#/components/schemas/PetFeeding`. + public struct PetFeeding: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/PetFeeding/schedule`. + @frozen + public enum schedulePayload: RawRepresentable, Codable, Equatable, Hashable, Sendable, + _AutoLosslessStringConvertible, CaseIterable + { + case hourly + case daily + case weekly + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "hourly": self = .hourly + case "daily": self = .daily + case "weekly": self = .weekly + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .hourly: return "hourly" + case .daily: return "daily" + case .weekly: return "weekly" + } + } + public static var allCases: [schedulePayload] { [.hourly, .daily, .weekly] } + } + /// - Remark: Generated from `#/components/schemas/PetFeeding/schedule`. + public var schedule: Components.Schemas.PetFeeding.schedulePayload? + /// Creates a new `PetFeeding`. + /// + /// - Parameters: + /// - schedule: + public init(schedule: Components.Schemas.PetFeeding.schedulePayload? = nil) { + self.schedule = schedule + } + public enum CodingKeys: String, CodingKey { case schedule } + } + /// - Remark: Generated from `#/components/schemas/DOB`. + public typealias DOB = Foundation.Date + /// - Remark: Generated from `#/components/schemas/ExtraInfo`. + public typealias ExtraInfo = Swift.String + /// - Remark: Generated from `#/components/schemas/NoAdditionalProperties`. + public struct NoAdditionalProperties: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/NoAdditionalProperties/foo`. + public var foo: Swift.String? + /// Creates a new `NoAdditionalProperties`. + /// + /// - Parameters: + /// - foo: + public init(foo: Swift.String? = nil) { self.foo = foo } + public enum CodingKeys: String, CodingKey { case foo } + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + foo = try container.decodeIfPresent(Swift.String.self, forKey: .foo) + try decoder.ensureNoAdditionalProperties(knownKeys: ["foo"]) + } + } + /// - Remark: Generated from `#/components/schemas/AnyAdditionalProperties`. + public struct AnyAdditionalProperties: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/AnyAdditionalProperties/foo`. + public var foo: Swift.String? + /// A container of undocumented properties. + public var additionalProperties: OpenAPIRuntime.OpenAPIObjectContainer + /// Creates a new `AnyAdditionalProperties`. + /// + /// - Parameters: + /// - foo: + /// - additionalProperties: A container of undocumented properties. + public init( + foo: Swift.String? = nil, + additionalProperties: OpenAPIRuntime.OpenAPIObjectContainer = .init() + ) { + self.foo = foo + self.additionalProperties = additionalProperties + } + public enum CodingKeys: String, CodingKey { case foo } + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + foo = try container.decodeIfPresent(Swift.String.self, forKey: .foo) + additionalProperties = try decoder.decodeAdditionalProperties(knownKeys: ["foo"]) + } + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(foo, forKey: .foo) + try encoder.encodeAdditionalProperties(additionalProperties) + } + } + /// - Remark: Generated from `#/components/schemas/TypedAdditionalProperties`. + public struct TypedAdditionalProperties: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/TypedAdditionalProperties/foo`. + public var foo: Swift.String? + /// A container of undocumented properties. + public var additionalProperties: [String: Swift.Int] + /// Creates a new `TypedAdditionalProperties`. + /// + /// - Parameters: + /// - foo: + /// - additionalProperties: A container of undocumented properties. + public init( + foo: Swift.String? = nil, + additionalProperties: [String: Swift.Int] = .init() + ) { + self.foo = foo + self.additionalProperties = additionalProperties + } + public enum CodingKeys: String, CodingKey { case foo } + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + foo = try container.decodeIfPresent(Swift.String.self, forKey: .foo) + additionalProperties = try decoder.decodeAdditionalProperties(knownKeys: ["foo"]) + } + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(foo, forKey: .foo) + try encoder.encodeAdditionalProperties(additionalProperties) + } + } + /// - Remark: Generated from `#/components/schemas/CodeError`. + public struct CodeError: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/CodeError/code`. + public var code: Swift.Int + /// Creates a new `CodeError`. + /// + /// - Parameters: + /// - code: + public init(code: Swift.Int) { self.code = code } + public enum CodingKeys: String, CodingKey { case code } + } + /// - Remark: Generated from `#/components/schemas/AllOfObjects`. + public struct AllOfObjects: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/AllOfObjects/value1`. + public struct Value1Payload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/AllOfObjects/value1/message`. + public var message: Swift.String + /// Creates a new `Value1Payload`. + /// + /// - Parameters: + /// - message: + public init(message: Swift.String) { self.message = message } + public enum CodingKeys: String, CodingKey { case message } + } + /// - Remark: Generated from `#/components/schemas/AllOfObjects/value1`. + public var value1: Components.Schemas.AllOfObjects.Value1Payload + /// - Remark: Generated from `#/components/schemas/AllOfObjects/value2`. + public var value2: Components.Schemas.CodeError + /// Creates a new `AllOfObjects`. + /// + /// - Parameters: + /// - value1: + /// - value2: + public init( + value1: Components.Schemas.AllOfObjects.Value1Payload, + value2: Components.Schemas.CodeError + ) { + self.value1 = value1 + self.value2 = value2 + } + public init(from decoder: any Decoder) throws { + value1 = try .init(from: decoder) + value2 = try .init(from: decoder) + } + public func encode(to encoder: any Encoder) throws { + try value1.encode(to: encoder) + try value2.encode(to: encoder) + } + } + /// - Remark: Generated from `#/components/schemas/AnyOfObjects`. + public struct AnyOfObjects: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/AnyOfObjects/value1`. + public struct Value1Payload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/AnyOfObjects/value1/message`. + public var message: Swift.String + /// Creates a new `Value1Payload`. + /// + /// - Parameters: + /// - message: + public init(message: Swift.String) { self.message = message } + public enum CodingKeys: String, CodingKey { case message } + } + /// - Remark: Generated from `#/components/schemas/AnyOfObjects/value1`. + public var value1: Components.Schemas.AnyOfObjects.Value1Payload? + /// - Remark: Generated from `#/components/schemas/AnyOfObjects/value2`. + public var value2: Components.Schemas.CodeError? + /// Creates a new `AnyOfObjects`. + /// + /// - Parameters: + /// - value1: + /// - value2: + public init( + value1: Components.Schemas.AnyOfObjects.Value1Payload? = nil, + value2: Components.Schemas.CodeError? = nil + ) { + self.value1 = value1 + self.value2 = value2 + } + public init(from decoder: any Decoder) throws { + value1 = try? .init(from: decoder) + value2 = try? .init(from: decoder) + try DecodingError.verifyAtLeastOneSchemaIsNotNil( + [value1, value2], + type: Self.self, + codingPath: decoder.codingPath + ) + } + public func encode(to encoder: any Encoder) throws { + try value1?.encode(to: encoder) + try value2?.encode(to: encoder) + } + } + /// - Remark: Generated from `#/components/schemas/OneOfAny`. + @frozen public enum OneOfAny: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/OneOfAny/case1`. + case case1(Swift.String) + /// - Remark: Generated from `#/components/schemas/OneOfAny/case2`. + case case2(Swift.Int) + /// - Remark: Generated from `#/components/schemas/OneOfAny/case3`. + case CodeError(Components.Schemas.CodeError) + /// - Remark: Generated from `#/components/schemas/OneOfAny/case4`. + public struct Case4Payload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/OneOfAny/case4/message`. + public var message: Swift.String + /// Creates a new `Case4Payload`. + /// + /// - Parameters: + /// - message: + public init(message: Swift.String) { self.message = message } + public enum CodingKeys: String, CodingKey { case message } + } + /// - Remark: Generated from `#/components/schemas/OneOfAny/case4`. + case case4(Components.Schemas.OneOfAny.Case4Payload) + /// Parsed a case that was not defined in the OpenAPI document. + case undocumented(OpenAPIRuntime.OpenAPIValueContainer) + public init(from decoder: any Decoder) throws { + do { + self = .case1(try .init(from: decoder)) + return + } catch {} + do { + self = .case2(try .init(from: decoder)) + return + } catch {} + do { + self = .CodeError(try .init(from: decoder)) + return + } catch {} + do { + self = .case4(try .init(from: decoder)) + return + } catch {} + let container = try decoder.singleValueContainer() + let value = try container.decode(OpenAPIRuntime.OpenAPIValueContainer.self) + self = .undocumented(value) + } + public func encode(to encoder: any Encoder) throws { + switch self { + case let .case1(value): try value.encode(to: encoder) + case let .case2(value): try value.encode(to: encoder) + case let .CodeError(value): try value.encode(to: encoder) + case let .case4(value): try value.encode(to: encoder) + case let .undocumented(value): try value.encode(to: encoder) + } + } + } + /// - Remark: Generated from `#/components/schemas/PetExercise`. + public struct PetExercise: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/PetExercise/kind`. + public var kind: Swift.String + /// Creates a new `PetExercise`. + /// + /// - Parameters: + /// - kind: + public init(kind: Swift.String) { self.kind = kind } + public enum CodingKeys: String, CodingKey { case kind } + } + /// - Remark: Generated from `#/components/schemas/Walk`. + public struct Walk: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/Walk/kind`. + public var kind: Swift.String + /// - Remark: Generated from `#/components/schemas/Walk/length`. + public var length: Swift.Int + /// Creates a new `Walk`. + /// + /// - Parameters: + /// - kind: + /// - length: + public init(kind: Swift.String, length: Swift.Int) { + self.kind = kind + self.length = length + } + public enum CodingKeys: String, CodingKey { + case kind + case length + } + } + /// - Remark: Generated from `#/components/schemas/MessagedExercise`. + public struct MessagedExercise: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/MessagedExercise/value1`. + public var value1: Components.Schemas.PetExercise + /// - Remark: Generated from `#/components/schemas/MessagedExercise/value2`. + public struct Value2Payload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/MessagedExercise/value2/message`. + public var message: Swift.String + /// Creates a new `Value2Payload`. + /// + /// - Parameters: + /// - message: + public init(message: Swift.String) { self.message = message } + public enum CodingKeys: String, CodingKey { case message } + } + /// - Remark: Generated from `#/components/schemas/MessagedExercise/value2`. + public var value2: Components.Schemas.MessagedExercise.Value2Payload + /// Creates a new `MessagedExercise`. + /// + /// - Parameters: + /// - value1: + /// - value2: + public init( + value1: Components.Schemas.PetExercise, + value2: Components.Schemas.MessagedExercise.Value2Payload + ) { + self.value1 = value1 + self.value2 = value2 + } + public init(from decoder: any Decoder) throws { + value1 = try .init(from: decoder) + value2 = try .init(from: decoder) + } + public func encode(to encoder: any Encoder) throws { + try value1.encode(to: encoder) + try value2.encode(to: encoder) + } + } + /// - Remark: Generated from `#/components/schemas/OneOfObjectsWithDiscriminator`. + @frozen public enum OneOfObjectsWithDiscriminator: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/OneOfObjectsWithDiscriminator/case1`. + case Walk(Components.Schemas.Walk) + /// - Remark: Generated from `#/components/schemas/OneOfObjectsWithDiscriminator/case2`. + case MessagedExercise(Components.Schemas.MessagedExercise) + /// Parsed a case that was not defined in the OpenAPI document. + case undocumented(OpenAPIRuntime.OpenAPIObjectContainer) + public enum CodingKeys: String, CodingKey { case kind } + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let discriminator = try container.decode(String.self, forKey: .kind) + switch discriminator { + case "Walk": self = .Walk(try .init(from: decoder)) + case "MessagedExercise": self = .MessagedExercise(try .init(from: decoder)) + default: + let container = try decoder.singleValueContainer() + let value = try container.decode(OpenAPIRuntime.OpenAPIObjectContainer.self) + self = .undocumented(value) + } + } + public func encode(to encoder: any Encoder) throws { + switch self { + case let .Walk(value): try value.encode(to: encoder) + case let .MessagedExercise(value): try value.encode(to: encoder) + case let .undocumented(value): try value.encode(to: encoder) + } + } + } + /// - Remark: Generated from `#/components/schemas/DeprecatedObject`. + @available(*, deprecated) + public struct DeprecatedObject: Codable, Equatable, Hashable, Sendable { + /// Creates a new `DeprecatedObject`. + public init() {} + public init(from decoder: any Decoder) throws { + try decoder.ensureNoAdditionalProperties(knownKeys: []) + } + } + /// - Remark: Generated from `#/components/schemas/ObjectWithDeprecatedProperty`. + public struct ObjectWithDeprecatedProperty: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/ObjectWithDeprecatedProperty/message`. + @available(*, deprecated) public var message: Swift.String? + /// Creates a new `ObjectWithDeprecatedProperty`. + /// + /// - Parameters: + /// - message: + public init(message: Swift.String? = nil) { self.message = message } + public enum CodingKeys: String, CodingKey { case message } + } + /// - Remark: Generated from `#/components/schemas/PetStats`. + public struct PetStats: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/PetStats/count`. + public var count: Swift.Int + /// Creates a new `PetStats`. + /// + /// - Parameters: + /// - count: + public init(count: Swift.Int) { self.count = count } + public enum CodingKeys: String, CodingKey { case count } + } + } + /// Types generated from the `#/components/parameters` section of the OpenAPI document. + public enum Parameters { + /// Supply this parameter to filter pets born since the provided date. + /// + /// - Remark: Generated from `#/components/parameters/query.born-since`. + public typealias query_born_since = Components.Schemas.DOB + /// The id of the pet to retrieve + /// + /// - Remark: Generated from `#/components/parameters/path.petId`. + public typealias path_petId = Swift.Int64 + /// A deprecated header parameter + /// + /// - Remark: Generated from `#/components/parameters/header.deprecatedHeader`. + public typealias header_deprecatedHeader = Swift.String + } + /// Types generated from the `#/components/requestBodies` section of the OpenAPI document. + public enum RequestBodies { + @frozen public enum UpdatePetRequest: Sendable, Equatable, Hashable { + /// - Remark: Generated from `#/components/requestBodies/UpdatePetRequest/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/requestBodies/UpdatePetRequest/json/name`. + public var name: Swift.String? + /// - Remark: Generated from `#/components/requestBodies/UpdatePetRequest/json/kind`. + public var kind: Components.Schemas.PetKind? + /// - Remark: Generated from `#/components/requestBodies/UpdatePetRequest/json/tag`. + public var tag: Swift.String? + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - name: + /// - kind: + /// - tag: + public init( + name: Swift.String? = nil, + kind: Components.Schemas.PetKind? = nil, + tag: Swift.String? = nil + ) { + self.name = name + self.kind = kind + self.tag = tag + } + public enum CodingKeys: String, CodingKey { + case name + case kind + case tag + } + } + case json(Components.RequestBodies.UpdatePetRequest.jsonPayload) + } + } + /// Types generated from the `#/components/responses` section of the OpenAPI document. + public enum Responses { + public struct ErrorBadRequest: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + public var X_Reason: Swift.String? + /// Creates a new `Headers`. + /// + /// - Parameters: + /// - X_Reason: + public init(X_Reason: Swift.String? = nil) { self.X_Reason = X_Reason } + } + /// Received HTTP response headers + public var headers: Components.Responses.ErrorBadRequest.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + /// - Remark: Generated from `#/components/responses/ErrorBadRequest/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/components/responses/ErrorBadRequest/json/code`. + public var code: Swift.Int + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - code: + public init(code: Swift.Int) { self.code = code } + public enum CodingKeys: String, CodingKey { case code } + } + case json(Components.Responses.ErrorBadRequest.Body.jsonPayload) + } + /// Received HTTP response body + public var body: Components.Responses.ErrorBadRequest.Body + /// Creates a new `ErrorBadRequest`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Components.Responses.ErrorBadRequest.Headers = .init(), + body: Components.Responses.ErrorBadRequest.Body + ) { + self.headers = headers + self.body = body + } + } + } + /// Types generated from the `#/components/headers` section of the OpenAPI document. + public enum Headers { + /// - Remark: Generated from `#/components/headers/TracingHeader`. + public typealias TracingHeader = Swift.String + } +} +/// API operations, with input and output types, generated from `#/paths` in the OpenAPI document. +public enum Operations { + /// List all pets + /// + /// You can fetch + /// all the pets here + /// + /// - Remark: HTTP `GET /pets`. + /// - Remark: Generated from `#/paths//pets/get(listPets)`. + public enum listPets { + public static let id: String = "listPets" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + /// Creates a new `Path`. + public init() {} + } + public var path: Operations.listPets.Input.Path + public struct Query: Sendable, Equatable, Hashable { + public var limit: Swift.Int32? + /// - Remark: Generated from `#/paths/pets/GET/query/habitat`. + @frozen + public enum habitatPayload: RawRepresentable, Codable, Equatable, Hashable, + Sendable, _AutoLosslessStringConvertible, CaseIterable + { + case water + case land + case air + case _empty + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "water": self = .water + case "land": self = .land + case "air": self = .air + case "": self = ._empty + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .water: return "water" + case .land: return "land" + case .air: return "air" + case ._empty: return "" + } + } + public static var allCases: [habitatPayload] { [.water, .land, .air, ._empty] } + } + public var habitat: Operations.listPets.Input.Query.habitatPayload? + /// - Remark: Generated from `#/paths/pets/GET/query/feedsPayload`. + @frozen + public enum feedsPayloadPayload: RawRepresentable, Codable, Equatable, Hashable, + Sendable, _AutoLosslessStringConvertible, CaseIterable + { + case omnivore + case carnivore + case herbivore + /// Parsed a raw value that was not defined in the OpenAPI document. + case undocumented(String) + public init?(rawValue: String) { + switch rawValue { + case "omnivore": self = .omnivore + case "carnivore": self = .carnivore + case "herbivore": self = .herbivore + default: self = .undocumented(rawValue) + } + } + public var rawValue: String { + switch self { + case let .undocumented(string): return string + case .omnivore: return "omnivore" + case .carnivore: return "carnivore" + case .herbivore: return "herbivore" + } + } + public static var allCases: [feedsPayloadPayload] { + [.omnivore, .carnivore, .herbivore] + } + } + /// - Remark: Generated from `#/paths/pets/GET/query/feeds`. + public typealias feedsPayload = [Operations.listPets.Input.Query + .feedsPayloadPayload] + public var feeds: Operations.listPets.Input.Query.feedsPayload? + public var since: Components.Parameters.query_born_since? + /// Creates a new `Query`. + /// + /// - Parameters: + /// - limit: + /// - habitat: + /// - feeds: + /// - since: + public init( + limit: Swift.Int32? = nil, + habitat: Operations.listPets.Input.Query.habitatPayload? = nil, + feeds: Operations.listPets.Input.Query.feedsPayload? = nil, + since: Components.Parameters.query_born_since? = nil + ) { + self.limit = limit + self.habitat = habitat + self.feeds = feeds + self.since = since + } + } + public var query: Operations.listPets.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + public var My_Request_UUID: Swift.String? + /// Creates a new `Headers`. + /// + /// - Parameters: + /// - My_Request_UUID: + public init(My_Request_UUID: Swift.String? = nil) { + self.My_Request_UUID = My_Request_UUID + } + } + public var headers: Operations.listPets.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.listPets.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.listPets.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.listPets.Input.Path = .init(), + query: Operations.listPets.Input.Query = .init(), + headers: Operations.listPets.Input.Headers = .init(), + cookies: Operations.listPets.Input.Cookies = .init(), + body: Operations.listPets.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + public var My_Response_UUID: Swift.String + public var My_Tracing_Header: Components.Headers.TracingHeader? + /// Creates a new `Headers`. + /// + /// - Parameters: + /// - My_Response_UUID: + /// - My_Tracing_Header: + public init( + My_Response_UUID: Swift.String, + My_Tracing_Header: Components.Headers.TracingHeader? = nil + ) { + self.My_Response_UUID = My_Response_UUID + self.My_Tracing_Header = My_Tracing_Header + } + } + /// Received HTTP response headers + public var headers: Operations.listPets.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.Pets) + } + /// Received HTTP response body + public var body: Operations.listPets.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.listPets.Output.Ok.Headers, + body: Operations.listPets.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// A paged array of pets + /// + /// - Remark: Generated from `#/paths//pets/get(listPets)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.listPets.Output.Ok) + public struct Default: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.listPets.Output.Default.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas._Error) + } + /// Received HTTP response body + public var body: Operations.listPets.Output.Default.Body + /// Creates a new `Default`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.listPets.Output.Default.Headers = .init(), + body: Operations.listPets.Output.Default.Body + ) { + self.headers = headers + self.body = body + } + } + /// Unexpected error + /// + /// - Remark: Generated from `#/paths//pets/get(listPets)/responses/default`. + /// + /// HTTP response code: `default`. + case `default`(statusCode: Int, Operations.listPets.Output.Default) + } + } + /// Create a pet + /// + /// - Remark: HTTP `POST /pets`. + /// - Remark: Generated from `#/paths//pets/post(createPet)`. + public enum createPet { + public static let id: String = "createPet" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + /// Creates a new `Path`. + public init() {} + } + public var path: Operations.createPet.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.createPet.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + public var X_Extra_Arguments: Components.Schemas.CodeError? + /// Creates a new `Headers`. + /// + /// - Parameters: + /// - X_Extra_Arguments: + public init(X_Extra_Arguments: Components.Schemas.CodeError? = nil) { + self.X_Extra_Arguments = X_Extra_Arguments + } + } + public var headers: Operations.createPet.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.createPet.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.CreatePetRequest) + } + public var body: Operations.createPet.Input.Body + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.createPet.Input.Path = .init(), + query: Operations.createPet.Input.Query = .init(), + headers: Operations.createPet.Input.Headers = .init(), + cookies: Operations.createPet.Input.Cookies = .init(), + body: Operations.createPet.Input.Body + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Created: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + public var X_Extra_Arguments: Components.Schemas.CodeError? + /// Creates a new `Headers`. + /// + /// - Parameters: + /// - X_Extra_Arguments: + public init(X_Extra_Arguments: Components.Schemas.CodeError? = nil) { + self.X_Extra_Arguments = X_Extra_Arguments + } + } + /// Received HTTP response headers + public var headers: Operations.createPet.Output.Created.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.Pet) + } + /// Received HTTP response body + public var body: Operations.createPet.Output.Created.Body + /// Creates a new `Created`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.createPet.Output.Created.Headers = .init(), + body: Operations.createPet.Output.Created.Body + ) { + self.headers = headers + self.body = body + } + } + /// Successfully created pet + /// + /// - Remark: Generated from `#/paths//pets/post(createPet)/responses/201`. + /// + /// HTTP response code: `201 created`. + case created(Operations.createPet.Output.Created) + /// Bad request + /// + /// - Remark: Generated from `#/paths//pets/post(createPet)/responses/400`. + /// + /// HTTP response code: `400 badRequest`. + case badRequest(Components.Responses.ErrorBadRequest) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// - Remark: HTTP `GET /pets/stats`. + /// - Remark: Generated from `#/paths//pets/stats/get(getStats)`. + public enum getStats { + public static let id: String = "getStats" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + /// Creates a new `Path`. + public init() {} + } + public var path: Operations.getStats.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.getStats.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.getStats.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.getStats.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.getStats.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.getStats.Input.Path = .init(), + query: Operations.getStats.Input.Query = .init(), + headers: Operations.getStats.Input.Headers = .init(), + cookies: Operations.getStats.Input.Cookies = .init(), + body: Operations.getStats.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.getStats.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.PetStats) + case text(Swift.String) + case binary(Foundation.Data) + } + /// Received HTTP response body + public var body: Operations.getStats.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.getStats.Output.Ok.Headers = .init(), + body: Operations.getStats.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// A successful response. + /// + /// - Remark: Generated from `#/paths//pets/stats/get(getStats)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.getStats.Output.Ok) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// - Remark: HTTP `POST /pets/stats`. + /// - Remark: Generated from `#/paths//pets/stats/post(postStats)`. + public enum postStats { + public static let id: String = "postStats" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + /// Creates a new `Path`. + public init() {} + } + public var path: Operations.postStats.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.postStats.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.postStats.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.postStats.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Components.Schemas.PetStats) + case text(Swift.String) + case binary(Foundation.Data) + } + public var body: Operations.postStats.Input.Body + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.postStats.Input.Path = .init(), + query: Operations.postStats.Input.Query = .init(), + headers: Operations.postStats.Input.Headers = .init(), + cookies: Operations.postStats.Input.Cookies = .init(), + body: Operations.postStats.Input.Body + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Accepted: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.postStats.Output.Accepted.Headers + @frozen public enum Body: Sendable, Equatable, Hashable {} + /// Received HTTP response body + public var body: Operations.postStats.Output.Accepted.Body? + /// Creates a new `Accepted`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.postStats.Output.Accepted.Headers = .init(), + body: Operations.postStats.Output.Accepted.Body? = nil + ) { + self.headers = headers + self.body = body + } + } + /// Accepted data. + /// + /// - Remark: Generated from `#/paths//pets/stats/post(postStats)/responses/202`. + /// + /// HTTP response code: `202 accepted`. + case accepted(Operations.postStats.Output.Accepted) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// - Remark: HTTP `POST /probe/`. + /// - Remark: Generated from `#/paths//probe//post(probe)`. + public enum probe { + public static let id: String = "probe" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + /// Creates a new `Path`. + public init() {} + } + public var path: Operations.probe.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.probe.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.probe.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.probe.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable {} + public var body: Operations.probe.Input.Body? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.probe.Input.Path = .init(), + query: Operations.probe.Input.Query = .init(), + headers: Operations.probe.Input.Headers = .init(), + cookies: Operations.probe.Input.Cookies = .init(), + body: Operations.probe.Input.Body? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct NoContent: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.probe.Output.NoContent.Headers + @frozen public enum Body: Sendable, Equatable, Hashable {} + /// Received HTTP response body + public var body: Operations.probe.Output.NoContent.Body? + /// Creates a new `NoContent`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.probe.Output.NoContent.Headers = .init(), + body: Operations.probe.Output.NoContent.Body? = nil + ) { + self.headers = headers + self.body = body + } + } + /// Ack + /// + /// - Remark: Generated from `#/paths//probe//post(probe)/responses/204`. + /// + /// HTTP response code: `204 noContent`. + case noContent(Operations.probe.Output.NoContent) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Update just a specific property of an existing pet. Nothing is updated if no request body is provided. + /// + /// - Remark: HTTP `PATCH /pets/{petId}`. + /// - Remark: Generated from `#/paths//pets/{petId}/patch(updatePet)`. + public enum updatePet { + public static let id: String = "updatePet" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var petId: Swift.Int64 + /// Creates a new `Path`. + /// + /// - Parameters: + /// - petId: + public init(petId: Swift.Int64) { self.petId = petId } + } + public var path: Operations.updatePet.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.updatePet.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.updatePet.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.updatePet.Input.Cookies + public var body: Components.RequestBodies.UpdatePetRequest? + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.updatePet.Input.Path, + query: Operations.updatePet.Input.Query = .init(), + headers: Operations.updatePet.Input.Headers = .init(), + cookies: Operations.updatePet.Input.Cookies = .init(), + body: Components.RequestBodies.UpdatePetRequest? = nil + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct NoContent: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updatePet.Output.NoContent.Headers + @frozen public enum Body: Sendable, Equatable, Hashable {} + /// Received HTTP response body + public var body: Operations.updatePet.Output.NoContent.Body? + /// Creates a new `NoContent`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updatePet.Output.NoContent.Headers = .init(), + body: Operations.updatePet.Output.NoContent.Body? = nil + ) { + self.headers = headers + self.body = body + } + } + /// Successfully updated + /// + /// - Remark: Generated from `#/paths//pets/{petId}/patch(updatePet)/responses/204`. + /// + /// HTTP response code: `204 noContent`. + case noContent(Operations.updatePet.Output.NoContent) + public struct BadRequest: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.updatePet.Output.BadRequest.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + /// - Remark: Generated from `#/paths/pets/{petId}/PATCH/json`. + public struct jsonPayload: Codable, Equatable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/pets/{petId}/PATCH/json/message`. + public var message: Swift.String + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - message: + public init(message: Swift.String) { self.message = message } + public enum CodingKeys: String, CodingKey { case message } + } + case json(Operations.updatePet.Output.BadRequest.Body.jsonPayload) + } + /// Received HTTP response body + public var body: Operations.updatePet.Output.BadRequest.Body + /// Creates a new `BadRequest`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.updatePet.Output.BadRequest.Headers = .init(), + body: Operations.updatePet.Output.BadRequest.Body + ) { + self.headers = headers + self.body = body + } + } + /// Update input error + /// + /// - Remark: Generated from `#/paths//pets/{petId}/patch(updatePet)/responses/400`. + /// + /// HTTP response code: `400 badRequest`. + case badRequest(Operations.updatePet.Output.BadRequest) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } + /// Upload an avatar + /// + /// - Remark: HTTP `PUT /pets/{petId}/avatar`. + /// - Remark: Generated from `#/paths//pets/{petId}/avatar/put(uploadAvatarForPet)`. + public enum uploadAvatarForPet { + public static let id: String = "uploadAvatarForPet" + public struct Input: Sendable, Equatable, Hashable { + public struct Path: Sendable, Equatable, Hashable { + public var petId: Components.Parameters.path_petId + /// Creates a new `Path`. + /// + /// - Parameters: + /// - petId: + public init(petId: Components.Parameters.path_petId) { self.petId = petId } + } + public var path: Operations.uploadAvatarForPet.Input.Path + public struct Query: Sendable, Equatable, Hashable { + /// Creates a new `Query`. + public init() {} + } + public var query: Operations.uploadAvatarForPet.Input.Query + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + public var headers: Operations.uploadAvatarForPet.Input.Headers + public struct Cookies: Sendable, Equatable, Hashable { + /// Creates a new `Cookies`. + public init() {} + } + public var cookies: Operations.uploadAvatarForPet.Input.Cookies + @frozen public enum Body: Sendable, Equatable, Hashable { case binary(Foundation.Data) } + public var body: Operations.uploadAvatarForPet.Input.Body + /// Creates a new `Input`. + /// + /// - Parameters: + /// - path: + /// - query: + /// - headers: + /// - cookies: + /// - body: + public init( + path: Operations.uploadAvatarForPet.Input.Path, + query: Operations.uploadAvatarForPet.Input.Query = .init(), + headers: Operations.uploadAvatarForPet.Input.Headers = .init(), + cookies: Operations.uploadAvatarForPet.Input.Cookies = .init(), + body: Operations.uploadAvatarForPet.Input.Body + ) { + self.path = path + self.query = query + self.headers = headers + self.cookies = cookies + self.body = body + } + } + @frozen public enum Output: Sendable, Equatable, Hashable { + public struct Ok: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.uploadAvatarForPet.Output.Ok.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case binary(Foundation.Data) + } + /// Received HTTP response body + public var body: Operations.uploadAvatarForPet.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.uploadAvatarForPet.Output.Ok.Headers = .init(), + body: Operations.uploadAvatarForPet.Output.Ok.Body + ) { + self.headers = headers + self.body = body + } + } + /// Echoes avatar back + /// + /// - Remark: Generated from `#/paths//pets/{petId}/avatar/put(uploadAvatarForPet)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.uploadAvatarForPet.Output.Ok) + public struct PreconditionFailed: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.uploadAvatarForPet.Output.PreconditionFailed.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { case json(Swift.String) } + /// Received HTTP response body + public var body: Operations.uploadAvatarForPet.Output.PreconditionFailed.Body + /// Creates a new `PreconditionFailed`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.uploadAvatarForPet.Output.PreconditionFailed.Headers = + .init(), + body: Operations.uploadAvatarForPet.Output.PreconditionFailed.Body + ) { + self.headers = headers + self.body = body + } + } + /// Avatar is not acceptable + /// + /// - Remark: Generated from `#/paths//pets/{petId}/avatar/put(uploadAvatarForPet)/responses/412`. + /// + /// HTTP response code: `412 preconditionFailed`. + case preconditionFailed(Operations.uploadAvatarForPet.Output.PreconditionFailed) + public struct InternalServerError: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { + /// Creates a new `Headers`. + public init() {} + } + /// Received HTTP response headers + public var headers: Operations.uploadAvatarForPet.Output.InternalServerError.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { case text(Swift.String) } + /// Received HTTP response body + public var body: Operations.uploadAvatarForPet.Output.InternalServerError.Body + /// Creates a new `InternalServerError`. + /// + /// - Parameters: + /// - headers: Received HTTP response headers + /// - body: Received HTTP response body + public init( + headers: Operations.uploadAvatarForPet.Output.InternalServerError.Headers = + .init(), + body: Operations.uploadAvatarForPet.Output.InternalServerError.Body + ) { + self.headers = headers + self.body = body + } + } + /// Server error + /// + /// - Remark: Generated from `#/paths//pets/{petId}/avatar/put(uploadAvatarForPet)/responses/500`. + /// + /// HTTP response code: `500 internalServerError`. + case internalServerError(Operations.uploadAvatarForPet.Output.InternalServerError) + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) + } + } +} diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index 2bebc6fd..205307f9 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -616,7 +616,79 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } - func testComponentsResponsesResponseWithOptionalHeader() throws { + func testComponentsResponsesResponseMultipleContentTypes() throws { + try self.assertResponsesTranslation( + featureFlags: [], + ignoredDiagnosticMessages: [#"Feature "Multiple content types" is not supported, skipping"#], + """ + responses: + MultipleContentTypes: + description: Multiple content types + content: + application/json: + schema: + type: integer + text/plain: {} + application/octet-stream: {} + """, + """ + public enum Responses { + public struct MultipleContentTypes: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { public init() {} } + public var headers: Components.Responses.MultipleContentTypes.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Swift.Int) + } + public var body: Components.Responses.MultipleContentTypes.Body + public init( + headers: Components.Responses.MultipleContentTypes.Headers = .init(), + body: Components.Responses.MultipleContentTypes.Body + ) { + self.headers = headers + self.body = body + } + } + } + """ + ) + try self.assertResponsesTranslation( + featureFlags: [.multipleContentTypes], + """ + responses: + MultipleContentTypes: + description: Multiple content types + content: + application/json: + schema: + type: integer + text/plain: {} + application/octet-stream: {} + """, + """ + public enum Responses { + public struct MultipleContentTypes: Sendable, Equatable, Hashable { + public struct Headers: Sendable, Equatable, Hashable { public init() {} } + public var headers: Components.Responses.MultipleContentTypes.Headers + @frozen public enum Body: Sendable, Equatable, Hashable { + case json(Swift.Int) + case text(Swift.String) + case binary(Foundation.Data) + } + public var body: Components.Responses.MultipleContentTypes.Body + public init( + headers: Components.Responses.MultipleContentTypes.Headers = .init(), + body: Components.Responses.MultipleContentTypes.Body + ) { + self.headers = headers + self.body = body + } + } + } + """ + ) + } + + func testComponentsResponsesResponseWithHeader() throws { try self.assertResponsesTranslation( """ responses: @@ -727,6 +799,52 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } + func testComponentsRequestBodiesMultipleContentTypes() throws { + try self.assertRequestBodiesTranslation( + featureFlags: [], + ignoredDiagnosticMessages: [#"Feature "Multiple content types" is not supported, skipping"#], + """ + requestBodies: + MyResponseBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MyBody' + text/plain: {} + application/octet-stream: {} + """, + """ + public enum RequestBodies { + @frozen public enum MyResponseBody: Sendable, Equatable, Hashable { + case json(Components.Schemas.MyBody) + } + } + """ + ) + try self.assertRequestBodiesTranslation( + featureFlags: [.multipleContentTypes], + """ + requestBodies: + MyResponseBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MyBody' + text/plain: {} + application/octet-stream: {} + """, + """ + public enum RequestBodies { + @frozen public enum MyResponseBody: Sendable, Equatable, Hashable { + case json(Components.Schemas.MyBody) + case text(Swift.String) + case binary(Foundation.Data) + } + } + """ + ) + } + func testPathsSimplestCase() throws { try self.assertPathsTranslation( """ diff --git a/Tests/PetstoreConsumerTests/TestClient.swift b/Tests/PetstoreConsumerTests/TestClient.swift index dbd10184..8f731fc3 100644 --- a/Tests/PetstoreConsumerTests/TestClient.swift +++ b/Tests/PetstoreConsumerTests/TestClient.swift @@ -38,6 +38,28 @@ struct TestClient: APIProtocol { return try await block(input) } + typealias GetStatsSignature = @Sendable (Operations.getStats.Input) async throws -> Operations.getStats.Output + var getStatsBlock: GetStatsSignature? + func getStats( + _ input: Operations.getStats.Input + ) async throws -> Operations.getStats.Output { + guard let block = getStatsBlock else { + throw UnspecifiedBlockError() + } + return try await block(input) + } + + typealias PostStatsSignature = @Sendable (Operations.postStats.Input) async throws -> Operations.postStats.Output + var postStatsBlock: PostStatsSignature? + func postStats( + _ input: Operations.postStats.Input + ) async throws -> Operations.postStats.Output { + guard let block = postStatsBlock else { + throw UnspecifiedBlockError() + } + return try await block(input) + } + typealias ProbeSignature = @Sendable (Operations.probe.Input) async throws -> Operations.probe.Output var probeBlock: ProbeSignature? func probe( diff --git a/Tests/PetstoreConsumerTests/TestServer.swift b/Tests/PetstoreConsumerTests/TestServer.swift index 77060c68..23b004ba 100644 --- a/Tests/PetstoreConsumerTests/TestServer.swift +++ b/Tests/PetstoreConsumerTests/TestServer.swift @@ -77,6 +77,24 @@ extension TestServerTransport { } } + var getStats: Handler { + get throws { + try findHandler( + method: .get, + path: ["api", "pets", "stats"] + ) + } + } + + var postStats: Handler { + get throws { + try findHandler( + method: .post, + path: ["api", "pets", "stats"] + ) + } + } + var probe: Handler { get throws { try findHandler( diff --git a/Tests/PetstoreConsumerTests/Test_Client.swift b/Tests/PetstoreConsumerTests/Test_Client.swift index 2fdd4e05..31e6ecfc 100644 --- a/Tests/PetstoreConsumerTests/Test_Client.swift +++ b/Tests/PetstoreConsumerTests/Test_Client.swift @@ -342,6 +342,116 @@ final class Test_Client: XCTestCase { } } + func testGetStats_200_json() async throws { + transport = .init { request, baseURL, operationID in + XCTAssertEqual(operationID, "getStats") + XCTAssertEqual(request.path, "/pets/stats") + XCTAssertEqual(request.method, .get) + XCTAssertNil(request.body) + return .init( + statusCode: 200, + headers: [ + .init(name: "content-type", value: "application/json") + ], + encodedBody: #""" + { + "count" : 1 + } + """# + ) + } + let response = try await client.getStats(.init()) + guard case let .ok(value) = response else { + XCTFail("Unexpected response: \(response)") + return + } + switch value.body { + case .json(let stats): + XCTAssertEqual(stats, .init(count: 1)) + } + } + + func testGetStats_200_default_json() async throws { + transport = .init { request, baseURL, operationID in + XCTAssertEqual(operationID, "getStats") + XCTAssertEqual(request.path, "/pets/stats") + XCTAssertEqual(request.method, .get) + XCTAssertNil(request.body) + return .init( + statusCode: 200, + headers: [], + encodedBody: #""" + { + "count" : 1 + } + """# + ) + } + let response = try await client.getStats(.init()) + guard case let .ok(value) = response else { + XCTFail("Unexpected response: \(response)") + return + } + switch value.body { + case .json(let stats): + XCTAssertEqual(stats, .init(count: 1)) + } + } + + func testGetStats_200_unexpectedContentType() async throws { + transport = .init { request, baseURL, operationID in + XCTAssertEqual(operationID, "getStats") + XCTAssertEqual(request.path, "/pets/stats") + XCTAssertEqual(request.method, .get) + XCTAssertNil(request.body) + return .init( + statusCode: 200, + headers: [ + .init(name: "content-type", value: "foo/bar") + ], + encodedBody: #""" + count_is_1 + """# + ) + } + do { + _ = try await client.getStats(.init()) + XCTFail("Should have thrown an error") + } catch {} + } + + func testPostStats_202_json() async throws { + transport = .init { request, baseURL, operationID in + XCTAssertEqual(operationID, "postStats") + XCTAssertEqual(request.path, "/pets/stats") + XCTAssertNil(request.query) + XCTAssertEqual(baseURL.absoluteString, "/api") + XCTAssertEqual(request.method, .post) + XCTAssertEqual( + request.headerFields, + [ + .init(name: "content-type", value: "application/json; charset=utf-8") + ] + ) + XCTAssertEqual( + request.body?.pretty, + #""" + { + "count" : 1 + } + """# + ) + return .init(statusCode: 202) + } + let response = try await client.postStats( + .init(body: .json(.init(count: 1))) + ) + guard case .accepted = response else { + XCTFail("Unexpected response: \(response)") + return + } + } + @available(*, deprecated) func testProbe_204() async throws { transport = .init { request, baseURL, operationID in diff --git a/Tests/PetstoreConsumerTests/Test_Server.swift b/Tests/PetstoreConsumerTests/Test_Server.swift index 2ce84134..9e645eba 100644 --- a/Tests/PetstoreConsumerTests/Test_Server.swift +++ b/Tests/PetstoreConsumerTests/Test_Server.swift @@ -239,7 +239,6 @@ final class Test_Server: XCTestCase { fatalError("Unreachable") } ) - do { _ = try await server.createPet( .init( @@ -280,7 +279,8 @@ final class Test_Server: XCTestCase { path: "/api/pets/1", method: .patch, headerFields: [ - .init(name: "accept", value: "application/json") + .init(name: "accept", value: "application/json"), + .init(name: "content-type", value: "application/json"), ], encodedBody: #""" { @@ -298,6 +298,41 @@ final class Test_Server: XCTestCase { XCTAssertEqual(response.headerFields, []) } + func testUpdatePet_204_withBody_default_json() async throws { + client = .init( + updatePetBlock: { input in + XCTAssertEqual(input.path.petId, 1) + guard let body = input.body else { + throw TestError.unexpectedMissingRequestBody + } + guard case let .json(updatePet) = body else { + throw TestError.unexpectedValue(body) + } + XCTAssertEqual(updatePet, .init(name: "Fluffz")) + return .noContent(.init()) + } + ) + let response = try await server.updatePet( + .init( + path: "/api/pets/1", + method: .patch, + headerFields: [], + encodedBody: #""" + { + "name" : "Fluffz" + } + """# + ), + .init( + pathParameters: [ + "petId": "1" + ] + ) + ) + XCTAssertEqual(response.statusCode, 204) + XCTAssertEqual(response.headerFields, []) + } + func testUpdatePet_204_withoutBody() async throws { client = .init( updatePetBlock: { input in @@ -357,6 +392,124 @@ final class Test_Server: XCTestCase { ) } + func testGetStats_200_json() async throws { + client = .init( + getStatsBlock: { input in + return .ok(.init(body: .json(.init(count: 1)))) + } + ) + let response = try await server.getStats( + .init( + path: "/api/pets/stats", + method: .patch, + headerFields: [ + .init(name: "accept", value: "application/json, text/plain, application/octet-stream") + ] + ), + .init() + ) + XCTAssertEqual(response.statusCode, 200) + XCTAssertEqual( + response.headerFields, + [ + .init(name: "content-type", value: "application/json; charset=utf-8") + ] + ) + XCTAssertEqualStringifiedData( + response.body, + #""" + { + "count" : 1 + } + """# + ) + } + + func testGetStats_200_unexpectedAccept() async throws { + client = .init( + getStatsBlock: { input in + return .ok(.init(body: .json(.init(count: 1)))) + } + ) + do { + _ = try await server.getStats( + .init( + path: "/api/pets/stats", + method: .patch, + headerFields: [ + .init(name: "accept", value: "foo/bar") + ] + ), + .init() + ) + XCTFail("Should have thrown an error.") + } catch {} + } + + func testPostStats_202_json() async throws { + client = .init( + postStatsBlock: { input in + guard case let .json(stats) = input.body else { + throw TestError.unexpectedValue(input.body) + } + XCTAssertEqual(stats, .init(count: 1)) + return .accepted(.init()) + } + ) + let response = try await server.postStats( + .init( + path: "/api/pets/stats", + method: .post, + headerFields: [ + .init(name: "content-type", value: "application/json; charset=utf-8") + ], + encodedBody: #""" + { + "count" : 1 + } + """# + ), + .init() + ) + XCTAssertEqual(response.statusCode, 202) + XCTAssertEqual( + response.headerFields, + [] + ) + XCTAssert(response.body.isEmpty) + } + + func testPostStats_202_default_json() async throws { + client = .init( + postStatsBlock: { input in + guard case let .json(stats) = input.body else { + throw TestError.unexpectedValue(input.body) + } + XCTAssertEqual(stats, .init(count: 1)) + return .accepted(.init()) + } + ) + let response = try await server.postStats( + .init( + path: "/api/pets/stats", + method: .post, + headerFields: [], + encodedBody: #""" + { + "count" : 1 + } + """# + ), + .init() + ) + XCTAssertEqual(response.statusCode, 202) + XCTAssertEqual( + response.headerFields, + [] + ) + XCTAssert(response.body.isEmpty) + } + func testProbe_204() async throws { client = .init( probeBlock: { input in diff --git a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Generated b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Generated new file mode 120000 index 00000000..a2357187 --- /dev/null +++ b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Generated @@ -0,0 +1 @@ +../OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes \ No newline at end of file diff --git a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/TestClient.swift b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/TestClient.swift new file mode 100644 index 00000000..8f731fc3 --- /dev/null +++ b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/TestClient.swift @@ -0,0 +1,112 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import OpenAPIRuntime +import Foundation + +struct TestClient: APIProtocol { + + typealias ListPetsSignature = @Sendable (Operations.listPets.Input) async throws -> Operations.listPets.Output + var listPetsBlock: ListPetsSignature? + func listPets( + _ input: Operations.listPets.Input + ) async throws -> Operations.listPets.Output { + guard let block = listPetsBlock else { + throw UnspecifiedBlockError() + } + return try await block(input) + } + + typealias CreatePetSignature = @Sendable (Operations.createPet.Input) async throws -> Operations.createPet.Output + var createPetBlock: CreatePetSignature? + func createPet( + _ input: Operations.createPet.Input + ) async throws -> Operations.createPet.Output { + guard let block = createPetBlock else { + throw UnspecifiedBlockError() + } + return try await block(input) + } + + typealias GetStatsSignature = @Sendable (Operations.getStats.Input) async throws -> Operations.getStats.Output + var getStatsBlock: GetStatsSignature? + func getStats( + _ input: Operations.getStats.Input + ) async throws -> Operations.getStats.Output { + guard let block = getStatsBlock else { + throw UnspecifiedBlockError() + } + return try await block(input) + } + + typealias PostStatsSignature = @Sendable (Operations.postStats.Input) async throws -> Operations.postStats.Output + var postStatsBlock: PostStatsSignature? + func postStats( + _ input: Operations.postStats.Input + ) async throws -> Operations.postStats.Output { + guard let block = postStatsBlock else { + throw UnspecifiedBlockError() + } + return try await block(input) + } + + typealias ProbeSignature = @Sendable (Operations.probe.Input) async throws -> Operations.probe.Output + var probeBlock: ProbeSignature? + func probe( + _ input: Operations.probe.Input + ) async throws -> Operations.probe.Output { + guard let block = probeBlock else { + throw UnspecifiedBlockError() + } + return try await block(input) + } + + typealias UpdatePetSignature = @Sendable (Operations.updatePet.Input) async throws -> Operations.updatePet.Output + var updatePetBlock: UpdatePetSignature? + func updatePet( + _ input: Operations.updatePet.Input + ) async throws -> Operations.updatePet.Output { + guard let block = updatePetBlock else { + throw UnspecifiedBlockError() + } + return try await block(input) + } + + typealias UploadAvatarForPetSignature = @Sendable (Operations.uploadAvatarForPet.Input) async throws -> Operations + .uploadAvatarForPet.Output + var uploadAvatarForPetBlock: UploadAvatarForPetSignature? + func uploadAvatarForPet( + _ input: Operations.uploadAvatarForPet.Input + ) async throws -> Operations.uploadAvatarForPet.Output { + guard let block = uploadAvatarForPetBlock else { + throw UnspecifiedBlockError() + } + return try await block(input) + } +} + +struct UnspecifiedBlockError: Swift.Error, LocalizedError, CustomStringConvertible { + var function: StaticString + + var description: String { + "Unspecified block for \(function)" + } + + var errorDescription: String? { + description + } + + init(function: StaticString = #function) { + self.function = function + } +} diff --git a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/TestServer.swift b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/TestServer.swift new file mode 100644 index 00000000..23b004ba --- /dev/null +++ b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/TestServer.swift @@ -0,0 +1,115 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import OpenAPIRuntime +import Foundation +import PetstoreConsumerTestCore + +extension APIProtocol { + func configuredServer( + for serverURLString: String = "/api" + ) throws -> TestServerTransport { + let transport = TestServerTransport() + try registerHandlers( + on: transport, + serverURL: try URL(validatingOpenAPIServerURL: serverURLString) + ) + return transport + } +} + +extension TestServerTransport { + + private func findHandler( + method: HTTPMethod, + path: [RouterPathComponent] + ) throws -> TestServerTransport.Handler { + guard + let handler = registered.first(where: { operation in + guard operation.inputs.method == method else { + return false + } + guard operation.inputs.path == path else { + return false + } + return true + }) + else { + throw TestError.noHandlerFound(method: method, path: path) + } + return handler.closure + } + + var listPets: Handler { + get throws { + try findHandler( + method: .get, + path: ["api", "pets"] + ) + } + } + + var createPet: Handler { + get throws { + try findHandler( + method: .post, + path: ["api", "pets"] + ) + } + } + + var updatePet: Handler { + get throws { + try findHandler( + method: .patch, + path: ["api", "pets", ":petId"] + ) + } + } + + var getStats: Handler { + get throws { + try findHandler( + method: .get, + path: ["api", "pets", "stats"] + ) + } + } + + var postStats: Handler { + get throws { + try findHandler( + method: .post, + path: ["api", "pets", "stats"] + ) + } + } + + var probe: Handler { + get throws { + try findHandler( + method: .post, + path: ["api", "probe"] + ) + } + } + + var uploadAvatarForPet: Handler { + get throws { + try findHandler( + method: .put, + path: ["api", "pets", ":petId", "avatar"] + ) + } + } +} diff --git a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift new file mode 100644 index 00000000..fdc87eb1 --- /dev/null +++ b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift @@ -0,0 +1,152 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +import OpenAPIRuntime +import PetstoreConsumerTestCore + +final class Test_Client: XCTestCase { + + var transport: TestClientTransport! + var client: Client { + get throws { + .init( + serverURL: try URL(validatingOpenAPIServerURL: "/api"), + transport: transport + ) + } + } + + override func setUp() async throws { + try await super.setUp() + continueAfterFailure = false + } + + func testGetStats_200_text() async throws { + transport = .init { request, baseURL, operationID in + XCTAssertEqual(operationID, "getStats") + XCTAssertEqual(request.path, "/pets/stats") + XCTAssertEqual(request.method, .get) + XCTAssertNil(request.body) + return .init( + statusCode: 200, + headers: [ + .init(name: "content-type", value: "text/plain") + ], + encodedBody: #""" + count is 1 + """# + ) + } + let response = try await client.getStats(.init()) + guard case let .ok(value) = response else { + XCTFail("Unexpected response: \(response)") + return + } + switch value.body { + case .text(let stats): + XCTAssertEqual(stats, "count is 1") + default: + XCTFail("Unexpected content type") + } + } + + func testGetStats_200_binary() async throws { + transport = .init { request, baseURL, operationID in + XCTAssertEqual(operationID, "getStats") + XCTAssertEqual(request.path, "/pets/stats") + XCTAssertEqual(request.method, .get) + XCTAssertNil(request.body) + return .init( + statusCode: 200, + headers: [ + .init(name: "content-type", value: "application/octet-stream") + ], + encodedBody: #""" + count_is_1 + """# + ) + } + let response = try await client.getStats(.init()) + guard case let .ok(value) = response else { + XCTFail("Unexpected response: \(response)") + return + } + switch value.body { + case .binary(let stats): + XCTAssertEqual(String(decoding: stats, as: UTF8.self), "count_is_1") + default: + XCTFail("Unexpected content type") + } + } + + func testPostStats_202_text() async throws { + transport = .init { request, baseURL, operationID in + XCTAssertEqual(operationID, "postStats") + XCTAssertEqual(request.path, "/pets/stats") + XCTAssertNil(request.query) + XCTAssertEqual(baseURL.absoluteString, "/api") + XCTAssertEqual(request.method, .post) + XCTAssertEqual( + request.headerFields, + [ + .init(name: "content-type", value: "text/plain") + ] + ) + XCTAssertEqual( + request.body?.pretty, + #""" + count is 1 + """# + ) + return .init(statusCode: 202) + } + let response = try await client.postStats( + .init(body: .text("count is 1")) + ) + guard case .accepted = response else { + XCTFail("Unexpected response: \(response)") + return + } + } + + func testPostStats_202_binary() async throws { + transport = .init { request, baseURL, operationID in + XCTAssertEqual(operationID, "postStats") + XCTAssertEqual(request.path, "/pets/stats") + XCTAssertNil(request.query) + XCTAssertEqual(baseURL.absoluteString, "/api") + XCTAssertEqual(request.method, .post) + XCTAssertEqual( + request.headerFields, + [ + .init(name: "content-type", value: "application/octet-stream") + ] + ) + XCTAssertEqual( + request.body?.pretty, + #""" + count_is_1 + """# + ) + return .init(statusCode: 202) + } + let response = try await client.postStats( + .init(body: .binary(Data("count_is_1".utf8))) + ) + guard case .accepted = response else { + XCTFail("Unexpected response: \(response)") + return + } + } +} diff --git a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift new file mode 100644 index 00000000..2e7c2be7 --- /dev/null +++ b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift @@ -0,0 +1,155 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +import OpenAPIRuntime +import PetstoreConsumerTestCore + +final class Test_Server: XCTestCase { + + var client: TestClient! + var server: TestServerTransport { + get throws { + try client.configuredServer() + } + } + + override func setUp() async throws { + try await super.setUp() + continueAfterFailure = false + } + + func testGetStats_200_text() async throws { + client = .init( + getStatsBlock: { input in + return .ok(.init(body: .text("count is 1"))) + } + ) + let response = try await server.getStats( + .init( + path: "/api/pets/stats", + method: .patch, + headerFields: [ + .init(name: "accept", value: "application/json, text/plain, application/octet-stream") + ] + ), + .init() + ) + XCTAssertEqual(response.statusCode, 200) + XCTAssertEqual( + response.headerFields, + [ + .init(name: "content-type", value: "text/plain") + ] + ) + XCTAssertEqualStringifiedData( + response.body, + #""" + count is 1 + """# + ) + } + + func testGetStats_200_binary() async throws { + client = .init( + getStatsBlock: { input in + return .ok(.init(body: .binary(Data("count_is_1".utf8)))) + } + ) + let response = try await server.getStats( + .init( + path: "/api/pets/stats", + method: .patch, + headerFields: [ + .init(name: "accept", value: "application/json, text/plain, application/octet-stream") + ] + ), + .init() + ) + XCTAssertEqual(response.statusCode, 200) + XCTAssertEqual( + response.headerFields, + [ + .init(name: "content-type", value: "application/octet-stream") + ] + ) + XCTAssertEqualStringifiedData( + response.body, + #""" + count_is_1 + """# + ) + } + + func testPostStats_202_text() async throws { + client = .init( + postStatsBlock: { input in + guard case let .text(stats) = input.body else { + throw TestError.unexpectedValue(input.body) + } + XCTAssertEqual(stats, "count is 1") + return .accepted(.init()) + } + ) + let response = try await server.postStats( + .init( + path: "/api/pets/stats", + method: .post, + headerFields: [ + .init(name: "content-type", value: "text/plain") + ], + encodedBody: #""" + count is 1 + """# + ), + .init() + ) + XCTAssertEqual(response.statusCode, 202) + XCTAssertEqual( + response.headerFields, + [] + ) + XCTAssert(response.body.isEmpty) + } + + func testPostStats_202_binary() async throws { + client = .init( + postStatsBlock: { input in + guard case let .binary(stats) = input.body else { + throw TestError.unexpectedValue(input.body) + } + XCTAssertEqualStringifiedData(stats, "count_is_1") + return .accepted(.init()) + } + ) + let response = try await server.postStats( + .init( + path: "/api/pets/stats", + method: .post, + headerFields: [ + .init(name: "content-type", value: "application/octet-stream") + ], + encodedBody: #""" + count_is_1 + """# + ), + .init() + ) + XCTAssertEqual(response.statusCode, 202) + XCTAssertEqual( + response.headerFields, + [] + ) + XCTAssert(response.body.isEmpty) + } +} diff --git a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Types.swift b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Types.swift new file mode 100644 index 00000000..46d28f9e --- /dev/null +++ b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Types.swift @@ -0,0 +1,233 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +import OpenAPIRuntime +import PetstoreConsumerTestCore + +final class Test_Types: XCTestCase { + + override func setUp() async throws { + try await super.setUp() + continueAfterFailure = false + } + + func testStructCodingKeys() throws { + let cases: [(Components.Schemas._Error.CodingKeys, String)] = [ + (.code, "code"), + (.me_sage, "me$sage"), + ] + for (value, rawValue) in cases { + XCTAssertEqual(value.rawValue, rawValue) + } + } + + func testEnumCoding() throws { + let cases: [(Components.Schemas.PetKind, String)] = [ + (.cat, "cat"), + (._nake, "$nake"), + ] + for (value, rawValue) in cases { + XCTAssertEqual(value.rawValue, rawValue) + } + } + + var testEncoder: JSONEncoder { + .init() + } + + var testDecoder: JSONDecoder { + .init() + } + + func roundtrip(_ value: T) throws -> T { + let data = try testEncoder.encode(value) + return try testDecoder.decode(T.self, from: data) + } + + func _testRoundtrip(_ value: T) throws { + let decodedValue = try roundtrip(value) + XCTAssertEqual(decodedValue, value) + } + + func testNoAdditionalPropertiesCoding_roundtrip() throws { + try _testRoundtrip( + Components.Schemas.NoAdditionalProperties(foo: "hi") + ) + } + + func testNoAdditionalPropertiesCoding_extraProperty() throws { + XCTAssertThrowsError( + try testDecoder.decode( + Components.Schemas.NoAdditionalProperties.self, + from: Data(#"{"foo":"hi","hello":1}"#.utf8) + ) + ) + } + + func testAnyAdditionalPropertiesCoding_roundtrip_noExtraProperty() throws { + try _testRoundtrip( + Components.Schemas.AnyAdditionalProperties( + foo: "hi", + additionalProperties: .init() + ) + ) + } + + func testAnyAdditionalPropertiesCoding_roundtrip_withExtraProperty() throws { + try _testRoundtrip( + Components.Schemas.AnyAdditionalProperties( + foo: "hi", + additionalProperties: .init(unvalidatedValue: [ + "hello": 1 + ]) + ) + ) + } + + func testTypedAdditionalPropertiesCoding_roundtrip_noExtraProperty() throws { + try _testRoundtrip( + Components.Schemas.TypedAdditionalProperties( + foo: "hi", + additionalProperties: [:] + ) + ) + } + + func testTypedAdditionalPropertiesCoding_roundtrip_withExtraProperty() throws { + try _testRoundtrip( + Components.Schemas.TypedAdditionalProperties( + foo: "hi", + additionalProperties: [ + "hello": 1 + ] + ) + ) + } + + func testAllOf_roundtrip() throws { + try _testRoundtrip( + Components.Schemas.AllOfObjects( + value1: .init(message: "hi"), + value2: .init(code: 1) + ) + ) + } + + func testAllOf_missingProperty() throws { + XCTAssertThrowsError( + try testDecoder.decode( + Components.Schemas.AllOfObjects.self, + from: Data(#"{}"#.utf8) + ) + ) + XCTAssertThrowsError( + try testDecoder.decode( + Components.Schemas.AllOfObjects.self, + from: Data(#"{"message":"hi"}"#.utf8) + ) + ) + XCTAssertThrowsError( + try testDecoder.decode( + Components.Schemas.AllOfObjects.self, + from: Data(#"{"code":1}"#.utf8) + ) + ) + } + + func testAnyOf_roundtrip() throws { + try _testRoundtrip( + Components.Schemas.AnyOfObjects( + value1: .init(message: "hi"), + value2: .init(code: 1) + ) + ) + try _testRoundtrip( + Components.Schemas.AnyOfObjects( + value1: .init(message: "hi"), + value2: nil + ) + ) + try _testRoundtrip( + Components.Schemas.AnyOfObjects( + value1: nil, + value2: .init(code: 1) + ) + ) + } + + func testAnyOf_allFailedToDecode() throws { + XCTAssertThrowsError( + try testDecoder.decode( + Components.Schemas.AnyOfObjects.self, + from: Data(#"{}"#.utf8) + ) + ) + } + + func testOneOfAny_roundtrip() throws { + try _testRoundtrip( + Components.Schemas.OneOfAny.case1("hi") + ) + try _testRoundtrip( + Components.Schemas.OneOfAny.case2(1) + ) + try _testRoundtrip( + Components.Schemas.OneOfAny.CodeError(.init(code: 2)) + ) + try _testRoundtrip( + Components.Schemas.OneOfAny.case4(.init(message: "hello")) + ) + try _testRoundtrip( + Components.Schemas.OneOfAny.undocumented(true) + ) + } + + func testOneOfWithDiscriminator_roundtrip() throws { + try _testRoundtrip( + Components.Schemas.OneOfObjectsWithDiscriminator + .Walk( + .init( + kind: "Walk", + length: 1 + ) + ) + ) + try _testRoundtrip( + Components.Schemas.OneOfObjectsWithDiscriminator + .MessagedExercise( + .init( + value1: .init(kind: "MessagedExercise"), + value2: .init(message: "hello") + ) + ) + ) + try _testRoundtrip( + Components.Schemas.OneOfObjectsWithDiscriminator + .undocumented( + .init(unvalidatedValue: [ + "kind": "nope" + ]) + ) + ) + } + + func testOneOfWithDiscriminator_invalidDiscriminator() throws { + XCTAssertThrowsError( + try testDecoder.decode( + Components.Schemas.OneOfObjectsWithDiscriminator.self, + from: Data(#"{}"#.utf8) + ) + ) + } +} diff --git a/scripts/check-license-headers.sh b/scripts/check-license-headers.sh index d0e0c398..b38c8bfb 100644 --- a/scripts/check-license-headers.sh +++ b/scripts/check-license-headers.sh @@ -54,6 +54,7 @@ read -ra PATHS_TO_CHECK_FOR_LICENSE <<< "$( \ ":(exclude)SECURITY.md" \ ":(exclude)scripts/unacceptable-language.txt" \ ":(exclude)Tests/PetstoreConsumerTests/Generated" \ + ":(exclude)Tests/PetstoreConsumerTestsFFMultipleContentTypes/Generated" \ ":(exclude)Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/*" \ ":(exclude)docker/*" \ ":(exclude)**/*.docc/*" \