From 40adcf9498153fd6a2864672c44337649bb35e8d Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 31 May 2023 10:23:46 +0200 Subject: [PATCH 01/11] [Generator] Choose the serialization method based on content type --- .../translateClientMethod.swift | 1 + .../Translator/CommonTypes/Constants.swift | 13 +++++ .../Translator/Content/CodingStrategy.swift | 38 +++++++++++++ .../Translator/Content/ContentType.swift | 12 +++++ .../Parameters/TypedParameter.swift | 9 +++- .../Parameters/translateParameter.swift | 41 +++++++++----- .../RequestBody/translateRequestBody.swift | 22 ++++++-- .../Responses/TypedResponseHeader.swift | 9 +++- .../Responses/translateResponseHeader.swift | 8 +++ .../Responses/translateResponseOutcome.swift | 15 +++++- .../Resources/Docs/petstore.yaml | 4 ++ .../ReferenceSources/Petstore/Client.swift | 46 ++++++++++++++-- .../ReferenceSources/Petstore/Server.swift | 53 ++++++++++++++++--- .../ReferenceSources/Petstore/Types.swift | 30 +++++++++++ Tests/PetstoreConsumerTests/Test_Client.swift | 30 ++++++++++- Tests/PetstoreConsumerTests/Test_Server.swift | 46 ++++++++++++++-- 16 files changed, 342 insertions(+), 35 deletions(-) create mode 100644 Sources/_OpenAPIGeneratorCore/Translator/Content/CodingStrategy.swift diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift index bf3e0825..91e8c842 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift @@ -68,6 +68,7 @@ extension ClientFileTranslator { .identifier("converter").dot("headerFieldAdd") .call([ .init(label: "in", expression: .inOut(.identifier("request").dot("headerFields"))), + .init(label: "strategy", expression: .dot(Constants.CodingStrategy.string)), .init(label: "name", expression: "accept"), .init(label: "value", expression: .literal(acceptValue)), ]) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index 43b17be2..8fd345ea 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 { + + /// Matches `OpenAPIRuntime.CodingStrategy.deferredToType`. + static let deferredToType: String = "deferredToType" + + /// Matches `OpenAPIRuntime.CodingStrategy.string`. + static let string: String = "string" + + /// Matches `OpenAPIRuntime.CodingStrategy.codable`. + static let codable: String = "codable" + } + /// 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..aa8f1acb --- /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 codable + + /// A strategy using LosslessStringConvertible. + case string + + /// A strategy for letting the type choose the appropriate option. + case deferredToType + + /// The name of the coding strategy in the runtime library. + var runtimeName: String { + switch self { + case .codable: + return Constants.CodingStrategy.codable + case .string: + return Constants.CodingStrategy.string + default: + return Constants.CodingStrategy.deferredToType + } + } +} diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift index 9f197145..f92d2d95 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 .codable + case .text: + return .string + default: + return .deferredToType + } + } + /// A Boolean value that indicates whether the content type /// is a type of JSON. var isJSON: Bool { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift index 543987e9..ccc022de 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 = .deferredToType // Check supported exploded/style types let location = parameter.location @@ -175,6 +180,7 @@ extension FileTranslator { return nil } schema = typedContent.content.schema ?? .b(.fragment) + codingStrategy = typedContent.content.contentType.codingStrategy } // Check if the underlying schema is supported @@ -207,7 +213,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..06dfc201 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift @@ -113,13 +113,21 @@ extension ClientFileTranslator { ) throws -> Expression? { let methodPrefix: String let containerExpr: Expression + let extraArguments: [FunctionArgumentDescription] switch parameter.location { case .header: methodPrefix = "headerField" containerExpr = .identifier(requestVariableName).dot("headerFields") + extraArguments = [ + .init( + label: "strategy", + expression: .dot(parameter.codingStrategy.runtimeName) + ) + ] case .query: methodPrefix = "query" containerExpr = .identifier(requestVariableName) + extraArguments = [] default: diagnostics.emitUnsupported( "Parameter of type \(parameter.location.rawValue)", @@ -130,19 +138,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) - ), - ]) + .call( + [ + .init( + label: "in", + expression: .inOut(containerExpr) + ) + ] + extraArguments + [ + .init(label: "name", expression: .literal(parameter.name)), + .init( + label: "value", + expression: .identifier(inputVariableName) + .dot(parameter.location.shortVariableName) + .dot(parameter.variableName) + ), + ] + ) ) } } @@ -210,6 +221,10 @@ extension ServerFileTranslator { .dot(methodName) .call([ .init(label: "in", expression: .identifier("request").dot("headerFields")), + .init( + label: "strategy", + expression: .dot(typedParameter.codingStrategy.runtimeName) + ), .init(label: "name", expression: .literal(parameter.name)), .init( label: "as", diff --git a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift index f01c846a..29278a10 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift @@ -183,8 +183,18 @@ 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) + ), + .init( + label: "strategy", + expression: .dot(contentType.codingStrategy.runtimeName) + ), ]) ) let caseDecl: SwitchCaseDescription = .init( @@ -260,7 +270,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( @@ -291,6 +303,10 @@ extension ServerFileTranslator { label: "from", expression: .identifier(requestVariableName).dot("body") ), + .init( + label: "strategy", + expression: .dot(codingStrategyName) + ), .init(label: "transforming", expression: transformExpr), ]) ) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift index edf658a5..c1c7c9e2 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 = .deferredToType case let .b(contentMap): guard let typedContent = try bestSingleTypedContent( @@ -116,6 +121,7 @@ extension FileTranslator { return nil } schema = typedContent.content.schema ?? .b(.fragment) + codingStrategy = typedContent.content.contentType.codingStrategy } // Check if schema is supported @@ -149,7 +155,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..17fe15a5 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift @@ -101,6 +101,10 @@ extension ClientFileTranslator { label: "in", expression: .identifier(responseVariableName).dot("headerFields") ), + .init( + label: "strategy", + expression: .dot(header.codingStrategy.runtimeName) + ), .init(label: "name", expression: .literal(header.name)), .init( label: "as", @@ -140,6 +144,10 @@ extension ServerFileTranslator { .dot("headerFields") ) ), + .init( + label: "strategy", + expression: .dot(header.codingStrategy.runtimeName) + ), .init(label: "name", expression: .literal(header.name)), .init( label: "value", diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift index 0ccf1c22..a3774829 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift @@ -189,6 +189,12 @@ extension ClientFileTranslator { expression: .identifier(contentTypeUsage.fullyQualifiedSwiftName).dot("self") ), .init(label: "from", expression: .identifier("response").dot("body")), + .init( + label: "strategy", + expression: .dot( + typedContent.content.contentType.codingStrategy.runtimeName + ) + ), .init( label: "transforming", expression: transformExpr @@ -329,11 +335,18 @@ 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) ), + .init( + label: "strategy", + expression: .dot(contentType.codingStrategy.runtimeName) + ), ]) ) ) diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml b/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml index d3538ae8..273d68ca 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml @@ -174,6 +174,10 @@ paths: application/json: schema: type: string + '500': + description: Server error + content: + text/plain: {} components: headers: TracingHeader: diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift index 9c01fe4c..806be4f7 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift @@ -50,12 +50,14 @@ public struct Client: APIProtocol { try converter.queryAdd(in: &request, name: "feeds", value: input.query.feeds) try converter.headerFieldAdd( in: &request.headerFields, + strategy: .deferredToType, name: "My-Request-UUID", value: input.headers.My_Request_UUID ) try converter.queryAdd(in: &request, name: "since", value: input.query.since) try converter.headerFieldAdd( in: &request.headerFields, + strategy: .string, name: "accept", value: "application/json" ) @@ -67,11 +69,13 @@ public struct Client: APIProtocol { let headers: Operations.listPets.Output.Ok.Headers = .init( My_Response_UUID: try converter.headerFieldGetRequired( in: response.headerFields, + strategy: .deferredToType, name: "My-Response-UUID", as: Swift.String.self ), My_Tracing_Header: try converter.headerFieldGetOptional( in: response.headerFields, + strategy: .deferredToType, name: "My-Tracing-Header", as: Components.Headers.TracingHeader.self ) @@ -83,6 +87,7 @@ public struct Client: APIProtocol { let body: Operations.listPets.Output.Ok.Body = try converter.bodyGet( Components.Schemas.Pets.self, from: response.body, + strategy: .codable, transforming: { value in .json(value) } ) return .ok(.init(headers: headers, body: body)) @@ -95,6 +100,7 @@ public struct Client: APIProtocol { let body: Operations.listPets.Output.Default.Body = try converter.bodyGet( Components.Schemas._Error.self, from: response.body, + strategy: .codable, transforming: { value in .json(value) } ) return .`default`( @@ -119,11 +125,13 @@ public struct Client: APIProtocol { suppressMutabilityWarning(&request) try converter.headerFieldAdd( in: &request.headerFields, + strategy: .codable, name: "X-Extra-Arguments", value: input.headers.X_Extra_Arguments ) try converter.headerFieldAdd( in: &request.headerFields, + strategy: .string, name: "accept", value: "application/json" ) @@ -135,7 +143,8 @@ public struct Client: APIProtocol { case let .json(value): return .init( value: value, - contentType: "application/json; charset=utf-8" + contentType: "application/json; charset=utf-8", + strategy: .codable ) } } @@ -148,6 +157,7 @@ public struct Client: APIProtocol { let headers: Operations.createPet.Output.Created.Headers = .init( X_Extra_Arguments: try converter.headerFieldGetOptional( in: response.headerFields, + strategy: .codable, name: "X-Extra-Arguments", as: Components.Schemas.CodeError.self ) @@ -159,6 +169,7 @@ public struct Client: APIProtocol { let body: Operations.createPet.Output.Created.Body = try converter.bodyGet( Components.Schemas.Pet.self, from: response.body, + strategy: .codable, transforming: { value in .json(value) } ) return .created(.init(headers: headers, body: body)) @@ -166,6 +177,7 @@ public struct Client: APIProtocol { let headers: Components.Responses.ErrorBadRequest.Headers = .init( X_Reason: try converter.headerFieldGetOptional( in: response.headerFields, + strategy: .deferredToType, name: "X-Reason", as: Swift.String.self ) @@ -177,6 +189,7 @@ public struct Client: APIProtocol { let body: Components.Responses.ErrorBadRequest.Body = try converter.bodyGet( Components.Responses.ErrorBadRequest.Body.jsonPayload.self, from: response.body, + strategy: .codable, transforming: { value in .json(value) } ) return .badRequest(.init(headers: headers, body: body)) @@ -224,6 +237,7 @@ public struct Client: APIProtocol { suppressMutabilityWarning(&request) try converter.headerFieldAdd( in: &request.headerFields, + strategy: .string, name: "accept", value: "application/json" ) @@ -235,7 +249,8 @@ public struct Client: APIProtocol { case let .json(value): return .init( value: value, - contentType: "application/json; charset=utf-8" + contentType: "application/json; charset=utf-8", + strategy: .codable ) } } @@ -256,6 +271,7 @@ public struct Client: APIProtocol { let body: Operations.updatePet.Output.BadRequest.Body = try converter.bodyGet( Operations.updatePet.Output.BadRequest.Body.jsonPayload.self, from: response.body, + strategy: .codable, transforming: { value in .json(value) } ) return .badRequest(.init(headers: headers, body: body)) @@ -281,8 +297,9 @@ public struct Client: APIProtocol { suppressMutabilityWarning(&request) try converter.headerFieldAdd( in: &request.headerFields, + strategy: .string, name: "accept", - value: "application/octet-stream, application/json" + value: "application/octet-stream, application/json, text/plain" ) request.body = try converter.bodyAddRequired( input.body, @@ -290,7 +307,11 @@ public struct Client: APIProtocol { transforming: { wrapped in switch wrapped { case let .binary(value): - return .init(value: value, contentType: "application/octet-stream") + return .init( + value: value, + contentType: "application/octet-stream", + strategy: .deferredToType + ) } } ) @@ -307,6 +328,7 @@ public struct Client: APIProtocol { let body: Operations.uploadAvatarForPet.Output.Ok.Body = try converter.bodyGet( Foundation.Data.self, from: response.body, + strategy: .deferredToType, transforming: { value in .binary(value) } ) return .ok(.init(headers: headers, body: body)) @@ -321,9 +343,25 @@ public struct Client: APIProtocol { try converter.bodyGet( Swift.String.self, from: response.body, + strategy: .codable, 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.bodyGet( + Swift.String.self, + from: response.body, + strategy: .string, + 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..e6524a20 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift @@ -93,6 +93,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { let headers: Operations.listPets.Input.Headers = .init( My_Request_UUID: try converter.headerFieldGetOptional( in: request.headerFields, + strategy: .deferredToType, name: "My-Request-UUID", as: Swift.String.self ) @@ -115,11 +116,13 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { suppressMutabilityWarning(&response) try converter.headerFieldAdd( in: &response.headerFields, + strategy: .deferredToType, name: "My-Response-UUID", value: value.headers.My_Response_UUID ) try converter.headerFieldAdd( in: &response.headerFields, + strategy: .deferredToType, name: "My-Tracing-Header", value: value.headers.My_Tracing_Header ) @@ -135,7 +138,8 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { case let .json(value): return .init( value: value, - contentType: "application/json; charset=utf-8" + contentType: "application/json; charset=utf-8", + strategy: .codable ) } } @@ -157,7 +161,8 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { case let .json(value): return .init( value: value, - contentType: "application/json; charset=utf-8" + contentType: "application/json; charset=utf-8", + strategy: .codable ) } } @@ -181,6 +186,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { let headers: Operations.createPet.Input.Headers = .init( X_Extra_Arguments: try converter.headerFieldGetOptional( in: request.headerFields, + strategy: .codable, name: "X-Extra-Arguments", as: Components.Schemas.CodeError.self ) @@ -189,6 +195,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { let body: Operations.createPet.Input.Body = try converter.bodyGetRequired( Components.Schemas.CreatePetRequest.self, from: request.body, + strategy: .codable, transforming: { value in .json(value) } ) return Operations.createPet.Input( @@ -207,6 +214,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { suppressMutabilityWarning(&response) try converter.headerFieldAdd( in: &response.headerFields, + strategy: .codable, name: "X-Extra-Arguments", value: value.headers.X_Extra_Arguments ) @@ -222,7 +230,8 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { case let .json(value): return .init( value: value, - contentType: "application/json; charset=utf-8" + contentType: "application/json; charset=utf-8", + strategy: .codable ) } } @@ -234,6 +243,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { suppressMutabilityWarning(&response) try converter.headerFieldAdd( in: &response.headerFields, + strategy: .deferredToType, name: "X-Reason", value: value.headers.X_Reason ) @@ -249,7 +259,8 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { case let .json(value): return .init( value: value, - contentType: "application/json; charset=utf-8" + contentType: "application/json; charset=utf-8", + strategy: .codable ) } } @@ -318,6 +329,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { try converter.bodyGetOptional( Components.RequestBodies.UpdatePetRequest.jsonPayload.self, from: request.body, + strategy: .codable, transforming: { value in .json(value) } ) return Operations.updatePet.Input( @@ -351,7 +363,8 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { case let .json(value): return .init( value: value, - contentType: "application/json; charset=utf-8" + contentType: "application/json; charset=utf-8", + strategy: .codable ) } } @@ -387,6 +400,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { let body: Operations.uploadAvatarForPet.Input.Body = try converter.bodyGetRequired( Foundation.Data.self, from: request.body, + strategy: .deferredToType, transforming: { value in .binary(value) } ) return Operations.uploadAvatarForPet.Input( @@ -413,7 +427,11 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { transforming: { wrapped in switch wrapped { case let .binary(value): - return .init(value: value, contentType: "application/octet-stream") + return .init( + value: value, + contentType: "application/octet-stream", + strategy: .deferredToType + ) } } ) @@ -434,7 +452,28 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { case let .json(value): return .init( value: value, - contentType: "application/json; charset=utf-8" + contentType: "application/json; charset=utf-8", + strategy: .codable + ) + } + } + ) + 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.bodyAdd( + value.body, + headerFields: &response.headerFields, + transforming: { wrapped in + switch wrapped { + case let .text(value): + return .init( + value: value, + contentType: "text/plain", + strategy: .string ) } } 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/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..05f5e04f 100644 --- a/Tests/PetstoreConsumerTests/Test_Server.swift +++ b/Tests/PetstoreConsumerTests/Test_Server.swift @@ -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 + ) + } + } From 53df9f643ff095b3cdbfcbfaf2285070654191a3 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Sat, 3 Jun 2023 09:50:06 +0200 Subject: [PATCH 02/11] PR feedback - split the coding strategy into Parameter and Body --- .../translateClientMethod.swift | 10 ++++-- .../Translator/CommonTypes/Constants.swift | 27 ++++++++++---- .../Translator/Content/CodingStrategy.swift | 35 ++++++++++++++++--- .../Translator/Content/ContentType.swift | 14 +++++++- .../Parameters/TypedParameter.swift | 9 +++-- .../RequestBody/translateRequestBody.swift | 4 +-- .../Responses/TypedResponseHeader.swift | 9 +++-- .../Responses/translateResponseOutcome.swift | 4 +-- .../ReferenceSources/Petstore/Client.swift | 4 +-- .../ReferenceSources/Petstore/Server.swift | 4 +-- 10 files changed, 92 insertions(+), 28 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift index 91e8c842..151e18ea 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift @@ -67,8 +67,14 @@ extension ClientFileTranslator { let addAcceptHeaderExpr: Expression = .try( .identifier("converter").dot("headerFieldAdd") .call([ - .init(label: "in", expression: .inOut(.identifier("request").dot("headerFields"))), - .init(label: "strategy", expression: .dot(Constants.CodingStrategy.string)), + .init( + label: "in", + expression: .inOut(.identifier("request").dot("headerFields")) + ), + .init( + label: "strategy", + expression: .dot(Constants.CodingStrategy.Parameter.string) + ), .init(label: "name", expression: "accept"), .init(label: "value", expression: .literal(acceptValue)), ]) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index 8fd345ea..1eef61d2 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -338,15 +338,30 @@ enum Constants { /// Constants related to the coding strategy. enum CodingStrategy { + + /// Constants related to ParameterCodingStrategy + enum Parameter { + /// Matches `OpenAPIRuntime.CodingStrategy.string`. + static let string: String = "string" - /// Matches `OpenAPIRuntime.CodingStrategy.deferredToType`. - static let deferredToType: String = "deferredToType" + /// Matches `OpenAPIRuntime.CodingStrategy.codable`. + static let codable: String = "codable" - /// Matches `OpenAPIRuntime.CodingStrategy.string`. - static let string: String = "string" + /// Matches `OpenAPIRuntime.CodingStrategy.deferredToType`. + static let deferredToType: String = "deferredToType" + } + + /// Constants related to BodyCodingStrategy + enum Body { + /// Matches `OpenAPIRuntime.CodingStrategy.string`. + static let string: String = "string" - /// Matches `OpenAPIRuntime.CodingStrategy.codable`. - static let codable: String = "codable" + /// Matches `OpenAPIRuntime.CodingStrategy.codable`. + static let codable: String = "codable" + + /// Matches `OpenAPIRuntime.CodingStrategy.data`. + static let data: String = "data" + } } /// Constants related to types used in many components. diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Content/CodingStrategy.swift b/Sources/_OpenAPIGeneratorCore/Translator/Content/CodingStrategy.swift index aa8f1acb..304a2766 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Content/CodingStrategy.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Content/CodingStrategy.swift @@ -12,8 +12,8 @@ // //===----------------------------------------------------------------------===// -/// Describes the underlying coding strategy. -enum CodingStrategy: String, Equatable, Hashable, Sendable { +/// Describes the underlying parameter coding strategy. +enum ParameterCodingStrategy: String, Equatable, Hashable, Sendable { /// A strategy using JSONEncoder/JSONDecoder. case codable @@ -28,11 +28,36 @@ enum CodingStrategy: String, Equatable, Hashable, Sendable { var runtimeName: String { switch self { case .codable: - return Constants.CodingStrategy.codable + return Constants.CodingStrategy.Parameter.codable case .string: - return Constants.CodingStrategy.string + return Constants.CodingStrategy.Parameter.string default: - return Constants.CodingStrategy.deferredToType + return Constants.CodingStrategy.Parameter.deferredToType + } + } +} + +/// Describes the underlying body coding strategy. +enum BodyCodingStrategy: String, Equatable, Hashable, Sendable { + + /// A strategy using JSONEncoder/JSONDecoder. + case codable + + /// A strategy using LosslessStringConvertible. + case string + + /// A strategy passing through the unmodified data. + case data + + /// The name of the coding strategy in the runtime library. + var runtimeName: String { + switch self { + case .codable: + return Constants.CodingStrategy.Body.codable + case .string: + return Constants.CodingStrategy.Body.string + case .data: + return Constants.CodingStrategy.Body.data } } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift index f92d2d95..db9f1b19 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift @@ -83,7 +83,7 @@ enum ContentType: Hashable { } /// The coding strategy appropriate for this content type. - var codingStrategy: CodingStrategy { + var parameterCodingStrategy: ParameterCodingStrategy { switch self { case .json: return .codable @@ -94,6 +94,18 @@ enum ContentType: Hashable { } } + /// The coding strategy appropriate for this content type. + var bodyCodingStrategy: BodyCodingStrategy { + switch self { + case .json: + return .codable + case .text: + return .string + default: + return .data + } + } + /// A Boolean value that indicates whether the content type /// is a type of JSON. var isJSON: Bool { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift index ccc022de..33e831c9 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift @@ -26,7 +26,7 @@ struct TypedParameter { var typeUsage: TypeUsage /// The coding strategy appropriate for this parameter. - var codingStrategy: CodingStrategy + var codingStrategy: ParameterCodingStrategy } extension TypedParameter: CustomStringConvertible { @@ -129,7 +129,7 @@ extension FileTranslator { let foundIn = "\(locationTypeName.description)/\(parameter.name)" let schema: Either, JSONSchema> - let codingStrategy: CodingStrategy + let codingStrategy: ParameterCodingStrategy switch parameter.schemaOrContent { case let .a(schemaContext): schema = schemaContext.schema @@ -180,7 +180,10 @@ extension FileTranslator { return nil } schema = typedContent.content.schema ?? .b(.fragment) - codingStrategy = typedContent.content.contentType.codingStrategy + codingStrategy = typedContent + .content + .contentType + .parameterCodingStrategy } // Check if the underlying schema is supported diff --git a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift index 29278a10..c7a5b851 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift @@ -193,7 +193,7 @@ extension ClientFileTranslator { ), .init( label: "strategy", - expression: .dot(contentType.codingStrategy.runtimeName) + expression: .dot(contentType.bodyCodingStrategy.runtimeName) ), ]) ) @@ -272,7 +272,7 @@ extension ServerFileTranslator { let content = typedContent.content let contentType = content.contentType let contentTypeIdentifier = contentType.identifier - let codingStrategyName = contentType.codingStrategy.runtimeName + let codingStrategyName = contentType.bodyCodingStrategy.runtimeName let isOptional = !requestBody.request.required let transformExpr: Expression = .closureInvocation( diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift index c1c7c9e2..50e0e807 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift @@ -30,7 +30,7 @@ struct TypedResponseHeader { var typeUsage: TypeUsage /// The coding strategy appropriate for this parameter. - var codingStrategy: CodingStrategy + var codingStrategy: ParameterCodingStrategy } extension TypedResponseHeader { @@ -104,7 +104,7 @@ extension FileTranslator { let foundIn = "\(parent.description)/\(name)" let schema: Either, JSONSchema> - let codingStrategy: CodingStrategy + let codingStrategy: ParameterCodingStrategy switch header.schemaOrContent { case let .a(schemaContext): @@ -121,7 +121,10 @@ extension FileTranslator { return nil } schema = typedContent.content.schema ?? .b(.fragment) - codingStrategy = typedContent.content.contentType.codingStrategy + codingStrategy = typedContent + .content + .contentType + .parameterCodingStrategy } // Check if schema is supported diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift index a3774829..6a3d0cb8 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift @@ -192,7 +192,7 @@ extension ClientFileTranslator { .init( label: "strategy", expression: .dot( - typedContent.content.contentType.codingStrategy.runtimeName + typedContent.content.contentType.bodyCodingStrategy.runtimeName ) ), .init( @@ -345,7 +345,7 @@ extension ServerFileTranslator { ), .init( label: "strategy", - expression: .dot(contentType.codingStrategy.runtimeName) + expression: .dot(contentType.bodyCodingStrategy.runtimeName) ), ]) ) diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift index 806be4f7..13041241 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift @@ -310,7 +310,7 @@ public struct Client: APIProtocol { return .init( value: value, contentType: "application/octet-stream", - strategy: .deferredToType + strategy: .data ) } } @@ -328,7 +328,7 @@ public struct Client: APIProtocol { let body: Operations.uploadAvatarForPet.Output.Ok.Body = try converter.bodyGet( Foundation.Data.self, from: response.body, - strategy: .deferredToType, + strategy: .data, transforming: { value in .binary(value) } ) return .ok(.init(headers: headers, body: body)) diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift index e6524a20..1b5105e1 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift @@ -400,7 +400,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { let body: Operations.uploadAvatarForPet.Input.Body = try converter.bodyGetRequired( Foundation.Data.self, from: request.body, - strategy: .deferredToType, + strategy: .data, transforming: { value in .binary(value) } ) return Operations.uploadAvatarForPet.Input( @@ -430,7 +430,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { return .init( value: value, contentType: "application/octet-stream", - strategy: .deferredToType + strategy: .data ) } } From 7066b4b58ab617c19d5081923ba91ceeedcbc277 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Sat, 3 Jun 2023 10:32:07 +0200 Subject: [PATCH 03/11] Formatting fixes --- .../Translator/CommonTypes/Constants.swift | 2 +- .../Translator/Parameters/TypedParameter.swift | 3 ++- .../Translator/Responses/TypedResponseHeader.swift | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index 1eef61d2..d2e877dd 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -338,7 +338,7 @@ enum Constants { /// Constants related to the coding strategy. enum CodingStrategy { - + /// Constants related to ParameterCodingStrategy enum Parameter { /// Matches `OpenAPIRuntime.CodingStrategy.string`. diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift index 33e831c9..bab02dc6 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift @@ -180,7 +180,8 @@ extension FileTranslator { return nil } schema = typedContent.content.schema ?? .b(.fragment) - codingStrategy = typedContent + codingStrategy = + typedContent .content .contentType .parameterCodingStrategy diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift index 50e0e807..8864f620 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift @@ -121,7 +121,8 @@ extension FileTranslator { return nil } schema = typedContent.content.schema ?? .b(.fragment) - codingStrategy = typedContent + codingStrategy = + typedContent .content .contentType .parameterCodingStrategy From 267c1f0d5b27a1bf8e280ebbb76b1a11c4ba44ff Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 6 Jun 2023 17:20:23 +0200 Subject: [PATCH 04/11] [Docs] Document the Converter type in preparation for #43 --- ...Converting-between-data-and-Swift-types.md | 234 ++++++++++++++++++ .../Documentation-for-maintainers.md | 13 + .../Swift-OpenAPI-Generator.md | 1 + 3 files changed, 248 insertions(+) create mode 100644 Sources/swift-openapi-generator/Documentation.docc/Development/Converting-between-data-and-Swift-types.md create mode 100644 Sources/swift-openapi-generator/Documentation.docc/Development/Documentation-for-maintainers.md 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 new file mode 100644 index 00000000..e221ab23 --- /dev/null +++ b/Sources/swift-openapi-generator/Documentation.docc/Development/Converting-between-data-and-Swift-types.md @@ -0,0 +1,234 @@ +# Converting between data and Swift types + +Learn about the type responsible for convertering between raw data and Swift types. + +## Overview + +The [`Converter`](https://github.com/apple/swift-openapi-runtime/blob/main/Sources/OpenAPIRuntime/Conversion/Converter.swift) type is a structure defined in the runtime library and is used by both the client and server generated code to perform conversions between raw data and Swift types. + +> Note: `Converter` is one of the SPI types, not considered part of the public API of the runtime library. However, because generated code relies on it, SPI stability needs to be considered when making changes to it and to the generator. + +Most of the functionality of `Converter` is implemented as helper methods in extensions: +- [`Converter+Client.swift`](https://github.com/apple/swift-openapi-runtime/blob/main/Sources/OpenAPIRuntime/Conversion/Converter%2BClient.swift) +- [`Converter+Server.swift`](https://github.com/apple/swift-openapi-runtime/blob/main/Sources/OpenAPIRuntime/Conversion/Converter%2BServer.swift) +- [`Converter+Common.swift`](https://github.com/apple/swift-openapi-runtime/blob/main/Sources/OpenAPIRuntime/Conversion/Converter%2BCommon.swift) + +Some helper methods can be reused between client and server code, such as headers, but most can't. It's important that we only generalize (move helper methods into common extensions) if the client and server variants would have been exact copies. However, if there are differences, prefer to keep them separate and optimize each variant (for client or server) separately. + +### Generated code and generics interaction + +As outlined in , we aim to minimize the complexity of the generator and rely on the Swift compiler to help ensure that if generated code compiles, it's likely to work correctly. + +To that end, if the input OpenAPI document contains an input that Swift OpenAPI Generator doesn't support, our first preference is to catch it in the generator and emit a descriptive error. However, there are cases where that is prohibitively complex, and we let the Swift compiler ensure that, for example, an array of strings cannot be used as a path parameter. In this example case, the generator emits code with the path parameter being of Swift type `[String]`, but since there doesn't exist a converter method for it, it will fail to build. This is considered expected behavior. + +In the case of the converter, it contains helper methods for all the supported combinations of an HTTP location, a "content type family" and a Swift type. + +First, a _schema location_ refers to one of the several places where schemas can be used in OpenAPI documents. For example: +- request path parameters +- request headers +- response bodies +- and more + +Second, a _content type family_ can be one of: +- `structured` + - example: `application/json` + - uses the type's `Codable` implementation +- `text` + - example: `text/plain` + - uses the type's `LosslessStringConvertible` implementation, except for `Foundation.Date`, which uses a system date formatter +- `raw` + - example: `application/octet-stream` + - doesn't transform the raw data, just passes it through + +The content type family is derived from the `content` map in the OpenAPI document, if provided. If none is provided, such as in case of parameters, `text` is used. + +And third, a Swift type is calculated from the JSON schema provided in the OpenAPI document. + +For example, a `string` schema is generated as `Swift.String`, an `object` schema is generated as a Swift structure, and an array schema is generated as a `Swift.Array` generic over the element type. + +Together, the schema location, the content type family, and the Swift type is enough to unambiguously decide which helper method on the converter should be used. + +For example, to use the converter to get a required response header of type `Foundation.Date` using the `text` content type family, look for a method (exact spelling is subject to change) that looks like: + +```swift +func headerFieldGetTextRequired( // <<< 1. + in headerFields: [HeaderField], + name: String, + as type: Date.Type // <<< 2. +) throws -> Date +``` + +In `1.`, notice that the method name contains which schema location, content type family, and optionality; whilie in `2.` it contains the Swift type. + +### Helper method variants + +In the nested list below, each leaf is one helper method. + +"string-convertible" refers to types that conform to `LosslessStringConvertible` (but not `Foundation.Date`, which is handled separately). + + +#### Required by client code + +- request + - set request path [client-only] + - text + - string-convertible + - optional/required + - date + - optional/required + - set request query [client-only] + - text + - string-convertible + - optional/required + - array of string-convertibles + - optional/required + - date + - optional/required + - array of dates + - optional/required + - set request headers [common] + - text + - string-convertible + - optional/required + - array of string-convertibles + - optional/required + - date + - optional/required + - array of dates + - optional/required + - structured + - codable + - optional/required + - set request body [client-only] + - text + - string-convertible + - optional + - required + - date + - optional + - required + - structured + - codable + - optional + - required + - raw + - data + - optional + - required +- response + - get response headers [common] + - text + - string-convertible + - optional + - required + - array of string-convertibles + - optional + - required + - date + - optional + - required + - array of dates + - optional + - required + - structured + - codable + - optional + - required + - get response body [client-only] + - text + - string-convertible + - required + - date + - required + - structured + - codable + - required + - raw + - data + - required + +#### Required by server code + +- request + - get request path [server-only] + - text + - string-convertible + - optional + - required + - date + - optional + - required + - get request query [server-only] + - text + - string-convertible + - optional + - required + - array of string-convertibles + - optional + - required + - date + - optional + - required + - array of dates + - optional + - required + - get request headers [common] + - text + - string-convertible + - optional + - required + - array of string-convertibles + - optional + - required + - date + - optional + - required + - array of dates + - optional + - required + - structured + - codable + - optional + - required + - get request body [server-only] + - text + - string-convertible + - optional + - required + - date + - optional + - required + - structured + - codable + - optional + - required + - raw + - data + - optional + - required +- response + - set response headers [common] + - text + - string-convertible + - optional/required + - array of string-convertibles + - optional/required + - date + - optional/required + - array of dates + - optional/required + - structured + - codable + - optional/required + - set response body [server-only] + - text + - string-convertible + - required + - date + - required + - structured + - codable + - required + - raw + - data + - required diff --git a/Sources/swift-openapi-generator/Documentation.docc/Development/Documentation-for-maintainers.md b/Sources/swift-openapi-generator/Documentation.docc/Development/Documentation-for-maintainers.md new file mode 100644 index 00000000..6ac22f4a --- /dev/null +++ b/Sources/swift-openapi-generator/Documentation.docc/Development/Documentation-for-maintainers.md @@ -0,0 +1,13 @@ +# Documentation for maintainers + +Learn about the internals of Swift OpenAPI Generator. + +## Overview + +Swift OpenAPI Generator contains multiple moving pieces, from the runtime library, to the generator CLI, plugin, to extension packages using the transport and middleware APIs. + +Use the resources below if you'd like to learn more about how the generator works under the hood, for example as part of contribututing a new feature to it. + +## Topics + +- diff --git a/Sources/swift-openapi-generator/Documentation.docc/Swift-OpenAPI-Generator.md b/Sources/swift-openapi-generator/Documentation.docc/Swift-OpenAPI-Generator.md index 7da32322..6ef76c61 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Swift-OpenAPI-Generator.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Swift-OpenAPI-Generator.md @@ -75,6 +75,7 @@ The generated code, runtime library, and transports are supported on more platfo ### Getting involved - - +- [openapi]: https://openapis.org [tools]: https://openapi.tools From 7190f479ee892f302526a9768bf5e4ea05523a1e Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 6 Jun 2023 18:16:14 +0200 Subject: [PATCH 05/11] PR feedback --- .../Development/Converting-between-data-and-Swift-types.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 e221ab23..cf5b992b 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 @@ -19,7 +19,7 @@ Some helper methods can be reused between client and server code, such as header As outlined in , we aim to minimize the complexity of the generator and rely on the Swift compiler to help ensure that if generated code compiles, it's likely to work correctly. -To that end, if the input OpenAPI document contains an input that Swift OpenAPI Generator doesn't support, our first preference is to catch it in the generator and emit a descriptive error. However, there are cases where that is prohibitively complex, and we let the Swift compiler ensure that, for example, an array of strings cannot be used as a path parameter. In this example case, the generator emits code with the path parameter being of Swift type `[String]`, but since there doesn't exist a converter method for it, it will fail to build. This is considered expected behavior. +To that end, if the input OpenAPI document contains an input that Swift OpenAPI Generator doesn't support, our first preference is to catch it in the generator and emit a descriptive diagnostic. However, there are cases where that is prohibitively complex, and we let the Swift compiler ensure that, for example, an array of strings cannot be used as a path parameter. In this example case, the generator emits code with the path parameter being of Swift type `[String]`, but since there doesn't exist a converter method for it, it will fail to build. This is considered expected behavior. In the case of the converter, it contains helper methods for all the supported combinations of an HTTP location, a "content type family" and a Swift type. @@ -58,7 +58,7 @@ func headerFieldGetTextRequired( // <<< 1. ) throws -> Date ``` -In `1.`, notice that the method name contains which schema location, content type family, and optionality; whilie in `2.` it contains the Swift type. +In `1.`, notice that the method name contains which schema location, content type family, and optionality; while in `2.` it contains the Swift type. ### Helper method variants From ed1e0ec9426360c8bda312ac6cbba9f8e16fcb7e Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 7 Jun 2023 13:27:29 +0200 Subject: [PATCH 06/11] Updated Converter docs based on feedback --- ...Converting-between-data-and-Swift-types.md | 313 ++++++------------ 1 file changed, 103 insertions(+), 210 deletions(-) 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 cf5b992b..f8995ccc 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 @@ -1,10 +1,10 @@ # Converting between data and Swift types -Learn about the type responsible for convertering between raw data and Swift types. +Learn about the type responsible for convertering between binary data and Swift types. ## Overview -The [`Converter`](https://github.com/apple/swift-openapi-runtime/blob/main/Sources/OpenAPIRuntime/Conversion/Converter.swift) type is a structure defined in the runtime library and is used by both the client and server generated code to perform conversions between raw data and Swift types. +The [`Converter`](https://github.com/apple/swift-openapi-runtime/blob/main/Sources/OpenAPIRuntime/Conversion/Converter.swift) type is a structure defined in the runtime library and is used by both the client and server generated code to perform conversions between binary data and Swift types. > Note: `Converter` is one of the SPI types, not considered part of the public API of the runtime library. However, because generated code relies on it, SPI stability needs to be considered when making changes to it and to the generator. @@ -21,214 +21,107 @@ As outlined in , we aim to minimize the complexity To that end, if the input OpenAPI document contains an input that Swift OpenAPI Generator doesn't support, our first preference is to catch it in the generator and emit a descriptive diagnostic. However, there are cases where that is prohibitively complex, and we let the Swift compiler ensure that, for example, an array of strings cannot be used as a path parameter. In this example case, the generator emits code with the path parameter being of Swift type `[String]`, but since there doesn't exist a converter method for it, it will fail to build. This is considered expected behavior. -In the case of the converter, it contains helper methods for all the supported combinations of an HTTP location, a "content type family" and a Swift type. - -First, a _schema location_ refers to one of the several places where schemas can be used in OpenAPI documents. For example: -- request path parameters -- request headers -- response bodies -- and more - -Second, a _content type family_ can be one of: -- `structured` - - example: `application/json` - - uses the type's `Codable` implementation -- `text` - - example: `text/plain` - - uses the type's `LosslessStringConvertible` implementation, except for `Foundation.Date`, which uses a system date formatter -- `raw` - - example: `application/octet-stream` - - doesn't transform the raw data, just passes it through - -The content type family is derived from the `content` map in the OpenAPI document, if provided. If none is provided, such as in case of parameters, `text` is used. - -And third, a Swift type is calculated from the JSON schema provided in the OpenAPI document. - -For example, a `string` schema is generated as `Swift.String`, an `object` schema is generated as a Swift structure, and an array schema is generated as a `Swift.Array` generic over the element type. - -Together, the schema location, the content type family, and the Swift type is enough to unambiguously decide which helper method on the converter should be used. - -For example, to use the converter to get a required response header of type `Foundation.Date` using the `text` content type family, look for a method (exact spelling is subject to change) that looks like: - -```swift -func headerFieldGetTextRequired( // <<< 1. - in headerFields: [HeaderField], - name: String, - as type: Date.Type // <<< 2. -) throws -> Date -``` - -In `1.`, notice that the method name contains which schema location, content type family, and optionality; while in `2.` it contains the Swift type. +In the case of the converter, it contains helper methods for all the supported combinations of an schema location, a "coding strategy" and a Swift type. + +### Dimensions of helper methods + +Below is a list of the "dimensions" across which the helper methods differ: + +- **Client/server** represents whether the code is needed by the client, server, or both ("common"). +- **Set/get** represents whether the generated code sets or gets the value. +- **Schema location** refers to one of the several places where schemas can be used in OpenAPI documents. Values: + - request path parameters + - request query items + - request header fields + - request body + - response header fields + - response body +- **Coding strategy** represents the chosen encoder/decoder to convert the Swift type to/from data. Values: + - `JSON` + - example: `application/json` + - uses the type's `Codable` implementation and `JSONEncoder`/`JSONDecoder` + - `text` + - example: `text/plain` + - uses the type's `LosslessStringConvertible` implementation, except for `Foundation.Date`, which uses a system date formatter + - `binary` + - example: `application/octet-stream` + - doesn't transform the binary data, just passes it through + - serves as the fallback for content types that don't have more specific handling +- **Swift type** represents the generated type in Swift that best represents the JSON schema defined in the OpenAPI document. For example, a `string` schema is generated as `Swift.String`, an `object` schema is generated as a Swift structure, and an `array` schema is generated as a `Swift.Array` generic over the element type. For the helper methods, it's important which protocol they conform to, as those are used for serialization. Values: + - _string-convertible_ refers to types that conform to `LosslessStringConvertible` + - _array of string-convertibles_ refers to an array of types that conform to `LosslessStringConvertible` + - _date_ is represented by `Foundation.Date` + - _array of dates_ refers to an array of `Foundation.Date` + - _codable_ refers to types that conform to `Codable` + - _data_ is represented by `Foundation.Data` +- **Optional/required** represents whether the method works with optional values. Values: + - _required_ represents a special overload only for required values + - _optional_ represents a special overload only for optional values + - _both_ represents a special overload that works for optional values without negatively impacting passed-in required values (for example, setters) ### Helper method variants -In the nested list below, each leaf is one helper method. - -"string-convertible" refers to types that conform to `LosslessStringConvertible` (but not `Foundation.Date`, which is handled separately). - - -#### Required by client code - -- request - - set request path [client-only] - - text - - string-convertible - - optional/required - - date - - optional/required - - set request query [client-only] - - text - - string-convertible - - optional/required - - array of string-convertibles - - optional/required - - date - - optional/required - - array of dates - - optional/required - - set request headers [common] - - text - - string-convertible - - optional/required - - array of string-convertibles - - optional/required - - date - - optional/required - - array of dates - - optional/required - - structured - - codable - - optional/required - - set request body [client-only] - - text - - string-convertible - - optional - - required - - date - - optional - - required - - structured - - codable - - optional - - required - - raw - - data - - optional - - required -- response - - get response headers [common] - - text - - string-convertible - - optional - - required - - array of string-convertibles - - optional - - required - - date - - optional - - required - - array of dates - - optional - - required - - structured - - codable - - optional - - required - - get response body [client-only] - - text - - string-convertible - - required - - date - - required - - structured - - codable - - required - - raw - - data - - required - -#### Required by server code - -- request - - get request path [server-only] - - text - - string-convertible - - optional - - required - - date - - optional - - required - - get request query [server-only] - - text - - string-convertible - - optional - - required - - array of string-convertibles - - optional - - required - - date - - optional - - required - - array of dates - - optional - - required - - get request headers [common] - - text - - string-convertible - - optional - - required - - array of string-convertibles - - optional - - required - - date - - optional - - required - - array of dates - - optional - - required - - structured - - codable - - optional - - required - - get request body [server-only] - - text - - string-convertible - - optional - - required - - date - - optional - - required - - structured - - codable - - optional - - required - - raw - - data - - optional - - required -- response - - set response headers [common] - - text - - string-convertible - - optional/required - - array of string-convertibles - - optional/required - - date - - optional/required - - array of dates - - optional/required - - structured - - codable - - optional/required - - set response body [server-only] - - text - - string-convertible - - required - - date - - required - - structured - - codable - - required - - raw - - data - - required +Together, the dimensions are enough to deterministically decide which helper method on the converter should be used. + +In the list below, each row represents one helper method. + +| 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 | From 60f1ca0be42d76a77f6e368883256618aec196a7 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 8 Jun 2023 00:01:27 +0200 Subject: [PATCH 07/11] Work in progress --- .../Development/Converting-between-data-and-Swift-types.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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..4bb0ffa9 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 @@ -67,8 +67,8 @@ In the list below, each row represents one helper method. | 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 | string-convertible | both | setHeaderFieldAsText | +| common | set | header field | text | array of string-convertibles | both | setHeaderFieldAsText | | 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 | From 69566b6c203e02f5dc579b265e21461e1ac83e89 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 8 Jun 2023 16:59:07 +0200 Subject: [PATCH 08/11] The new approach now working --- .../Extensions/Foundation.swift | 12 +- .../Renderer/TextBasedRenderer.swift | 2 +- .../translateClientMethod.swift | 22 ++- .../Translator/CommonTypes/Constants.swift | 27 +-- .../Translator/Content/CodingStrategy.swift | 49 ++--- .../Translator/Content/ContentType.swift | 22 +-- .../Operations/OperationDescription.swift | 22 ++- .../Parameters/TypedParameter.swift | 8 +- .../Parameters/translateParameter.swift | 62 +++---- .../RequestBody/translateRequestBody.swift | 14 +- .../Responses/TypedResponseHeader.swift | 8 +- .../Responses/translateResponseHeader.swift | 12 +- .../Responses/translateResponseOutcome.swift | 14 +- ...Converting-between-data-and-Swift-types.md | 114 ++++++------ .../Parser/Test_YamsParser.swift | 6 +- .../ReferenceTest.swift | 2 +- .../ReferenceSources/Petstore/Client.swift | 173 +++++++++--------- .../ReferenceSources/Petstore/Server.swift | 103 +++++------ Tests/PetstoreConsumerTests/Assertions.swift | 5 +- Tests/PetstoreConsumerTests/Common.swift | 15 +- Tests/PetstoreConsumerTests/Test_Server.swift | 8 +- Tests/PetstoreConsumerTests/Test_Types.swift | 12 +- 22 files changed, 297 insertions(+), 415 deletions(-) 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 151e18ea..5c6cf317 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift @@ -22,16 +22,27 @@ 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,16 +76,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: "strategy", - expression: .dot(Constants.CodingStrategy.Parameter.string) - ), .init(label: "name", expression: "accept"), .init(label: "value", expression: .literal(acceptValue)), ]) @@ -98,6 +105,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 d2e877dd..042ac6ff 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -339,29 +339,14 @@ enum Constants { /// Constants related to the coding strategy. enum CodingStrategy { - /// Constants related to ParameterCodingStrategy - enum Parameter { - /// Matches `OpenAPIRuntime.CodingStrategy.string`. - static let string: String = "string" + /// The substring used in method names for the JSON coding strategy. + static let json: String = "JSON" - /// Matches `OpenAPIRuntime.CodingStrategy.codable`. - static let codable: String = "codable" + /// The substring used in method names for the text coding strategy. + static let text: String = "Text" - /// Matches `OpenAPIRuntime.CodingStrategy.deferredToType`. - static let deferredToType: String = "deferredToType" - } - - /// Constants related to BodyCodingStrategy - enum Body { - /// Matches `OpenAPIRuntime.CodingStrategy.string`. - static let string: String = "string" - - /// Matches `OpenAPIRuntime.CodingStrategy.codable`. - static let codable: String = "codable" - - /// Matches `OpenAPIRuntime.CodingStrategy.data`. - static let data: String = "data" - } + /// The substring used in method names for the binary coding strategy. + static let binary: String = "Binary" } /// Constants related to types used in many components. diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Content/CodingStrategy.swift b/Sources/_OpenAPIGeneratorCore/Translator/Content/CodingStrategy.swift index 304a2766..921db100 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Content/CodingStrategy.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Content/CodingStrategy.swift @@ -12,52 +12,27 @@ // //===----------------------------------------------------------------------===// -/// Describes the underlying parameter coding strategy. -enum ParameterCodingStrategy: String, Equatable, Hashable, Sendable { +/// Describes the underlying coding strategy. +enum CodingStrategy: String, Equatable, Hashable, Sendable { /// A strategy using JSONEncoder/JSONDecoder. - case codable + case json /// A strategy using LosslessStringConvertible. - case string + case text - /// A strategy for letting the type choose the appropriate option. - case deferredToType + /// 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 .codable: - return Constants.CodingStrategy.Parameter.codable - case .string: - return Constants.CodingStrategy.Parameter.string - default: - return Constants.CodingStrategy.Parameter.deferredToType - } - } -} - -/// Describes the underlying body coding strategy. -enum BodyCodingStrategy: String, Equatable, Hashable, Sendable { - - /// A strategy using JSONEncoder/JSONDecoder. - case codable - - /// A strategy using LosslessStringConvertible. - case string - - /// A strategy passing through the unmodified data. - case data - - /// The name of the coding strategy in the runtime library. - var runtimeName: String { - switch self { - case .codable: - return Constants.CodingStrategy.Body.codable - case .string: - return Constants.CodingStrategy.Body.string - case .data: - return Constants.CodingStrategy.Body.data + 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 db9f1b19..1db30f73 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Content/ContentType.swift @@ -83,26 +83,14 @@ enum ContentType: Hashable { } /// The coding strategy appropriate for this content type. - var parameterCodingStrategy: ParameterCodingStrategy { + var codingStrategy: CodingStrategy { switch self { case .json: - return .codable + return .json case .text: - return .string - default: - return .deferredToType - } - } - - /// The coding strategy appropriate for this content type. - var bodyCodingStrategy: BodyCodingStrategy { - switch self { - case .json: - return .codable - case .text: - return .string - default: - return .data + return .text + case .binary: + return .binary } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift b/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift index d2365310..396b2782 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift @@ -232,23 +232,27 @@ 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 bab02dc6..36b901ce 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift @@ -26,7 +26,7 @@ struct TypedParameter { var typeUsage: TypeUsage /// The coding strategy appropriate for this parameter. - var codingStrategy: ParameterCodingStrategy + var codingStrategy: CodingStrategy } extension TypedParameter: CustomStringConvertible { @@ -129,11 +129,11 @@ extension FileTranslator { let foundIn = "\(locationTypeName.description)/\(parameter.name)" let schema: Either, JSONSchema> - let codingStrategy: ParameterCodingStrategy + let codingStrategy: CodingStrategy switch parameter.schemaOrContent { case let .a(schemaContext): schema = schemaContext.schema - codingStrategy = .deferredToType + codingStrategy = .text // Check supported exploded/style types let location = parameter.location @@ -184,7 +184,7 @@ extension FileTranslator { typedContent .content .contentType - .parameterCodingStrategy + .codingStrategy } // Check if the underlying schema is supported diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift index 06dfc201..15ef8183 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 } @@ -113,21 +114,13 @@ extension ClientFileTranslator { ) throws -> Expression? { let methodPrefix: String let containerExpr: Expression - let extraArguments: [FunctionArgumentDescription] switch parameter.location { case .header: - methodPrefix = "headerField" + methodPrefix = "HeaderField" containerExpr = .identifier(requestVariableName).dot("headerFields") - extraArguments = [ - .init( - label: "strategy", - expression: .dot(parameter.codingStrategy.runtimeName) - ) - ] case .query: - methodPrefix = "query" + methodPrefix = "QueryItem" containerExpr = .identifier(requestVariableName) - extraArguments = [] default: diagnostics.emitUnsupported( "Parameter of type \(parameter.location.rawValue)", @@ -137,14 +130,13 @@ extension ClientFileTranslator { } return .try( .identifier("converter") - .dot("\(methodPrefix)Add") + .dot("set\(methodPrefix)As\(parameter.codingStrategy.runtimeName)") .call( [ .init( label: "in", expression: .inOut(containerExpr) - ) - ] + extraArguments + [ + ), .init(label: "name", expression: .literal(parameter.name)), .init( label: "value", @@ -170,18 +162,22 @@ extension ServerFileTranslator { let parameterTypeName = typedParameter .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)), @@ -192,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)), @@ -210,21 +200,11 @@ 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: "strategy", - expression: .dot(typedParameter.codingStrategy.runtimeName) - ), .init(label: "name", expression: .literal(parameter.name)), .init( label: "as", diff --git a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift index c7a5b851..b4ccd649 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift @@ -191,10 +191,6 @@ extension ClientFileTranslator { label: "contentType", expression: .literal(contentTypeHeaderValue) ), - .init( - label: "strategy", - expression: .dot(contentType.bodyCodingStrategy.runtimeName) - ), ]) ) let caseDecl: SwitchCaseDescription = .init( @@ -220,7 +216,7 @@ 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( @@ -272,7 +268,7 @@ extension ServerFileTranslator { let content = typedContent.content let contentType = content.contentType let contentTypeIdentifier = contentType.identifier - let codingStrategyName = contentType.bodyCodingStrategy.runtimeName + let codingStrategyName = contentType.codingStrategy.runtimeName let isOptional = !requestBody.request.required let transformExpr: Expression = .closureInvocation( @@ -288,7 +284,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, @@ -303,10 +299,6 @@ extension ServerFileTranslator { label: "from", expression: .identifier(requestVariableName).dot("body") ), - .init( - label: "strategy", - expression: .dot(codingStrategyName) - ), .init(label: "transforming", expression: transformExpr), ]) ) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift index 8864f620..7accb2e8 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift @@ -30,7 +30,7 @@ struct TypedResponseHeader { var typeUsage: TypeUsage /// The coding strategy appropriate for this parameter. - var codingStrategy: ParameterCodingStrategy + var codingStrategy: CodingStrategy } extension TypedResponseHeader { @@ -104,12 +104,12 @@ extension FileTranslator { let foundIn = "\(parent.description)/\(name)" let schema: Either, JSONSchema> - let codingStrategy: ParameterCodingStrategy + let codingStrategy: CodingStrategy switch header.schemaOrContent { case let .a(schemaContext): schema = schemaContext.schema - codingStrategy = .deferredToType + codingStrategy = .text case let .b(contentMap): guard let typedContent = try bestSingleTypedContent( @@ -125,7 +125,7 @@ extension FileTranslator { typedContent .content .contentType - .parameterCodingStrategy + .codingStrategy } // Check if schema is supported diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift index 17fe15a5..9de12d91 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift @@ -95,16 +95,12 @@ 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", expression: .identifier(responseVariableName).dot("headerFields") ), - .init( - label: "strategy", - expression: .dot(header.codingStrategy.runtimeName) - ), .init(label: "name", expression: .literal(header.name)), .init( label: "as", @@ -135,7 +131,7 @@ extension ServerFileTranslator { ) throws -> Expression { return .try( .identifier("converter") - .dot("headerFieldAdd") + .dot("setHeaderFieldAs\(header.codingStrategy.runtimeName)") .call([ .init( label: "in", @@ -144,10 +140,6 @@ extension ServerFileTranslator { .dot("headerFields") ) ), - .init( - label: "strategy", - expression: .dot(header.codingStrategy.runtimeName) - ), .init(label: "name", expression: .literal(header.name)), .init( label: "value", diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift index 6a3d0cb8..a6dab94b 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift @@ -182,19 +182,13 @@ extension ClientFileTranslator { type: bodyTypeName.fullyQualifiedSwiftName, right: .try( .identifier("converter") - .dot("bodyGet") + .dot("getResponseBodyAs\(typedContent.content.contentType.codingStrategy.runtimeName)") .call([ .init( label: nil, expression: .identifier(contentTypeUsage.fullyQualifiedSwiftName).dot("self") ), .init(label: "from", expression: .identifier("response").dot("body")), - .init( - label: "strategy", - expression: .dot( - typedContent.content.contentType.bodyCodingStrategy.runtimeName - ) - ), .init( label: "transforming", expression: transformExpr @@ -343,10 +337,6 @@ extension ServerFileTranslator { label: "contentType", expression: .literal(contentType.headerValueForSending) ), - .init( - label: "strategy", - expression: .dot(contentType.bodyCodingStrategy.runtimeName) - ), ]) ) ) @@ -369,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 4bb0ffa9..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 | setHeaderFieldAsText | | common | set | header field | text | array of string-convertibles | both | setHeaderFieldAsText | -| 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 | 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/ReferenceSources/Petstore/Client.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift index 13041241..ff48c27d 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift @@ -43,21 +43,36 @@ 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, - strategy: .deferredToType, 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, - strategy: .string, name: "accept", value: "application/json" ) @@ -67,15 +82,13 @@ 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, - strategy: .deferredToType, name: "My-Response-UUID", as: Swift.String.self ), - My_Tracing_Header: try converter.headerFieldGetOptional( + My_Tracing_Header: try converter.getOptionalHeaderFieldAsText( in: response.headerFields, - strategy: .deferredToType, name: "My-Tracing-Header", as: Components.Headers.TracingHeader.self ) @@ -84,12 +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, - strategy: .codable, - 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() @@ -97,12 +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, - strategy: .codable, - 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) @@ -121,21 +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, - strategy: .codable, name: "X-Extra-Arguments", value: input.headers.X_Extra_Arguments ) - try converter.headerFieldAdd( + try converter.setHeaderFieldAsText( in: &request.headerFields, - strategy: .string, name: "accept", value: "application/json" ) - request.body = try converter.bodyAddRequired( + request.body = try converter.setRequiredRequestBodyAsJSON( input.body, headerFields: &request.headerFields, transforming: { wrapped in @@ -143,8 +155,7 @@ public struct Client: APIProtocol { case let .json(value): return .init( value: value, - contentType: "application/json; charset=utf-8", - strategy: .codable + contentType: "application/json; charset=utf-8" ) } } @@ -155,9 +166,8 @@ 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, - strategy: .codable, name: "X-Extra-Arguments", as: Components.Schemas.CodeError.self ) @@ -166,18 +176,17 @@ 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, - strategy: .codable, - 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, - strategy: .deferredToType, name: "X-Reason", as: Swift.String.self ) @@ -186,12 +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, - strategy: .codable, - 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()) } @@ -206,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 }, @@ -230,18 +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, - strategy: .string, name: "accept", value: "application/json" ) - request.body = try converter.bodyAddOptional( + request.body = try converter.setOptionalRequestBodyAsJSON( input.body, headerFields: &request.headerFields, transforming: { wrapped in @@ -249,8 +259,7 @@ public struct Client: APIProtocol { case let .json(value): return .init( value: value, - contentType: "application/json; charset=utf-8", - strategy: .codable + contentType: "application/json; charset=utf-8" ) } } @@ -268,12 +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, - strategy: .codable, - 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()) } @@ -290,28 +299,24 @@ 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, - strategy: .string, name: "accept", 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 switch wrapped { case let .binary(value): - return .init( - value: value, - contentType: "application/octet-stream", - strategy: .data - ) + return .init(value: value, contentType: "application/octet-stream") } } ) @@ -325,12 +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, - strategy: .data, - 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 = @@ -340,10 +345,9 @@ 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, - strategy: .codable, transforming: { value in .json(value) } ) return .preconditionFailed(.init(headers: headers, body: body)) @@ -355,10 +359,9 @@ public struct Client: APIProtocol { substring: "text/plain" ) let body: Operations.uploadAvatarForPet.Output.InternalServerError.Body = - try converter.bodyGet( + try converter.getResponseBodyAsText( Swift.String.self, from: response.body, - strategy: .string, transforming: { value in .text(value) } ) return .internalServerError(.init(headers: headers, body: body)) diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift index 1b5105e1..d1edcd47 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift @@ -69,31 +69,30 @@ 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, - strategy: .deferredToType, name: "My-Request-UUID", as: Swift.String.self ) @@ -114,15 +113,13 @@ 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, - strategy: .deferredToType, name: "My-Response-UUID", value: value.headers.My_Response_UUID ) - try converter.headerFieldAdd( + try converter.setHeaderFieldAsText( in: &response.headerFields, - strategy: .deferredToType, name: "My-Tracing-Header", value: value.headers.My_Tracing_Header ) @@ -130,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 @@ -138,8 +135,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { case let .json(value): return .init( value: value, - contentType: "application/json; charset=utf-8", - strategy: .codable + contentType: "application/json; charset=utf-8" ) } } @@ -153,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 @@ -161,8 +157,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { case let .json(value): return .init( value: value, - contentType: "application/json; charset=utf-8", - strategy: .codable + contentType: "application/json; charset=utf-8" ) } } @@ -184,20 +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, - strategy: .codable, 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, - strategy: .codable, - 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, @@ -212,9 +206,8 @@ 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, - strategy: .codable, name: "X-Extra-Arguments", value: value.headers.X_Extra_Arguments ) @@ -222,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 @@ -230,8 +223,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { case let .json(value): return .init( value: value, - contentType: "application/json; charset=utf-8", - strategy: .codable + contentType: "application/json; charset=utf-8" ) } } @@ -241,9 +233,8 @@ 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, - strategy: .deferredToType, name: "X-Reason", value: value.headers.X_Reason ) @@ -251,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 @@ -259,8 +250,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { case let .json(value): return .init( value: value, - contentType: "application/json; charset=utf-8", - strategy: .codable + contentType: "application/json; charset=utf-8" ) } } @@ -316,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 @@ -326,10 +316,9 @@ 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, - strategy: .codable, transforming: { value in .json(value) } ) return Operations.updatePet.Input( @@ -355,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 @@ -363,8 +352,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { case let .json(value): return .init( value: value, - contentType: "application/json; charset=utf-8", - strategy: .codable + contentType: "application/json; charset=utf-8" ) } } @@ -388,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 @@ -397,12 +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, - strategy: .data, - 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, @@ -421,17 +409,13 @@ 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 switch wrapped { case let .binary(value): - return .init( - value: value, - contentType: "application/octet-stream", - strategy: .data - ) + return .init(value: value, contentType: "application/octet-stream") } } ) @@ -444,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 @@ -452,8 +436,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { case let .json(value): return .init( value: value, - contentType: "application/json; charset=utf-8", - strategy: .codable + contentType: "application/json; charset=utf-8" ) } } @@ -464,17 +447,13 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { var response: Response = .init(statusCode: 500) suppressMutabilityWarning(&response) try converter.validateAcceptIfPresent("text/plain", in: request.headerFields) - response.body = try converter.bodyAdd( + 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", - strategy: .string - ) + return .init(value: value, contentType: "text/plain") } } ) 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_Server.swift b/Tests/PetstoreConsumerTests/Test_Server.swift index 05f5e04f..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, #""" 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) ) ) } From 5a01d7518188b5c0b348ddbc0a3acd93f58bdeff Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 8 Jun 2023 17:33:51 +0200 Subject: [PATCH 09/11] Make the plain text content explicit --- .../Resources/Docs/petstore.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml b/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml index 273d68ca..c874d1bd 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml @@ -177,7 +177,9 @@ paths: '500': description: Server error content: - text/plain: {} + text/plain: + schema: + type: string components: headers: TracingHeader: From 4ef033df2c84406a42485b5b07f02e4631724a85 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 8 Jun 2023 18:55:47 +0200 Subject: [PATCH 10/11] Bump runtime --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"), From a9c48f7275fada302ca496c2ec92ea7163f0341b Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 8 Jun 2023 19:01:51 +0200 Subject: [PATCH 11/11] Formatting fixes --- .../ClientTranslator/translateClientMethod.swift | 9 +++++---- .../Translator/Operations/OperationDescription.swift | 3 ++- .../Translator/Parameters/translateParameter.swift | 4 ++-- .../Translator/RequestBody/translateRequestBody.swift | 4 +++- .../Translator/Responses/translateResponseHeader.swift | 4 +++- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift index 5c6cf317..3186cd49 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift @@ -30,10 +30,11 @@ extension ClientFileTranslator { left: "path", right: .try( .identifier("converter") - .dot("renderedRequestPath").call([ - .init(label: "template", expression: .literal(pathTemplate)), - .init(label: "parameters", expression: pathParamsArrayExpr) - ]) + .dot("renderedRequestPath") + .call([ + .init(label: "template", expression: .literal(pathTemplate)), + .init(label: "parameters", expression: pathParamsArrayExpr), + ]) ) ) let requestDecl: Declaration = .variable( diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift b/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift index 396b2782..c9b2be36 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift @@ -247,7 +247,8 @@ extension OperationDescription { with: "{}" ) } - let names: [Expression] = pathParameters + let names: [Expression] = + pathParameters .map { param in .identifier("input.path.\(param.name.asSwiftSafeName)") } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift index 15ef8183..a70f656a 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift @@ -162,7 +162,7 @@ extension ServerFileTranslator { let parameterTypeName = typedParameter .typeUsage .fullyQualifiedNonOptionalSwiftName - + func methodName(_ parameterLocationName: String, _ requiresOptionality: Bool = true) -> String { let optionality: String if requiresOptionality { @@ -172,7 +172,7 @@ extension ServerFileTranslator { } return "get\(optionality)\(parameterLocationName)As\(typedParameter.codingStrategy.runtimeName)" } - + let convertExpr: Expression switch parameter.location { case .path: diff --git a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift index b4ccd649..92aee5f1 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift @@ -216,7 +216,9 @@ extension ClientFileTranslator { left: .identifier(requestVariableName).dot("body"), right: .try( .identifier("converter") - .dot("set\(requestBody.request.required ? "Required" : "Optional")RequestBodyAs\(contentType.codingStrategy.runtimeName)") + .dot( + "set\(requestBody.request.required ? "Required" : "Optional")RequestBodyAs\(contentType.codingStrategy.runtimeName)" + ) .call([ .init(label: nil, expression: .identifier(inputVariableName).dot("body")), .init( diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift index 9de12d91..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("get\(header.isOptional ? "Optional" : "Required")HeaderFieldAs\(header.codingStrategy.runtimeName)") + .dot( + "get\(header.isOptional ? "Optional" : "Required")HeaderFieldAs\(header.codingStrategy.runtimeName)" + ) .call([ .init( label: "in",