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/*" \