diff --git a/Package.swift b/Package.swift index e19d68e2..87a3e1a1 100644 --- a/Package.swift +++ b/Package.swift @@ -61,7 +61,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.0")), + .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.1.1")), // Build and preview docs .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), diff --git a/Sources/_OpenAPIGeneratorCore/Extensions/Foundation.swift b/Sources/_OpenAPIGeneratorCore/Extensions/Foundation.swift index ce0c7368..34b1cb41 100644 --- a/Sources/_OpenAPIGeneratorCore/Extensions/Foundation.swift +++ b/Sources/_OpenAPIGeneratorCore/Extensions/Foundation.swift @@ -21,17 +21,7 @@ extension Data { /// - Throws: When data is not valid UTF-8. var swiftFormatted: Data { get throws { - struct FormattingError: Error, LocalizedError, CustomStringConvertible { - var description: String { - "Invalid UTF-8 data" - } - var errorDescription: String? { - description - } - } - guard let string = String(data: self, encoding: .utf8) else { - throw FormattingError() - } + let string = String(decoding: self, as: UTF8.self) return try Self(string.swiftFormatted.utf8) } } diff --git a/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift b/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift index b149ef37..4985ebb9 100644 --- a/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift +++ b/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift @@ -33,7 +33,7 @@ struct TextBasedRenderer: RendererProtocol { /// Renders the specified Swift file. func renderFile(_ description: FileDescription) -> Data { - renderedFile(description).data(using: .utf8)! + Data(renderedFile(description).utf8) } /// Renders the specified comment. diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift index bf3e0825..3186cd49 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift @@ -22,16 +22,28 @@ extension ClientFileTranslator { _ description: OperationDescription ) throws -> Expression { - let clientPathTemplate = try translatePathParameterInClient( + let (pathTemplate, pathParamsArrayExpr) = try translatePathParameterInClient( description: description ) + let pathDecl: Declaration = .variable( + kind: .let, + left: "path", + right: .try( + .identifier("converter") + .dot("renderedRequestPath") + .call([ + .init(label: "template", expression: .literal(pathTemplate)), + .init(label: "parameters", expression: pathParamsArrayExpr), + ]) + ) + ) let requestDecl: Declaration = .variable( kind: .var, left: "request", type: TypeName.request.fullyQualifiedSwiftName, right: .dot("init") .call([ - .init(label: "path", expression: .literal(clientPathTemplate)), + .init(label: "path", expression: .identifier("path")), .init(label: "method", expression: .dot(description.httpMethodLowercased)), ]) ) @@ -65,9 +77,12 @@ extension ClientFileTranslator { .map(\.headerValueForValidation) .joined(separator: ", ") let addAcceptHeaderExpr: Expression = .try( - .identifier("converter").dot("headerFieldAdd") + .identifier("converter").dot("setHeaderFieldAsText") .call([ - .init(label: "in", expression: .inOut(.identifier("request").dot("headerFields"))), + .init( + label: "in", + expression: .inOut(.identifier("request").dot("headerFields")) + ), .init(label: "name", expression: "accept"), .init(label: "value", expression: .literal(acceptValue)), ]) @@ -91,6 +106,7 @@ extension ClientFileTranslator { "input" ], body: [ + .declaration(pathDecl), .declaration(requestDecl), .expression(requestDecl.suppressMutabilityWarningExpr), ] + requestExprs.map { .expression($0) } + [ diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index 43b17be2..042ac6ff 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -336,6 +336,19 @@ enum Constants { ] } + /// Constants related to the coding strategy. + enum CodingStrategy { + + /// The substring used in method names for the JSON coding strategy. + static let json: String = "JSON" + + /// The substring used in method names for the text coding strategy. + static let text: String = "Text" + + /// The substring used in method names for the binary coding strategy. + static let binary: String = "Binary" + } + /// Constants related to types used in many components. enum Global { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Content/CodingStrategy.swift b/Sources/_OpenAPIGeneratorCore/Translator/Content/CodingStrategy.swift new file mode 100644 index 00000000..921db100 --- /dev/null +++ b/Sources/_OpenAPIGeneratorCore/Translator/Content/CodingStrategy.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// Describes the underlying coding strategy. +enum CodingStrategy: String, Equatable, Hashable, Sendable { + + /// A strategy using JSONEncoder/JSONDecoder. + case json + + /// A strategy using LosslessStringConvertible. + case text + + /// A strategy that passes through the data unmodified. + case binary + + /// The name of the coding strategy in the runtime library. + var runtimeName: String { + switch self { + case .json: + return Constants.CodingStrategy.json + case .text: + return Constants.CodingStrategy.text + case .binary: + return Constants.CodingStrategy.binary + } + } +} diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift index 9f197145..1db30f73 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift @@ -82,6 +82,18 @@ enum ContentType: Hashable { } } + /// The coding strategy appropriate for this content type. + var codingStrategy: CodingStrategy { + switch self { + case .json: + return .json + case .text: + return .text + case .binary: + return .binary + } + } + /// A Boolean value that indicates whether the content type /// is a type of JSON. var isJSON: Bool { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift b/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift index d2365310..c9b2be36 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift @@ -232,23 +232,28 @@ extension OperationDescription { } /// Returns a string that contains the template to be generated for - /// the client that fills in path parameters. + /// the client that fills in path parameters, and an array expression + /// with the parameter values. /// - /// For example, `/cats/\(input.catId)`. - var templatedPathForClient: String { + /// For example, `/cats/{}` and `[input.catId]`. + var templatedPathForClient: (String, Expression) { get throws { let path = self.path.rawValue let pathParameters = try allResolvedParameters.filter { $0.location == .path } - guard !pathParameters.isEmpty else { - return path - } - // replace "{foo}" with "\(input.foo)" for each parameter - return pathParameters.reduce(into: path) { partialResult, parameter in + // replace "{foo}" with "{}" for each parameter + let template = pathParameters.reduce(into: path) { partialResult, parameter in partialResult = partialResult.replacingOccurrences( of: "{\(parameter.name)}", - with: "\\(input.path.\(parameter.name.asSwiftSafeName))" + with: "{}" ) } + let names: [Expression] = + pathParameters + .map { param in + .identifier("input.path.\(param.name.asSwiftSafeName)") + } + let arrayExpr: Expression = .literal(.array(names)) + return (template, arrayExpr) } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift index 543987e9..36b901ce 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift @@ -24,6 +24,9 @@ struct TypedParameter { /// The computed type usage. var typeUsage: TypeUsage + + /// The coding strategy appropriate for this parameter. + var codingStrategy: CodingStrategy } extension TypedParameter: CustomStringConvertible { @@ -126,9 +129,11 @@ extension FileTranslator { let foundIn = "\(locationTypeName.description)/\(parameter.name)" let schema: Either, JSONSchema> + let codingStrategy: CodingStrategy switch parameter.schemaOrContent { case let .a(schemaContext): schema = schemaContext.schema + codingStrategy = .text // Check supported exploded/style types let location = parameter.location @@ -175,6 +180,11 @@ extension FileTranslator { return nil } schema = typedContent.content.schema ?? .b(.fragment) + codingStrategy = + typedContent + .content + .contentType + .codingStrategy } // Check if the underlying schema is supported @@ -207,7 +217,8 @@ extension FileTranslator { return .init( parameter: parameter, schema: schema, - typeUsage: usage + typeUsage: usage, + codingStrategy: codingStrategy ) } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift index c438d8cd..a70f656a 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift @@ -91,11 +91,12 @@ extension TypesFileTranslator { extension ClientFileTranslator { /// Returns a templated string that includes all path parameters in - /// the specified operation. + /// the specified operation, and an expression of an array literal + /// with all those parameters. /// - Parameter description: The OpenAPI operation. func translatePathParameterInClient( description: OperationDescription - ) throws -> String { + ) throws -> (String, Expression) { try description.templatedPathForClient } @@ -115,10 +116,10 @@ extension ClientFileTranslator { let containerExpr: Expression switch parameter.location { case .header: - methodPrefix = "headerField" + methodPrefix = "HeaderField" containerExpr = .identifier(requestVariableName).dot("headerFields") case .query: - methodPrefix = "query" + methodPrefix = "QueryItem" containerExpr = .identifier(requestVariableName) default: diagnostics.emitUnsupported( @@ -129,20 +130,22 @@ extension ClientFileTranslator { } return .try( .identifier("converter") - .dot("\(methodPrefix)Add") - .call([ - .init( - label: "in", - expression: .inOut(containerExpr) - ), - .init(label: "name", expression: .literal(parameter.name)), - .init( - label: "value", - expression: .identifier(inputVariableName) - .dot(parameter.location.shortVariableName) - .dot(parameter.variableName) - ), - ]) + .dot("set\(methodPrefix)As\(parameter.codingStrategy.runtimeName)") + .call( + [ + .init( + label: "in", + expression: .inOut(containerExpr) + ), + .init(label: "name", expression: .literal(parameter.name)), + .init( + label: "value", + expression: .identifier(inputVariableName) + .dot(parameter.location.shortVariableName) + .dot(parameter.variableName) + ), + ] + ) ) } } @@ -160,17 +163,21 @@ extension ServerFileTranslator { .typeUsage .fullyQualifiedNonOptionalSwiftName + func methodName(_ parameterLocationName: String, _ requiresOptionality: Bool = true) -> String { + let optionality: String + if requiresOptionality { + optionality = parameter.required ? "Required" : "Optional" + } else { + optionality = "" + } + return "get\(optionality)\(parameterLocationName)As\(typedParameter.codingStrategy.runtimeName)" + } + let convertExpr: Expression switch parameter.location { case .path: - let methodName: String - if parameter.required { - methodName = "pathGetRequired" - } else { - methodName = "pathGetOptional" - } convertExpr = .try( - .identifier("converter").dot(methodName) + .identifier("converter").dot(methodName("PathParameter", false)) .call([ .init(label: "in", expression: .identifier("metadata").dot("pathParameters")), .init(label: "name", expression: .literal(parameter.name)), @@ -181,14 +188,8 @@ extension ServerFileTranslator { ]) ) case .query: - let methodName: String - if parameter.required { - methodName = "queryGetRequired" - } else { - methodName = "queryGetOptional" - } convertExpr = .try( - .identifier("converter").dot(methodName) + .identifier("converter").dot(methodName("QueryItem")) .call([ .init(label: "in", expression: .identifier("metadata").dot("queryParameters")), .init(label: "name", expression: .literal(parameter.name)), @@ -199,15 +200,9 @@ extension ServerFileTranslator { ]) ) case .header: - let methodName: String - if parameter.required { - methodName = "headerFieldGetRequired" - } else { - methodName = "headerFieldGetOptional" - } convertExpr = .try( .identifier("converter") - .dot(methodName) + .dot(methodName("HeaderField")) .call([ .init(label: "in", expression: .identifier("request").dot("headerFields")), .init(label: "name", expression: .literal(parameter.name)), diff --git a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift index f01c846a..92aee5f1 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift @@ -183,8 +183,14 @@ extension ClientFileTranslator { let transformReturnExpr: Expression = .return( .dot("init") .call([ - .init(label: "value", expression: .identifier("value")), - .init(label: "contentType", expression: .literal(contentTypeHeaderValue)), + .init( + label: "value", + expression: .identifier("value") + ), + .init( + label: "contentType", + expression: .literal(contentTypeHeaderValue) + ), ]) ) let caseDecl: SwitchCaseDescription = .init( @@ -210,7 +216,9 @@ extension ClientFileTranslator { left: .identifier(requestVariableName).dot("body"), right: .try( .identifier("converter") - .dot("bodyAdd\(requestBody.request.required ? "Required" : "Optional")") + .dot( + "set\(requestBody.request.required ? "Required" : "Optional")RequestBodyAs\(contentType.codingStrategy.runtimeName)" + ) .call([ .init(label: nil, expression: .identifier(inputVariableName).dot("body")), .init( @@ -260,7 +268,9 @@ extension ServerFileTranslator { let typedContent = requestBody.content let contentTypeUsage = typedContent.resolvedTypeUsage let content = typedContent.content - let contentTypeIdentifier = content.contentType.identifier + let contentType = content.contentType + let contentTypeIdentifier = contentType.identifier + let codingStrategyName = contentType.codingStrategy.runtimeName let isOptional = !requestBody.request.required let transformExpr: Expression = .closureInvocation( @@ -276,7 +286,7 @@ extension ServerFileTranslator { ) let initExpr: Expression = .try( .identifier("converter") - .dot("bodyGet\(isOptional ? "Optional" : "Required")") + .dot("get\(isOptional ? "Optional" : "Required")RequestBodyAs\(codingStrategyName)") .call([ .init( label: nil, diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift index edf658a5..7accb2e8 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift @@ -28,6 +28,9 @@ struct TypedResponseHeader { /// The Swift type representing the response header. var typeUsage: TypeUsage + + /// The coding strategy appropriate for this parameter. + var codingStrategy: CodingStrategy } extension TypedResponseHeader { @@ -101,10 +104,12 @@ extension FileTranslator { let foundIn = "\(parent.description)/\(name)" let schema: Either, JSONSchema> + let codingStrategy: CodingStrategy switch header.schemaOrContent { case let .a(schemaContext): schema = schemaContext.schema + codingStrategy = .text case let .b(contentMap): guard let typedContent = try bestSingleTypedContent( @@ -116,6 +121,11 @@ extension FileTranslator { return nil } schema = typedContent.content.schema ?? .b(.fragment) + codingStrategy = + typedContent + .content + .contentType + .codingStrategy } // Check if schema is supported @@ -149,7 +159,8 @@ extension FileTranslator { header: header, name: name, schema: schema, - typeUsage: usage + typeUsage: usage, + codingStrategy: codingStrategy ) } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift index 4b233013..6ed7166d 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift @@ -95,7 +95,9 @@ extension ClientFileTranslator { label: header.variableName, expression: .try( .identifier("converter") - .dot("headerFieldGet\(header.isOptional ? "Optional" : "Required")") + .dot( + "get\(header.isOptional ? "Optional" : "Required")HeaderFieldAs\(header.codingStrategy.runtimeName)" + ) .call([ .init( label: "in", @@ -131,7 +133,7 @@ extension ServerFileTranslator { ) throws -> Expression { return .try( .identifier("converter") - .dot("headerFieldAdd") + .dot("setHeaderFieldAs\(header.codingStrategy.runtimeName)") .call([ .init( label: "in", diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift index 0ccf1c22..a6dab94b 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift @@ -182,7 +182,7 @@ extension ClientFileTranslator { type: bodyTypeName.fullyQualifiedSwiftName, right: .try( .identifier("converter") - .dot("bodyGet") + .dot("getResponseBodyAs\(typedContent.content.contentType.codingStrategy.runtimeName)") .call([ .init( label: nil, @@ -329,7 +329,10 @@ extension ServerFileTranslator { .return( .dot("init") .call([ - .init(label: "value", expression: .identifier("value")), + .init( + label: "value", + expression: .identifier("value") + ), .init( label: "contentType", expression: .literal(contentType.headerValueForSending) @@ -356,7 +359,7 @@ extension ServerFileTranslator { left: .identifier("response").dot("body"), right: .try( .identifier("converter") - .dot("bodyAdd") + .dot("setResponseBodyAs\(contentType.codingStrategy.runtimeName)") .call([ .init(label: nil, expression: .identifier("value").dot("body")), .init( diff --git a/Sources/swift-openapi-generator/Documentation.docc/Development/Converting-between-data-and-Swift-types.md b/Sources/swift-openapi-generator/Documentation.docc/Development/Converting-between-data-and-Swift-types.md index f8995ccc..0d4f4300 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Development/Converting-between-data-and-Swift-types.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Development/Converting-between-data-and-Swift-types.md @@ -65,63 +65,65 @@ Together, the dimensions are enough to deterministically decide which helper met In the list below, each row represents one helper method. +The helper method naming convention can be described as: + +``` +method name: {set,get}{required/optional/omit if both}{location}As{strategy} +method parameters: value or type of value +``` + | Client/server | Set/get | Schema location | Coding strategy | Swift type | Optional/required | Method name | | --------------| ------- | --------------- | --------------- | ---------- | ------------------| ----------- | -| common | set | header field | text | string-convertible | both | TODO | -| common | set | header field | text | array of string-convertibles | both | TODO | -| common | set | header field | text | date | both | TODO | -| common | set | header field | text | array of dates | both | TODO | -| common | set | header field | JSON | codable | both | TODO | -| common | get | header field | text | string-convertible | optional | TODO | -| common | get | header field | text | string-convertible | required | TODO | -| common | get | header field | text | array of string-convertibles | optional | TODO | -| common | get | header field | text | array of string-convertibles | required | TODO | -| common | get | header field | text | date | optional | TODO | -| common | get | header field | text | date | required | TODO | -| common | get | header field | text | array of dates | optional | TODO | -| common | get | header field | text | array of dates | required | TODO | -| common | get | header field | JSON | codable | optional | TODO | -| common | get | header field | JSON | codable | required | TODO | -| client | set | request path | text | string-convertible | both | TODO | -| client | set | request path | text | date | both | TODO | -| client | set | request query | text | string-convertible | both | TODO | -| client | set | request query | text | array of string-convertibles | both | TODO | -| client | set | request query | text | date | both | TODO | -| client | set | request query | text | array of dates | both | TODO | -| client | set | request query | text | array of dates | both | TODO | -| client | set | request body | text | string-convertible | optional | TODO | -| client | set | request body | text | string-convertible | required | TODO | -| client | set | request body | text | date | optional | TODO | -| client | set | request body | text | date | required | TODO | -| client | set | request body | JSON | codable | optional | TODO | -| client | set | request body | JSON | codable | required | TODO | -| client | set | request body | binary | data | optional | TODO | -| client | set | request body | binary | data | required | TODO | -| client | get | response body | text | string-convertible | required | TODO | -| client | get | response body | text | date | required | TODO | -| client | get | response body | JSON | codable | required | TODO | -| client | get | response body | binary | data | required | TODO | -| server | get | request path | text | string-convertible | optional | TODO | -| server | get | request path | text | string-convertible | required | TODO | -| server | get | request path | text | date | optional | TODO | -| server | get | request path | text | date | required | TODO | -| server | get | request query | text | string-convertible | optional | TODO | -| server | get | request query | text | string-convertible | required | TODO | -| server | get | request query | text | array of string-convertibles | optional | TODO | -| server | get | request query | text | array of string-convertibles | required | TODO | -| server | get | request query | text | date | optional | TODO | -| server | get | request query | text | date | required | TODO | -| server | get | request query | text | array of dates | optional | TODO | -| server | get | request query | text | array of dates | required | TODO | -| server | get | request body | text | string-convertible | optional | TODO | -| server | get | request body | text | string-convertible | required | TODO | -| server | get | request body | text | date | optional | TODO | -| server | get | request body | text | date | required | TODO | -| server | get | request body | JSON | codable | optional | TODO | -| server | get | request body | JSON | codable | required | TODO | -| server | get | request body | binary | data | optional | TODO | -| server | get | request body | binary | data | required | TODO | -| server | set | response body | text | string-convertible | required | TODO | -| server | set | response body | text | date | required | TODO | -| server | set | response body | JSON | codable | required | TODO | -| server | set | response body | binary | data | required | TODO | +| common | set | header field | text | string-convertible | both | setHeaderFieldAsText | +| common | set | header field | text | array of string-convertibles | both | setHeaderFieldAsText | +| common | set | header field | text | date | both | setHeaderFieldAsText | +| common | set | header field | text | array of dates | both | setHeaderFieldAsText | +| common | set | header field | JSON | codable | both | setHeaderFieldAsJSON | +| common | get | header field | text | string-convertible | optional | getOptionalHeaderFieldAsText | +| common | get | header field | text | string-convertible | required | getRequiredHeaderFieldAsText | +| common | get | header field | text | array of string-convertibles | optional | getOptionalHeaderFieldAsText | +| common | get | header field | text | array of string-convertibles | required | getRequiredHeaderFieldAsText | +| common | get | header field | text | date | optional | getOptionalHeaderFieldAsText | +| common | get | header field | text | date | required | getRequiredHeaderFieldAsText | +| common | get | header field | text | array of dates | optional | getOptionalHeaderFieldAsText | +| common | get | header field | text | array of dates | required | getRequiredHeaderFieldAsText | +| common | get | header field | JSON | codable | optional | getOptionalHeaderFieldAsJSON | +| common | get | header field | JSON | codable | required | getRequiredHeaderFieldAsJSON | +| client | set | request path | text | string-convertible | required | renderedRequestPath | +| client | set | request query | text | string-convertible | both | setQueryItemAsText | +| client | set | request query | text | array of string-convertibles | both | setQueryItemAsText | +| client | set | request query | text | date | both | setQueryItemAsText | +| client | set | request query | text | array of dates | both | setQueryItemAsText | +| client | set | request body | text | string-convertible | optional | setOptionalRequestBodyAsText | +| client | set | request body | text | string-convertible | required | setRequiredRequestBodyAsText | +| client | set | request body | text | date | optional | setOptionalRequestBodyAsText | +| client | set | request body | text | date | required | setRequiredRequestBodyAsText | +| client | set | request body | JSON | codable | optional | setOptionalRequestBodyAsJSON | +| client | set | request body | JSON | codable | required | setRequiredRequestBodyAsJSON | +| client | set | request body | binary | data | optional | setOptionalRequestBodyAsBinary | +| client | set | request body | binary | data | required | setRequiredRequestBodyAsBinary | +| client | get | response body | text | string-convertible | required | getResponseBodyAsText | +| client | get | response body | text | date | required | getResponseBodyAsText | +| client | get | response body | JSON | codable | required | getResponseBodyAsJSON | +| client | get | response body | binary | data | required | getResponseBodyAsBinary | +| server | get | request path | text | string-convertible | required | getPathParameterAsText | +| server | get | request query | text | string-convertible | optional | getOptionalQueryItemAsText | +| server | get | request query | text | string-convertible | required | getRequiredQueryItemAsText | +| server | get | request query | text | array of string-convertibles | optional | getOptionalQueryItemAsText | +| server | get | request query | text | array of string-convertibles | required | getRequiredQueryItemAsText | +| server | get | request query | text | date | optional | getOptionalQueryItemAsText | +| server | get | request query | text | date | required | getRequiredQueryItemAsText | +| server | get | request query | text | array of dates | optional | getOptionalQueryItemAsText | +| server | get | request query | text | array of dates | required | getRequiredQueryItemAsText | +| server | get | request body | text | string-convertible | optional | getOptionalRequestBodyAsText | +| server | get | request body | text | string-convertible | required | getRequiredRequestBodyAsText | +| server | get | request body | text | date | optional | getOptionalRequestBodyAsText | +| server | get | request body | text | date | required | getRequiredRequestBodyAsText | +| server | get | request body | JSON | codable | optional | getOptionalRequestBodyAsJSON | +| server | get | request body | JSON | codable | required | getRequiredRequestBodyAsJSON | +| server | get | request body | binary | data | optional | getOptionalRequestBodyAsBinary | +| server | get | request body | binary | data | required | getRequiredRequestBodyAsBinary | +| server | set | response body | text | string-convertible | required | setResponseBodyAsText | +| server | set | response body | text | date | required | setResponseBodyAsText | +| server | set | response body | JSON | codable | required | setResponseBodyAsJSON | +| server | set | response body | binary | data | required | setResponseBodyAsBinary | diff --git a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift index 534a463d..045f1748 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift @@ -30,14 +30,16 @@ final class Test_YamsParser: Test_Core { .parseOpenAPI( .init( absolutePath: URL(fileURLWithPath: "/foo.yaml"), - contents: """ + contents: Data( + """ openapi: "\(openAPIVersionString)" info: title: "Test" version: "1.0.0" paths: {} """ - .data(using: .utf8)! + .utf8 + ) ), config: .init(mode: .types), diagnostics: PrintingDiagnosticCollector() diff --git a/Tests/OpenAPIGeneratorReferenceTests/ReferenceTest.swift b/Tests/OpenAPIGeneratorReferenceTests/ReferenceTest.swift index d5d17f91..9927dd08 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/ReferenceTest.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/ReferenceTest.swift @@ -274,7 +274,7 @@ extension ReferenceTests { \(process.executableURL!.path) \(process.arguments!.joined(separator: " ")) """ ) - return try XCTUnwrap(String(data: pipeData, encoding: .utf8)) + return String(decoding: pipeData, as: UTF8.self) } func heading(_ message: String, paddingCharacter: Character, lineLength: Int) -> String { diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml b/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml index d3538ae8..c874d1bd 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml @@ -174,6 +174,12 @@ paths: application/json: schema: type: string + '500': + description: Server error + content: + text/plain: + schema: + type: string components: headers: TracingHeader: diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift index 9c01fe4c..ff48c27d 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift @@ -43,18 +43,35 @@ public struct Client: APIProtocol { input: input, forOperation: Operations.listPets.id, serializer: { input in - var request: OpenAPIRuntime.Request = .init(path: "/pets", method: .get) + let path = try converter.renderedRequestPath(template: "/pets", parameters: []) + var request: OpenAPIRuntime.Request = .init(path: path, method: .get) suppressMutabilityWarning(&request) - try converter.queryAdd(in: &request, name: "limit", value: input.query.limit) - try converter.queryAdd(in: &request, name: "habitat", value: input.query.habitat) - try converter.queryAdd(in: &request, name: "feeds", value: input.query.feeds) - try converter.headerFieldAdd( + 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.queryAdd(in: &request, name: "since", value: input.query.since) - try converter.headerFieldAdd( + try converter.setQueryItemAsText( + in: &request, + name: "since", + value: input.query.since + ) + try converter.setHeaderFieldAsText( in: &request.headerFields, name: "accept", value: "application/json" @@ -65,12 +82,12 @@ public struct Client: APIProtocol { switch response.statusCode { case 200: let headers: Operations.listPets.Output.Ok.Headers = .init( - My_Response_UUID: try converter.headerFieldGetRequired( + My_Response_UUID: try converter.getRequiredHeaderFieldAsText( in: response.headerFields, name: "My-Response-UUID", as: Swift.String.self ), - My_Tracing_Header: try converter.headerFieldGetOptional( + My_Tracing_Header: try converter.getOptionalHeaderFieldAsText( in: response.headerFields, name: "My-Tracing-Header", as: Components.Headers.TracingHeader.self @@ -80,11 +97,12 @@ public struct Client: APIProtocol { in: response.headerFields, substring: "application/json" ) - let body: Operations.listPets.Output.Ok.Body = try converter.bodyGet( - Components.Schemas.Pets.self, - from: response.body, - transforming: { value in .json(value) } - ) + let body: Operations.listPets.Output.Ok.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.Pets.self, + from: response.body, + transforming: { value in .json(value) } + ) return .ok(.init(headers: headers, body: body)) default: let headers: Operations.listPets.Output.Default.Headers = .init() @@ -92,11 +110,12 @@ public struct Client: APIProtocol { in: response.headerFields, substring: "application/json" ) - let body: Operations.listPets.Output.Default.Body = try converter.bodyGet( - Components.Schemas._Error.self, - from: response.body, - transforming: { value in .json(value) } - ) + let body: Operations.listPets.Output.Default.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas._Error.self, + from: response.body, + transforming: { value in .json(value) } + ) return .`default`( statusCode: response.statusCode, .init(headers: headers, body: body) @@ -115,19 +134,20 @@ public struct Client: APIProtocol { input: input, forOperation: Operations.createPet.id, serializer: { input in - var request: OpenAPIRuntime.Request = .init(path: "/pets", method: .post) + let path = try converter.renderedRequestPath(template: "/pets", parameters: []) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) suppressMutabilityWarning(&request) - try converter.headerFieldAdd( + try converter.setHeaderFieldAsJSON( in: &request.headerFields, name: "X-Extra-Arguments", value: input.headers.X_Extra_Arguments ) - try converter.headerFieldAdd( + try converter.setHeaderFieldAsText( in: &request.headerFields, name: "accept", value: "application/json" ) - request.body = try converter.bodyAddRequired( + request.body = try converter.setRequiredRequestBodyAsJSON( input.body, headerFields: &request.headerFields, transforming: { wrapped in @@ -146,7 +166,7 @@ public struct Client: APIProtocol { switch response.statusCode { case 201: let headers: Operations.createPet.Output.Created.Headers = .init( - X_Extra_Arguments: try converter.headerFieldGetOptional( + X_Extra_Arguments: try converter.getOptionalHeaderFieldAsJSON( in: response.headerFields, name: "X-Extra-Arguments", as: Components.Schemas.CodeError.self @@ -156,15 +176,16 @@ public struct Client: APIProtocol { in: response.headerFields, substring: "application/json" ) - let body: Operations.createPet.Output.Created.Body = try converter.bodyGet( - Components.Schemas.Pet.self, - from: response.body, - transforming: { value in .json(value) } - ) + let body: Operations.createPet.Output.Created.Body = + try converter.getResponseBodyAsJSON( + Components.Schemas.Pet.self, + from: response.body, + transforming: { value in .json(value) } + ) return .created(.init(headers: headers, body: body)) case 400: let headers: Components.Responses.ErrorBadRequest.Headers = .init( - X_Reason: try converter.headerFieldGetOptional( + X_Reason: try converter.getOptionalHeaderFieldAsText( in: response.headerFields, name: "X-Reason", as: Swift.String.self @@ -174,11 +195,12 @@ public struct Client: APIProtocol { in: response.headerFields, substring: "application/json" ) - let body: Components.Responses.ErrorBadRequest.Body = try converter.bodyGet( - Components.Responses.ErrorBadRequest.Body.jsonPayload.self, - from: response.body, - transforming: { value in .json(value) } - ) + let body: Components.Responses.ErrorBadRequest.Body = + try converter.getResponseBodyAsJSON( + Components.Responses.ErrorBadRequest.Body.jsonPayload.self, + from: response.body, + transforming: { value in .json(value) } + ) return .badRequest(.init(headers: headers, body: body)) default: return .undocumented(statusCode: response.statusCode, .init()) } @@ -193,7 +215,8 @@ public struct Client: APIProtocol { input: input, forOperation: Operations.probe.id, serializer: { input in - var request: OpenAPIRuntime.Request = .init(path: "/probe", method: .post) + let path = try converter.renderedRequestPath(template: "/probe", parameters: []) + var request: OpenAPIRuntime.Request = .init(path: path, method: .post) suppressMutabilityWarning(&request) return request }, @@ -217,17 +240,18 @@ public struct Client: APIProtocol { input: input, forOperation: Operations.updatePet.id, serializer: { input in - var request: OpenAPIRuntime.Request = .init( - path: "/pets/\(input.path.petId)", - method: .patch + let path = try converter.renderedRequestPath( + template: "/pets/{}", + parameters: [input.path.petId] ) + var request: OpenAPIRuntime.Request = .init(path: path, method: .patch) suppressMutabilityWarning(&request) - try converter.headerFieldAdd( + try converter.setHeaderFieldAsText( in: &request.headerFields, name: "accept", value: "application/json" ) - request.body = try converter.bodyAddOptional( + request.body = try converter.setOptionalRequestBodyAsJSON( input.body, headerFields: &request.headerFields, transforming: { wrapped in @@ -253,11 +277,12 @@ public struct Client: APIProtocol { in: response.headerFields, substring: "application/json" ) - let body: Operations.updatePet.Output.BadRequest.Body = try converter.bodyGet( - Operations.updatePet.Output.BadRequest.Body.jsonPayload.self, - from: response.body, - transforming: { value in .json(value) } - ) + let body: Operations.updatePet.Output.BadRequest.Body = + try converter.getResponseBodyAsJSON( + Operations.updatePet.Output.BadRequest.Body.jsonPayload.self, + from: response.body, + transforming: { value in .json(value) } + ) return .badRequest(.init(headers: headers, body: body)) default: return .undocumented(statusCode: response.statusCode, .init()) } @@ -274,17 +299,18 @@ public struct Client: APIProtocol { input: input, forOperation: Operations.uploadAvatarForPet.id, serializer: { input in - var request: OpenAPIRuntime.Request = .init( - path: "/pets/\(input.path.petId)/avatar", - method: .put + 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.headerFieldAdd( + try converter.setHeaderFieldAsText( in: &request.headerFields, name: "accept", - value: "application/octet-stream, application/json" + value: "application/octet-stream, application/json, text/plain" ) - request.body = try converter.bodyAddRequired( + request.body = try converter.setRequiredRequestBodyAsBinary( input.body, headerFields: &request.headerFields, transforming: { wrapped in @@ -304,11 +330,12 @@ public struct Client: APIProtocol { in: response.headerFields, substring: "application/octet-stream" ) - let body: Operations.uploadAvatarForPet.Output.Ok.Body = try converter.bodyGet( - Foundation.Data.self, - from: response.body, - transforming: { value in .binary(value) } - ) + let body: Operations.uploadAvatarForPet.Output.Ok.Body = + try converter.getResponseBodyAsBinary( + Foundation.Data.self, + from: response.body, + transforming: { value in .binary(value) } + ) return .ok(.init(headers: headers, body: body)) case 412: let headers: Operations.uploadAvatarForPet.Output.PreconditionFailed.Headers = @@ -318,12 +345,26 @@ public struct Client: APIProtocol { substring: "application/json" ) let body: Operations.uploadAvatarForPet.Output.PreconditionFailed.Body = - try converter.bodyGet( + try converter.getResponseBodyAsJSON( Swift.String.self, from: response.body, transforming: { value in .json(value) } ) 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 body: Operations.uploadAvatarForPet.Output.InternalServerError.Body = + try converter.getResponseBodyAsText( + Swift.String.self, + from: response.body, + transforming: { value in .text(value) } + ) + 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 8d67a26e..d1edcd47 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift @@ -69,29 +69,29 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { using: APIHandler.listPets, deserializer: { request, metadata in let path: Operations.listPets.Input.Path = .init() let query: Operations.listPets.Input.Query = .init( - limit: try converter.queryGetOptional( + limit: try converter.getOptionalQueryItemAsText( in: metadata.queryParameters, name: "limit", as: Swift.Int32.self ), - habitat: try converter.queryGetOptional( + habitat: try converter.getOptionalQueryItemAsText( in: metadata.queryParameters, name: "habitat", as: Operations.listPets.Input.Query.habitatPayload.self ), - feeds: try converter.queryGetOptional( + feeds: try converter.getOptionalQueryItemAsText( in: metadata.queryParameters, name: "feeds", as: Operations.listPets.Input.Query.feedsPayload.self ), - since: try converter.queryGetOptional( + 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.headerFieldGetOptional( + My_Request_UUID: try converter.getOptionalHeaderFieldAsText( in: request.headerFields, name: "My-Request-UUID", as: Swift.String.self @@ -113,12 +113,12 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { suppressUnusedWarning(value) var response: Response = .init(statusCode: 200) suppressMutabilityWarning(&response) - try converter.headerFieldAdd( + try converter.setHeaderFieldAsText( in: &response.headerFields, name: "My-Response-UUID", value: value.headers.My_Response_UUID ) - try converter.headerFieldAdd( + try converter.setHeaderFieldAsText( in: &response.headerFields, name: "My-Tracing-Header", value: value.headers.My_Tracing_Header @@ -127,7 +127,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { "application/json", in: request.headerFields ) - response.body = try converter.bodyAdd( + response.body = try converter.setResponseBodyAsJSON( value.body, headerFields: &response.headerFields, transforming: { wrapped in @@ -149,7 +149,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { "application/json", in: request.headerFields ) - response.body = try converter.bodyAdd( + response.body = try converter.setResponseBodyAsJSON( value.body, headerFields: &response.headerFields, transforming: { wrapped in @@ -179,18 +179,19 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { 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.headerFieldGetOptional( + 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 body: Operations.createPet.Input.Body = try converter.bodyGetRequired( - Components.Schemas.CreatePetRequest.self, - from: request.body, - transforming: { value in .json(value) } - ) + let body: Operations.createPet.Input.Body = + try converter.getRequiredRequestBodyAsJSON( + Components.Schemas.CreatePetRequest.self, + from: request.body, + transforming: { value in .json(value) } + ) return Operations.createPet.Input( path: path, query: query, @@ -205,7 +206,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { suppressUnusedWarning(value) var response: Response = .init(statusCode: 201) suppressMutabilityWarning(&response) - try converter.headerFieldAdd( + try converter.setHeaderFieldAsJSON( in: &response.headerFields, name: "X-Extra-Arguments", value: value.headers.X_Extra_Arguments @@ -214,7 +215,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { "application/json", in: request.headerFields ) - response.body = try converter.bodyAdd( + response.body = try converter.setResponseBodyAsJSON( value.body, headerFields: &response.headerFields, transforming: { wrapped in @@ -232,7 +233,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { suppressUnusedWarning(value) var response: Response = .init(statusCode: 400) suppressMutabilityWarning(&response) - try converter.headerFieldAdd( + try converter.setHeaderFieldAsText( in: &response.headerFields, name: "X-Reason", value: value.headers.X_Reason @@ -241,7 +242,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { "application/json", in: request.headerFields ) - response.body = try converter.bodyAdd( + response.body = try converter.setResponseBodyAsJSON( value.body, headerFields: &response.headerFields, transforming: { wrapped in @@ -305,7 +306,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { using: APIHandler.updatePet, deserializer: { request, metadata in let path: Operations.updatePet.Input.Path = .init( - petId: try converter.pathGetRequired( + petId: try converter.getPathParameterAsText( in: metadata.pathParameters, name: "petId", as: Swift.Int64.self @@ -315,7 +316,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { let headers: Operations.updatePet.Input.Headers = .init() let cookies: Operations.updatePet.Input.Cookies = .init() let body: Components.RequestBodies.UpdatePetRequest? = - try converter.bodyGetOptional( + try converter.getOptionalRequestBodyAsJSON( Components.RequestBodies.UpdatePetRequest.jsonPayload.self, from: request.body, transforming: { value in .json(value) } @@ -343,7 +344,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { "application/json", in: request.headerFields ) - response.body = try converter.bodyAdd( + response.body = try converter.setResponseBodyAsJSON( value.body, headerFields: &response.headerFields, transforming: { wrapped in @@ -375,7 +376,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { using: APIHandler.uploadAvatarForPet, deserializer: { request, metadata in let path: Operations.uploadAvatarForPet.Input.Path = .init( - petId: try converter.pathGetRequired( + petId: try converter.getPathParameterAsText( in: metadata.pathParameters, name: "petId", as: Components.Parameters.path_petId.self @@ -384,11 +385,12 @@ 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.bodyGetRequired( - Foundation.Data.self, - from: request.body, - transforming: { value in .binary(value) } - ) + let body: Operations.uploadAvatarForPet.Input.Body = + try converter.getRequiredRequestBodyAsBinary( + Foundation.Data.self, + from: request.body, + transforming: { value in .binary(value) } + ) return Operations.uploadAvatarForPet.Input( path: path, query: query, @@ -407,7 +409,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { "application/octet-stream", in: request.headerFields ) - response.body = try converter.bodyAdd( + response.body = try converter.setResponseBodyAsBinary( value.body, headerFields: &response.headerFields, transforming: { wrapped in @@ -426,7 +428,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { "application/json", in: request.headerFields ) - response.body = try converter.bodyAdd( + response.body = try converter.setResponseBodyAsJSON( value.body, headerFields: &response.headerFields, transforming: { wrapped in @@ -440,6 +442,22 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { } ) return response + case let .internalServerError(value): + suppressUnusedWarning(value) + var response: Response = .init(statusCode: 500) + suppressMutabilityWarning(&response) + try converter.validateAcceptIfPresent("text/plain", in: request.headerFields) + response.body = try converter.setResponseBodyAsText( + value.body, + headerFields: &response.headerFields, + transforming: { wrapped in + switch wrapped { + case let .text(value): + return .init(value: value, contentType: "text/plain") + } + } + ) + return response case let .undocumented(statusCode, _): return .init(statusCode: statusCode) } } diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift index 998b13a2..87b13e6f 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift @@ -1323,6 +1323,36 @@ public enum Operations { /// /// 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 + 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. diff --git a/Tests/PetstoreConsumerTests/Assertions.swift b/Tests/PetstoreConsumerTests/Assertions.swift index 0f283223..234eb603 100644 --- a/Tests/PetstoreConsumerTests/Assertions.swift +++ b/Tests/PetstoreConsumerTests/Assertions.swift @@ -22,10 +22,7 @@ public func XCTAssertEqualStringifiedData( line: UInt = #line ) { do { - guard let actualString = String(data: try expression1(), encoding: .utf8) else { - XCTFail("Data is not a valid UTF-8 string", file: file, line: line) - return - } + let actualString = String(decoding: try expression1(), as: UTF8.self) XCTAssertEqual(actualString, try expression2(), file: file, line: line) } catch { XCTFail(error.localizedDescription, file: file, line: line) diff --git a/Tests/PetstoreConsumerTests/Common.swift b/Tests/PetstoreConsumerTests/Common.swift index b13007c4..d955d643 100644 --- a/Tests/PetstoreConsumerTests/Common.swift +++ b/Tests/PetstoreConsumerTests/Common.swift @@ -17,7 +17,6 @@ import Foundation enum TestError: Swift.Error, LocalizedError, CustomStringConvertible { case noHandlerFound(method: HTTPMethod, path: [RouterPathComponent]) case invalidURLString(String) - case invalidJSONBody(String) case unexpectedValue(Any) case unexpectedMissingRequestBody @@ -27,8 +26,6 @@ enum TestError: Swift.Error, LocalizedError, CustomStringConvertible { return "No handler found for method \(method.name) and path \(path.stringPath)" case .invalidURLString(let string): return "Invalid URL string: \(string)" - case .invalidJSONBody(let body): - return "Invalid JSON body: \(body)" case .unexpectedValue(let value): return "Unexpected value: \(value)" case .unexpectedMissingRequestBody: @@ -66,7 +63,7 @@ extension Response { self.init( statusCode: statusCode, headerFields: headers, - body: encodedBody.data(using: .utf8)! + body: Data(encodedBody.utf8) ) } @@ -96,7 +93,7 @@ extension Operations.listPets.Output { extension Data { var pretty: String { - String(data: self, encoding: .utf8) ?? String(data: self, encoding: .ascii) ?? String(describing: self) + String(decoding: self, as: UTF8.self) } static var abcdString: String { @@ -104,7 +101,7 @@ extension Data { } static var abcd: Data { - abcdString.data(using: .utf8)! + Data(abcdString.utf8) } static var efghString: String { @@ -116,7 +113,7 @@ extension Data { } static var efgh: Data { - efghString.data(using: .utf8)! + Data(efghString.utf8) } } @@ -128,9 +125,7 @@ extension Request { headerFields: [HeaderField] = [], encodedBody: String ) throws { - guard let body = encodedBody.data(using: .utf8) else { - throw TestError.invalidJSONBody(encodedBody) - } + let body = Data(encodedBody.utf8) self.init( path: path, query: query, diff --git a/Tests/PetstoreConsumerTests/Test_Client.swift b/Tests/PetstoreConsumerTests/Test_Client.swift index 4f26831e..c975a14a 100644 --- a/Tests/PetstoreConsumerTests/Test_Client.swift +++ b/Tests/PetstoreConsumerTests/Test_Client.swift @@ -381,7 +381,7 @@ final class Test_Client: XCTestCase { XCTAssertEqual( request.headerFields, [ - .init(name: "accept", value: "application/octet-stream, application/json"), + .init(name: "accept", value: "application/octet-stream, application/json, text/plain"), .init(name: "content-type", value: "application/octet-stream"), ] ) @@ -420,7 +420,7 @@ final class Test_Client: XCTestCase { XCTAssertEqual( request.headerFields, [ - .init(name: "accept", value: "application/octet-stream, application/json"), + .init(name: "accept", value: "application/octet-stream, application/json, text/plain"), .init(name: "content-type", value: "application/octet-stream"), ] ) @@ -448,4 +448,30 @@ final class Test_Client: XCTestCase { XCTAssertEqual(json, Data.efghString) } } + + func testUploadAvatarForPet_500() async throws { + transport = .init { request, baseURL, operationID in + return .init( + statusCode: 500, + headers: [ + .init(name: "content-type", value: "text/plain") + ], + encodedBody: Data.efghString + ) + } + let response = try await client.uploadAvatarForPet( + .init( + path: .init(petId: 1), + body: .binary(.abcd) + ) + ) + guard case let .internalServerError(value) = response else { + XCTFail("Unexpected response: \(response)") + return + } + switch value.body { + case .text(let text): + XCTAssertEqual(text, Data.efghString) + } + } } diff --git a/Tests/PetstoreConsumerTests/Test_Server.swift b/Tests/PetstoreConsumerTests/Test_Server.swift index 24603843..5ad99757 100644 --- a/Tests/PetstoreConsumerTests/Test_Server.swift +++ b/Tests/PetstoreConsumerTests/Test_Server.swift @@ -77,7 +77,7 @@ final class Test_Server: XCTestCase { .init(name: "content-type", value: "application/json; charset=utf-8"), ] ) - let bodyString = try XCTUnwrap(String(data: response.body, encoding: .utf8)) + let bodyString = String(decoding: response.body, as: UTF8.self) XCTAssertEqual( bodyString, #""" @@ -114,7 +114,7 @@ final class Test_Server: XCTestCase { .init(name: "content-type", value: "application/json; charset=utf-8") ] ) - let bodyString = try XCTUnwrap(String(data: response.body, encoding: .utf8)) + let bodyString = String(decoding: response.body, as: UTF8.self) XCTAssertEqual( bodyString, #""" @@ -170,7 +170,7 @@ final class Test_Server: XCTestCase { .init(name: "content-type", value: "application/json; charset=utf-8"), ] ) - let bodyString = try XCTUnwrap(String(data: response.body, encoding: .utf8)) + let bodyString = String(decoding: response.body, as: UTF8.self) XCTAssertEqual( bodyString, #""" @@ -220,7 +220,7 @@ final class Test_Server: XCTestCase { .init(name: "content-type", value: "application/json; charset=utf-8"), ] ) - let bodyString = try XCTUnwrap(String(data: response.body, encoding: .utf8)) + let bodyString = String(decoding: response.body, as: UTF8.self) XCTAssertEqual( bodyString, #""" @@ -379,7 +379,7 @@ final class Test_Server: XCTestCase { path: "/api/pets/1/avatar", method: .put, headerFields: [ - .init(name: "accept", value: "application/octet-stream, application/json"), + .init(name: "accept", value: "application/octet-stream, application/json, text/plain"), .init(name: "content-type", value: "application/octet-stream"), ], encodedBody: Data.abcdString @@ -403,7 +403,7 @@ final class Test_Server: XCTestCase { ) } - func testUploadAvatarForPet_201() async throws { + func testUploadAvatarForPet_412() async throws { client = .init( uploadAvatarForPetBlock: { input in guard case let .binary(avatar) = input.body else { @@ -418,7 +418,7 @@ final class Test_Server: XCTestCase { path: "/api/pets/1/avatar", method: .put, headerFields: [ - .init(name: "accept", value: "application/octet-stream, application/json"), + .init(name: "accept", value: "application/octet-stream, application/json, text/plain"), .init(name: "content-type", value: "application/octet-stream"), ], encodedBody: Data.abcdString @@ -441,4 +441,44 @@ final class Test_Server: XCTestCase { Data.quotedEfghString ) } + + func testUploadAvatarForPet_500() async throws { + client = .init( + uploadAvatarForPetBlock: { input in + guard case let .binary(avatar) = input.body else { + throw TestError.unexpectedValue(input.body) + } + XCTAssertEqualStringifiedData(avatar, Data.abcdString) + return .internalServerError(.init(body: .text(Data.efghString))) + } + ) + let response = try await server.uploadAvatarForPet( + .init( + path: "/api/pets/1/avatar", + method: .put, + headerFields: [ + .init(name: "accept", value: "application/octet-stream, application/json, text/plain"), + .init(name: "content-type", value: "application/octet-stream"), + ], + encodedBody: Data.abcdString + ), + .init( + pathParameters: [ + "petId": "1" + ] + ) + ) + XCTAssertEqual(response.statusCode, 500) + XCTAssertEqual( + response.headerFields, + [ + .init(name: "content-type", value: "text/plain") + ] + ) + XCTAssertEqualStringifiedData( + response.body, + Data.efghString + ) + } + } diff --git a/Tests/PetstoreConsumerTests/Test_Types.swift b/Tests/PetstoreConsumerTests/Test_Types.swift index 064431f0..9bf49520 100644 --- a/Tests/PetstoreConsumerTests/Test_Types.swift +++ b/Tests/PetstoreConsumerTests/Test_Types.swift @@ -69,7 +69,7 @@ final class Test_Types: XCTestCase { XCTAssertThrowsError( try testDecoder.decode( Components.Schemas.NoAdditionalProperties.self, - from: #"{"foo":"hi","hello":1}"#.data(using: .utf8)! + from: Data(#"{"foo":"hi","hello":1}"#.utf8) ) ) } @@ -127,19 +127,19 @@ final class Test_Types: XCTestCase { XCTAssertThrowsError( try testDecoder.decode( Components.Schemas.AllOfObjects.self, - from: #"{}"#.data(using: .utf8)! + from: Data(#"{}"#.utf8) ) ) XCTAssertThrowsError( try testDecoder.decode( Components.Schemas.AllOfObjects.self, - from: #"{"message":"hi"}"#.data(using: .utf8)! + from: Data(#"{"message":"hi"}"#.utf8) ) ) XCTAssertThrowsError( try testDecoder.decode( Components.Schemas.AllOfObjects.self, - from: #"{"code":1}"#.data(using: .utf8)! + from: Data(#"{"code":1}"#.utf8) ) ) } @@ -169,7 +169,7 @@ final class Test_Types: XCTestCase { XCTAssertThrowsError( try testDecoder.decode( Components.Schemas.AnyOfObjects.self, - from: #"{}"#.data(using: .utf8)! + from: Data(#"{}"#.utf8) ) ) } @@ -225,7 +225,7 @@ final class Test_Types: XCTestCase { XCTAssertThrowsError( try testDecoder.decode( Components.Schemas.OneOfObjectsWithDiscriminator.self, - from: #"{}"#.data(using: .utf8)! + from: Data(#"{}"#.utf8) ) ) }