From 0e4b57ca57297ed19f85e0820e7ec1aecc304023 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 10 Aug 2023 10:36:41 +0200 Subject: [PATCH 1/7] [Prototype] Accept header --- .../Parameters/TypedParameter.swift | 11 +++++ .../Client.swift | 5 +- .../Server.swift | 6 ++- .../Types.swift | 42 +++++++++++++++- .../Test_Client.swift | 48 +++++++++++++++++++ .../Test_Server.swift | 38 +++++++++++++++ 6 files changed, 145 insertions(+), 5 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift index a7e7a847..356af5ad 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift @@ -135,6 +135,17 @@ extension FileTranslator { parameter = _parameter } + // OpenAPI 3.0.3: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#fixed-fields-10 + // > If in is "header" and the name field is "Accept", "Content-Type" or "Authorization", the parameter definition SHALL be ignored. + if parameter.location == .header { + switch parameter.name.lowercased() { + case "accept", "content-type", "authorization": + return nil + default: + break + } + } + let locationTypeName = parameter.location.typeName(in: parent) let foundIn = "\(locationTypeName.description)/\(parameter.name)" diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Client.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Client.swift index ae604978..d30de66c 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Client.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Client.swift @@ -262,10 +262,9 @@ public struct Client: APIProtocol { ) var request: OpenAPIRuntime.Request = .init(path: path, method: .get) suppressMutabilityWarning(&request) - try converter.setHeaderFieldAsText( + try converter.setAcceptHeader( in: &request.headerFields, - name: "accept", - value: "application/json, text/plain, application/octet-stream" + value: input.headers.accept ) return request }, diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Server.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Server.swift index 0d4a3472..67b1ef9b 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Server.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Server.swift @@ -285,7 +285,11 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { using: { APIHandler.getStats($0) }, deserializer: { request, metadata in let path: Operations.getStats.Input.Path = .init() let query: Operations.getStats.Input.Query = .init() - let headers: Operations.getStats.Input.Headers = .init() + let headers: Operations.getStats.Input.Headers = .init( + accept: try converter.extractAcceptHeaderIfPresent( + in: request.headerFields + ) + ) let cookies: Operations.getStats.Input.Cookies = .init() return Operations.getStats.Input( path: path, diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Types.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Types.swift index b8e1de93..9d609b09 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Types.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Types.swift @@ -1066,8 +1066,48 @@ public enum Operations { } public var query: Operations.getStats.Input.Query public struct Headers: Sendable, Equatable, Hashable { + public enum AcceptableContentType: AcceptableProtocol { + case json + case text + case binary + case other(String) + public static var defaultValues: [Operations.getStats.Input.Headers.AcceptableContentType] { + [.json, .text, .binary] + } + public init?(rawValue: String) { + let lowercasedRawValue = rawValue.lowercased() + switch lowercasedRawValue { + case "application/json": + self = .json + case "text/plain": + self = .text + case "application/octet-stream": + self = .binary + default: + self = .other(rawValue) + } + } + public var rawValue: String { + switch self { + case .json: + return "application/json" + case .text: + return "text/plain" + case .binary: + return "application/octet-stream" + case .other(let value): + return value + } + } + } + public typealias Accept = [AcceptHeaderContentType] + public var accept: Accept /// Creates a new `Headers`. - public init() {} + public init( + accept: Accept = Accept.Element.defaultValues + ) { + self.accept = accept + } } public var headers: Operations.getStats.Input.Headers public struct Cookies: Sendable, Equatable, Hashable { diff --git a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift index fdc87eb1..fe866c50 100644 --- a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift +++ b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift @@ -37,6 +37,12 @@ final class Test_Client: XCTestCase { XCTAssertEqual(operationID, "getStats") XCTAssertEqual(request.path, "/pets/stats") XCTAssertEqual(request.method, .get) + XCTAssertEqual( + request.headerFields, + [ + .init(name: "accept", value: "application/json, text/plain, application/octet-stream") + ] + ) XCTAssertNil(request.body) return .init( statusCode: 200, @@ -61,6 +67,48 @@ final class Test_Client: XCTestCase { } } + func testGetStats_200_text_customAccept() async throws { + transport = .init { request, baseURL, operationID in + XCTAssertEqual(operationID, "getStats") + XCTAssertEqual(request.path, "/pets/stats") + XCTAssertEqual(request.method, .get) + XCTAssertEqual( + request.headerFields, + [ + .init(name: "accept", value: "application/json; q=0.800, text/plain") + ] + ) + XCTAssertNil(request.body) + return .init( + statusCode: 200, + headers: [ + .init(name: "content-type", value: "text/plain") + ], + encodedBody: #""" + count is 1 + """# + ) + } + let response = try await client.getStats( + .init( + headers: .init(accept: [ + .init(quality: 0.8, contentType: .json), + .init(contentType: .text), + ]) + ) + ) + guard case let .ok(value) = response else { + XCTFail("Unexpected response: \(response)") + return + } + switch value.body { + case .text(let stats): + XCTAssertEqual(stats, "count is 1") + default: + XCTFail("Unexpected content type") + } + } + func testGetStats_200_binary() async throws { transport = .init { request, baseURL, operationID in XCTAssertEqual(operationID, "getStats") diff --git a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift index 2e7c2be7..8bb3c97e 100644 --- a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift +++ b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift @@ -60,6 +60,44 @@ final class Test_Server: XCTestCase { ) } + func testGetStats_200_text_customAccept() async throws { + client = .init( + getStatsBlock: { input in + XCTAssertEqual( + input.headers.accept, + [ + .init(quality: 0.8, contentType: .json), + .init(contentType: .text), + ] + ) + return .ok(.init(body: .text("count is 1"))) + } + ) + let response = try await server.getStats( + .init( + path: "/api/pets/stats", + method: .patch, + headerFields: [ + .init(name: "accept", value: "application/json; q=0.8, text/plain") + ] + ), + .init() + ) + XCTAssertEqual(response.statusCode, 200) + XCTAssertEqual( + response.headerFields, + [ + .init(name: "content-type", value: "text/plain") + ] + ) + XCTAssertEqualStringifiedData( + response.body, + #""" + count is 1 + """# + ) + } + func testGetStats_200_binary() async throws { client = .init( getStatsBlock: { input in From d051353bdb6fdae3fa44c0fc2b29d3966923d9ef Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 15 Aug 2023 11:33:24 +0200 Subject: [PATCH 2/7] PR feedback --- .../Petstore_FF_MultipleContentTypes/Client.swift | 2 +- .../Petstore_FF_MultipleContentTypes/Types.swift | 13 ++++++------- .../Test_Client.swift | 6 +++--- .../Test_Server.swift | 6 +++--- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Client.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Client.swift index 4564c820..dfe75b40 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Client.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Client.swift @@ -264,7 +264,7 @@ public struct Client: APIProtocol { suppressMutabilityWarning(&request) try converter.setAcceptHeader( in: &request.headerFields, - value: input.headers.accept + contentTypes: input.headers.accept ) return request }, diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Types.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Types.swift index fede0c19..8b87b38a 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Types.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Types.swift @@ -1070,11 +1070,11 @@ public enum Operations { public struct Headers: Sendable, Equatable, Hashable { public enum AcceptableContentType: AcceptableProtocol { case json - case text + case plainText case binary case other(String) public static var defaultValues: [Operations.getStats.Input.Headers.AcceptableContentType] { - [.json, .text, .binary] + [.json, .plainText, .binary] } public init?(rawValue: String) { let lowercasedRawValue = rawValue.lowercased() @@ -1082,7 +1082,7 @@ public enum Operations { case "application/json": self = .json case "text/plain": - self = .text + self = .plainText case "application/octet-stream": self = .binary default: @@ -1093,7 +1093,7 @@ public enum Operations { switch self { case .json: return "application/json" - case .text: + case .plainText: return "text/plain" case .binary: return "application/octet-stream" @@ -1102,11 +1102,10 @@ public enum Operations { } } } - public typealias Accept = [AcceptHeaderContentType] - public var accept: Accept + public var accept: [AcceptHeaderContentType] /// Creates a new `Headers`. public init( - accept: Accept = Accept.Element.defaultValues + accept: [AcceptHeaderContentType] = .defaultValues() ) { self.accept = accept } diff --git a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift index d4942f7a..def34f1f 100644 --- a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift +++ b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift @@ -92,8 +92,8 @@ final class Test_Client: XCTestCase { let response = try await client.getStats( .init( headers: .init(accept: [ - .init(quality: 0.8, contentType: .json), - .init(contentType: .text), + .init(contentType: .json, quality: 0.8), + .init(contentType: .plainText), ]) ) ) @@ -102,7 +102,7 @@ final class Test_Client: XCTestCase { return } switch value.body { - case .text(let stats): + case .plainText(let stats): XCTAssertEqual(stats, "count is 1") default: XCTFail("Unexpected content type") diff --git a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift index 9c690729..d8ee53fc 100644 --- a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift +++ b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift @@ -66,11 +66,11 @@ final class Test_Server: XCTestCase { XCTAssertEqual( input.headers.accept, [ - .init(quality: 0.8, contentType: .json), - .init(contentType: .text), + .init(contentType: .json, quality: 0.8), + .init(contentType: .plainText), ] ) - return .ok(.init(body: .text("count is 1"))) + return .ok(.init(body: .plainText("count is 1"))) } ) let response = try await server.getStats( From 6ad16302ae5b5fd255966b5e069a9e5c2e6c1721 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 15 Aug 2023 15:59:35 +0200 Subject: [PATCH 3/7] Prototype working end-to-end --- .../translateClientMethod.swift | 31 ++- .../translateRawRepresentableEnum.swift | 207 +++++++++++++++++ .../translateStringEnum.swift | 181 +-------------- .../Translator/CommonTypes/Constants.swift | 23 ++ .../Operations/OperationDescription.swift | 21 ++ .../translateServerMethod.swift | 40 +++- .../Translator/TypeAssignment/TypeName.swift | 7 +- .../Translator/TypeAssignment/TypeUsage.swift | 15 +- .../TypesTranslator/translateOperations.swift | 71 +++++- .../ReferenceSources/Petstore/Client.swift | 25 +-- .../ReferenceSources/Petstore/Server.swift | 18 +- .../ReferenceSources/Petstore/Types.swift | 166 +++++++++++++- .../Client.swift | 22 +- .../Server.swift | 18 +- .../Types.swift | 209 ++++++++++++++---- .../SnippetBasedReferenceTests.swift | 2 +- Tests/PetstoreConsumerTests/Test_Client.swift | 3 + .../Test_Client.swift | 41 ++++ .../Test_Server.swift | 36 +++ 19 files changed, 828 insertions(+), 308 deletions(-) create mode 100644 Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawRepresentableEnum.swift diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift index be92013c..c9508bca 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift @@ -71,23 +71,20 @@ extension ClientFileTranslator { for: description ) if !acceptContent.isEmpty { - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept - let acceptValue = - acceptContent - .map(\.headerValueForValidation) - .joined(separator: ", ") - let addAcceptHeaderExpr: Expression = .try( - .identifier("converter").dot("setHeaderFieldAsText") - .call([ - .init( - label: "in", - expression: .inOut(.identifier("request").dot("headerFields")) - ), - .init(label: "name", expression: "accept"), - .init(label: "value", expression: .literal(acceptValue)), - ]) - ) - requestExprs.append(addAcceptHeaderExpr) + let setAcceptHeaderExpr: Expression = + .identifier("converter") + .dot("setAcceptHeader") + .call([ + .init( + label: "in", + expression: .inOut(.identifier("request").dot("headerFields")) + ), + .init( + label: "contentTypes", + expression: .identifier("input").dot("headers").dot("accept") + ), + ]) + requestExprs.append(setAcceptHeaderExpr) } if let requestBody = try typedRequestBody(in: description) { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawRepresentableEnum.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawRepresentableEnum.swift new file mode 100644 index 00000000..3ec263eb --- /dev/null +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawRepresentableEnum.swift @@ -0,0 +1,207 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import OpenAPIKit30 + +extension FileTranslator { + + /// Returns a declaration of the specified raw representable enum. + /// - Parameters: + /// - typeName: The name of the type to give to the declared enum. + /// - conformances: The list of types the enum conforms to. + /// - userDescription: The contents of the documentation comment. + /// - cases: The list of cases to generate. + /// - unknownCaseName: The name of the extra unknown case that preserves + /// the string value that doesn't fit any of the cases. + /// - unknownCaseDescription: The contents of the documentation comment + /// for the unknown case. + /// - customSwitchedExpression: A closure + func translateRawRepresentableEnum( + typeName: TypeName, + conformances: [String], + userDescription: String?, + cases: [(caseName: String, rawValue: String)], + unknownCaseName: String, + unknownCaseDescription: String?, + customSwitchedExpression: (Expression) -> Expression = { $0 } + ) throws -> Declaration { + let knownCases: [Declaration] = cases + .map { caseName, _ in + .enumCase( + name: caseName, + kind: .nameOnly + ) + } + let undocumentedCase: Declaration = .commentable( + unknownCaseDescription.flatMap { .doc($0) }, + .enumCase( + name: unknownCaseName, + kind: .nameWithAssociatedValues([ + .init(type: "String") + ]) + ) + ) + let rawRepresentableInitializer: Declaration + do { + let knownCases: [SwitchCaseDescription] = cases.map { caseName, rawValue in + .init( + kind: .case(.literal(rawValue)), + body: [ + .expression( + .assignment( + Expression + .identifier("self") + .equals( + .dot(caseName) + ) + ) + ) + ] + ) + } + let unknownCase = SwitchCaseDescription( + kind: .default, + body: [ + .expression( + .assignment( + Expression + .identifier("self") + .equals( + .functionCall( + calledExpression: .dot( + unknownCaseName + ), + arguments: [ + .identifier("rawValue") + ] + ) + ) + ) + ) + ] + ) + rawRepresentableInitializer = .function( + .init( + accessModifier: config.access, + kind: .initializer(failable: true), + parameters: [ + .init(label: "rawValue", type: "String") + ], + body: [ + .expression( + .switch( + switchedExpression: customSwitchedExpression( + .identifier("rawValue") + ), + cases: knownCases + [unknownCase] + ) + ) + ] + ) + ) + } + + let rawValueGetter: Declaration + do { + let knownCases: [SwitchCaseDescription] = cases.map { caseName, rawValue in + .init( + kind: .case(.dot(caseName)), + body: [ + .expression( + .return(.literal(rawValue)) + ) + ] + ) + } + let unknownCase = SwitchCaseDescription( + kind: .case( + .valueBinding( + kind: .let, + value: .init( + calledExpression: .dot( + unknownCaseName + ), + arguments: [ + .identifier("string") + ] + ) + ) + ), + body: [ + .expression( + .return(.identifier("string")) + ) + ] + ) + + let variableDescription = VariableDescription( + accessModifier: config.access, + kind: .var, + left: "rawValue", + type: "String", + body: [ + .expression( + .switch( + switchedExpression: .identifier("self"), + cases: [unknownCase] + knownCases + ) + ) + ] + ) + + rawValueGetter = .variable( + variableDescription + ) + } + + let allCasesGetter: Declaration + do { + let caseExpressions: [Expression] = cases.map { caseName, _ in + .memberAccess(.init(right: caseName)) + } + allCasesGetter = .variable( + .init( + accessModifier: config.access, + isStatic: true, + kind: .var, + left: "allCases", + type: "[Self]", + body: [ + .expression(.literal(.array(caseExpressions))) + ] + ) + ) + } + + let enumDescription = EnumDescription( + isFrozen: true, + accessModifier: config.access, + name: typeName.shortSwiftName, + conformances: conformances, + members: knownCases + [ + undocumentedCase, + rawRepresentableInitializer, + rawValueGetter, + allCasesGetter, + ] + ) + + let comment: Comment? = + typeName + .docCommentWithUserDescription(userDescription) + return .commentable( + comment, + .enum(enumDescription) + ) + } +} diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift index cd0ac7eb..9657dd3d 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift @@ -18,7 +18,7 @@ extension FileTranslator { /// Returns a declaration of the specified string-based enum schema. /// - Parameters: /// - typeName: The name of the type to give to the declared enum. - /// - openAPIDescription: A user-specified description from the OpenAPI + /// - userDescription: A user-specified description from the OpenAPI /// document. /// - isNullable: Whether the enum schema is nullable. /// - allowedValues: The enumerated allowed values. @@ -28,7 +28,6 @@ extension FileTranslator { isNullable: Bool, allowedValues: [AnyCodable] ) throws -> Declaration { - let rawValues = try allowedValues.map(\.value) .map { anyValue in // In nullable enum schemas, empty strings are parsed as Void. @@ -42,177 +41,17 @@ extension FileTranslator { } return string } - - let knownCases: [Declaration] = - rawValues - .map { rawValue in - let caseName = swiftSafeName(for: rawValue) - return .enumCase( - name: caseName, - kind: .nameOnly - ) - } - let undocumentedCase: Declaration = .commentable( - .doc("Parsed a raw value that was not defined in the OpenAPI document."), - .enumCase( - name: Constants.StringEnum.undocumentedCaseName, - kind: .nameWithAssociatedValues([ - .init(type: "String") - ]) - ) - ) - - let rawRepresentableInitializer: Declaration - do { - let knownCases: [SwitchCaseDescription] = rawValues.map { rawValue in - .init( - kind: .case(.literal(rawValue)), - body: [ - .expression( - .assignment( - Expression - .identifier("self") - .equals( - .dot(swiftSafeName(for: rawValue)) - ) - ) - ) - ] - ) - } - let unknownCase = SwitchCaseDescription( - kind: .default, - body: [ - .expression( - .assignment( - Expression - .identifier("self") - .equals( - .functionCall( - calledExpression: .dot( - Constants - .StringEnum - .undocumentedCaseName - ), - arguments: [ - .identifier("rawValue") - ] - ) - ) - ) - ) - ] - ) - rawRepresentableInitializer = .function( - .init( - accessModifier: config.access, - kind: .initializer(failable: true), - parameters: [ - .init(label: "rawValue", type: "String") - ], - body: [ - .expression( - .switch( - switchedExpression: .identifier("rawValue"), - cases: knownCases + [unknownCase] - ) - ) - ] - ) - ) + let cases = rawValues.map { rawValue in + let caseName = swiftSafeName(for: rawValue) + return (caseName, rawValue) } - - let rawValueGetter: Declaration - do { - let knownCases: [SwitchCaseDescription] = rawValues.map { rawValue in - .init( - kind: .case(.dot(swiftSafeName(for: rawValue))), - body: [ - .expression( - .return(.literal(rawValue)) - ) - ] - ) - } - let unknownCase = SwitchCaseDescription( - kind: .case( - .valueBinding( - kind: .let, - value: .init( - calledExpression: .dot( - Constants.StringEnum.undocumentedCaseName - ), - arguments: [ - .identifier("string") - ] - ) - ) - ), - body: [ - .expression( - .return(.identifier("string")) - ) - ] - ) - - let variableDescription = VariableDescription( - accessModifier: config.access, - kind: .var, - left: "rawValue", - type: "String", - body: [ - .expression( - .switch( - switchedExpression: .identifier("self"), - cases: [unknownCase] + knownCases - ) - ) - ] - ) - - rawValueGetter = .variable( - variableDescription - ) - } - - let allCasesGetter: Declaration - do { - let caseExpressions: [Expression] = rawValues.map { rawValue in - .memberAccess(.init(right: swiftSafeName(for: rawValue))) - } - allCasesGetter = .variable( - .init( - accessModifier: config.access, - isStatic: true, - kind: .var, - left: "allCases", - type: typeName.asUsage.asArray.shortSwiftName, - body: [ - .expression(.literal(.array(caseExpressions))) - ] - ) - ) - } - - let enumDescription = EnumDescription( - isFrozen: true, - accessModifier: config.access, - name: typeName.shortSwiftName, + return try translateRawRepresentableEnum( + typeName: typeName, conformances: Constants.StringEnum.conformances, - members: knownCases + [ - undocumentedCase, - rawRepresentableInitializer, - rawValueGetter, - allCasesGetter, - ] - ) - - let comment: Comment? = - typeName - .docCommentWithUserDescription(userDescription) - return .commentable( - comment, - .enum(enumDescription) + userDescription: userDescription, + cases: cases, + unknownCaseName: Constants.StringEnum.undocumentedCaseName, + unknownCaseDescription: "Parsed a raw value that was not defined in the OpenAPI document." ) } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index 5809bae6..50ad9827 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -279,6 +279,29 @@ enum Constants { /// The name of the undocumented payload type. static let undocumentedCaseAssociatedValueTypeName = "UndocumentedPayload" } + + /// Constants related to every OpenAPI operation's AcceptableContentType + /// type. + enum AcceptableContentType { + + /// The name of the type. + static let typeName: String = "AcceptableContentType" + + /// The types that the AcceptableContentType type conforms to. + static let conformances: [String] = [ + "AcceptableProtocol", + ] + + /// The name of the variable on Input given to the acceptable + /// content types array. + static let variableName: String = "accept" + + /// The name of the wrapper type. + static let headerTypeName: String = "AcceptHeaderContentType" + + /// The name of the "other" case name. + static let otherCaseName: String = "other" + } } /// Constants related to the Components namespace. diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift b/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift index b21f63fc..24d9b596 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift @@ -143,6 +143,27 @@ extension OperationDescription { jsonComponent: nil ) } + + /// Returns the name of the AcceptableContentType type. + var acceptableContentTypeName: TypeName { + operationNamespace.appending( + swiftComponent: Constants.Operation.AcceptableContentType.typeName, + + // intentionally nil, we'll append the specific params etc + // with their valid JSON key path if nested further + jsonComponent: nil + ) + } + + /// Returns the name of the array of wrapped AcceptableContentType type. + var acceptableArrayName: TypeUsage { + acceptableContentTypeName + .asUsage + .asWrapped(in: .runtime( + Constants.Operation.AcceptableContentType.headerTypeName + )) + .asArray + } /// Merged parameters from both the path item level and the operation level. /// If duplicate parameters exist, only the parameters from the operation level are preserved. diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift index c3afd31b..62f625cd 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift @@ -28,7 +28,8 @@ extension ServerFileTranslator { func locationSpecificInputDecl( locatedIn location: OpenAPI.Parameter.Context.Location, - fromParameters parameters: [UnresolvedParameter] + fromParameters parameters: [UnresolvedParameter], + extraArguments: [FunctionArgumentDescription] ) throws -> Declaration { let variableName = location.shortVariableName let type = location.typeName(in: inputTypeName) @@ -45,29 +46,56 @@ extension ServerFileTranslator { ) } .compactMap(translateParameterInServer(_:)) + + extraArguments ) ) } + let extraHeaderArguments: [FunctionArgumentDescription] + let acceptableContentTypes = try acceptHeaderContentTypes(for: operation) + if acceptableContentTypes.isEmpty { + extraHeaderArguments = [] + } else { + extraHeaderArguments = [ + .init( + label: Constants.Operation.AcceptableContentType.variableName, + expression: .try( + .identifier("converter") + .dot("extractAcceptHeaderIfPresent") + .call([ + .init( + label: "in", + expression: .identifier("request").dot("headerFields") + ), + ]) + ) + ) + ] + } + var inputMemberCodeBlocks = try [ ( .path, - operation.allPathParameters + operation.allPathParameters, + [] ), ( .query, - operation.allQueryParameters + operation.allQueryParameters, + [] ), ( .header, - operation.allHeaderParameters + operation.allHeaderParameters, + extraHeaderArguments ), ( .cookie, - operation.allCookieParameters + operation.allCookieParameters, + [] ), ] - .map(locationSpecificInputDecl(locatedIn:fromParameters:)) + .map(locationSpecificInputDecl) .map(CodeBlock.declaration) let requestBodyExpr: Expression diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeName.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeName.swift index c1505b21..a7e709ee 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeName.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeName.swift @@ -91,9 +91,12 @@ struct TypeName: Equatable { /// /// For example: `#/components/schemas/Foo`. /// - Returns: A string representation; nil if the type name has no - /// JSON path components. + /// JSON path components or if the last JSON path component is nil. var fullyQualifiedJSONPath: String? { - jsonKeyPathComponents?.joined(separator: "/") + guard components.last?.json != nil else { + return nil + } + return jsonKeyPathComponents?.joined(separator: "/") } /// A string representation of the last path component of the JSON path. diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeUsage.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeUsage.swift index 99360602..002498e1 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeUsage.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeUsage.swift @@ -59,8 +59,13 @@ struct TypeUsage { /// A dictionary value wrapper for the underlying type. /// - /// For examplle: `Wrapped` becomes `[String: Wrapped]`. + /// For example: `Wrapped` becomes `[String: Wrapped]`. case dictionaryValue + + /// A generic type wrapper for the underlying type. + /// + /// For example, `Wrapped` becomes `Wrapper`. + case generic(wrapper: TypeName) } /// The type usage applied to the underlying type. @@ -132,6 +137,8 @@ extension TypeUsage { return "[" + component + "]" case .dictionaryValue: return "[String: " + component + "]" + case .generic(wrapper: let wrapper): + return "\(wrapper.fullyQualifiedSwiftName)<" + component + ">" } } @@ -193,6 +200,12 @@ extension TypeUsage { var asDictionaryValue: Self { TypeUsage(wrapped: .usage(self), usage: .dictionaryValue) } + + /// A type usage created by wrapping the current type usage inside the + /// wrapper type, where the wrapper type is generic over the current type. + func asWrapped(in wrapper: TypeName) -> Self { + TypeUsage(wrapped: .usage(self), usage: .generic(wrapper: wrapper)) + } } extension TypeName { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateOperations.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateOperations.swift index 88521b64..c93073c7 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateOperations.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateOperations.swift @@ -26,16 +26,18 @@ extension TypesFileTranslator { func propertyBlueprintForNamespacedStruct( locatedIn location: OpenAPI.Parameter.Context.Location, - withPropertiesFrom parameters: [UnresolvedParameter] + withPropertiesFrom parameters: [UnresolvedParameter], + extraProperties: [PropertyBlueprint] = [] ) throws -> PropertyBlueprint { let inputTypeName = description.inputTypeName let structTypeName = location.typeName(in: inputTypeName) - let structProperties: [PropertyBlueprint] = try parameters.compactMap { parameter in - try parseParameterAsProperty( - for: parameter, - inParent: inputTypeName - ) - } + let structProperties: [PropertyBlueprint] = try parameters + .compactMap { parameter in + try parseParameterAsProperty( + for: parameter, + inParent: inputTypeName + ) + } + extraProperties let structDecl: Declaration = translateStructBlueprint( .init( comment: nil, @@ -51,7 +53,7 @@ extension TypesFileTranslator { // If inner struct is being used as an optional property, its default value in the // initializer of the outer struct is `nil`. defaultValue = .nil - } else if structProperties.allSatisfy(\.typeUsage.isOptional) { + } else if structProperties.allSatisfy({ $0.defaultValue != nil }) { // If inner struct is being used as an non-optional property, but it only has // optional inner properties, its default value in the initializer of the outer // struct is `.init()`. @@ -79,6 +81,21 @@ extension TypesFileTranslator { inParent: inputTypeName ) + let acceptableContentTypes = try acceptHeaderContentTypes(for: description) + let extraHeaderProperties: [PropertyBlueprint] + if acceptableContentTypes.isEmpty { + extraHeaderProperties = [] + } else { + let acceptPropertyBlueprint = PropertyBlueprint( + comment: nil, + originalName: Constants.Operation.AcceptableContentType.variableName, + typeUsage: description.acceptableArrayName, + default: .expression(.dot("defaultValues").call([])), + asSwiftSafeName: swiftSafeName + ) + extraHeaderProperties = [acceptPropertyBlueprint] + } + let inputStructDecl = translateStructBlueprint( .init( comment: nil, @@ -96,7 +113,8 @@ extension TypesFileTranslator { ), try propertyBlueprintForNamespacedStruct( locatedIn: .header, - withPropertiesFrom: description.allHeaderParameters + withPropertiesFrom: description.allHeaderParameters, + extraProperties: extraHeaderProperties ), try propertyBlueprintForNamespacedStruct( locatedIn: .cookie, @@ -170,6 +188,38 @@ extension TypesFileTranslator { ) return enumDecl } + + /// Returns a declaration of the AcceptableContentType type for the specified + /// operation. + /// - Parameter description: The OpenAPI operation. + /// - Returns: A structure declaration that represents + /// the AcceptableContentType type, or nil if no acceptable content types + /// were specified. + func translateOperationAcceptableContentType( + _ description: OperationDescription + ) throws -> Declaration? { + let acceptableContentTypeName = description.acceptableContentTypeName + let contentTypes = try acceptHeaderContentTypes(for: description) + guard !contentTypes.isEmpty else { + return nil + } + let cases: [(caseName: String, rawValue: String)] = contentTypes + .map { contentType in + (contentSwiftName(contentType), contentType.lowercasedTypeAndSubtype) + } + return try translateRawRepresentableEnum( + typeName: acceptableContentTypeName, + conformances: Constants.Operation.AcceptableContentType.conformances, + userDescription: nil, + cases: cases, + unknownCaseName: Constants.Operation.AcceptableContentType.otherCaseName, + unknownCaseDescription: nil, + customSwitchedExpression: { expr in + // Lowercase the raw value before switching over. + expr.dot("lowercased").call([]) + } + ) + } /// Returns a declaration of the namespace type of the specified operation. /// @@ -195,6 +245,7 @@ extension TypesFileTranslator { let inputDecl: Declaration = try translateOperationInput(operation) let outputDecl: Declaration = try translateOperationOutput(operation) + let acceptDecl: Declaration? = try translateOperationAcceptableContentType(operation) let operationNamespace = operation.operationNamespace let operationEnumDecl = Declaration.commentable( @@ -207,7 +258,7 @@ extension TypesFileTranslator { idPropertyDecl, inputDecl, outputDecl, - ] + ] + (acceptDecl.flatMap { [$0] } ?? []) ) ) ) diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift index 7ac7b329..a14c2c8a 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift @@ -83,10 +83,9 @@ public struct Client: APIProtocol { name: "since", value: input.query.since ) - try converter.setHeaderFieldAsText( + converter.setAcceptHeader( in: &request.headerFields, - name: "accept", - value: "application/json" + contentTypes: input.headers.accept ) return request }, @@ -171,10 +170,9 @@ public struct Client: APIProtocol { name: "X-Extra-Arguments", value: input.headers.X_Extra_Arguments ) - try converter.setHeaderFieldAsText( + converter.setAcceptHeader( in: &request.headerFields, - name: "accept", - value: "application/json" + contentTypes: input.headers.accept ) switch input.body { case let .json(value): @@ -262,10 +260,9 @@ public struct Client: APIProtocol { ) var request: OpenAPIRuntime.Request = .init(path: path, method: .get) suppressMutabilityWarning(&request) - try converter.setHeaderFieldAsText( + converter.setAcceptHeader( in: &request.headerFields, - name: "accept", - value: "application/json" + contentTypes: input.headers.accept ) return request }, @@ -373,10 +370,9 @@ public struct Client: APIProtocol { ) var request: OpenAPIRuntime.Request = .init(path: path, method: .patch) suppressMutabilityWarning(&request) - try converter.setHeaderFieldAsText( + converter.setAcceptHeader( in: &request.headerFields, - name: "accept", - value: "application/json" + contentTypes: input.headers.accept ) switch input.body { case .none: request.body = nil @@ -437,10 +433,9 @@ public struct Client: APIProtocol { ) var request: OpenAPIRuntime.Request = .init(path: path, method: .put) suppressMutabilityWarning(&request) - try converter.setHeaderFieldAsText( + converter.setAcceptHeader( in: &request.headerFields, - name: "accept", - value: "application/octet-stream, application/json, text/plain" + contentTypes: input.headers.accept ) switch input.body { case let .binary(value): diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift index b53efce2..e213aecf 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift @@ -119,7 +119,8 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { in: request.headerFields, name: "My-Request-UUID", as: Swift.String.self - ) + ), + accept: try converter.extractAcceptHeaderIfPresent(in: request.headerFields) ) let cookies: Operations.listPets.Input.Cookies = .init() return Operations.listPets.Input( @@ -197,7 +198,8 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { in: request.headerFields, name: "X-Extra-Arguments", as: Components.Schemas.CodeError.self - ) + ), + accept: try converter.extractAcceptHeaderIfPresent(in: request.headerFields) ) let cookies: Operations.createPet.Input.Cookies = .init() let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) @@ -285,7 +287,9 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { using: { APIHandler.getStats($0) }, deserializer: { request, metadata in let path: Operations.getStats.Input.Path = .init() let query: Operations.getStats.Input.Query = .init() - let headers: Operations.getStats.Input.Headers = .init() + let headers: Operations.getStats.Input.Headers = .init( + accept: try converter.extractAcceptHeaderIfPresent(in: request.headerFields) + ) let cookies: Operations.getStats.Input.Cookies = .init() return Operations.getStats.Input( path: path, @@ -420,7 +424,9 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { ) ) let query: Operations.updatePet.Input.Query = .init() - let headers: Operations.updatePet.Input.Headers = .init() + let headers: Operations.updatePet.Input.Headers = .init( + accept: try converter.extractAcceptHeaderIfPresent(in: request.headerFields) + ) let cookies: Operations.updatePet.Input.Cookies = .init() let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) let body: Components.RequestBodies.UpdatePetRequest? @@ -496,7 +502,9 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { ) ) let query: Operations.uploadAvatarForPet.Input.Query = .init() - let headers: Operations.uploadAvatarForPet.Input.Headers = .init() + let headers: Operations.uploadAvatarForPet.Input.Headers = .init( + accept: try converter.extractAcceptHeaderIfPresent(in: request.headerFields) + ) let cookies: Operations.uploadAvatarForPet.Input.Cookies = .init() let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) let body: Operations.uploadAvatarForPet.Input.Body diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift index aaedc0cd..e8b1566d 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift @@ -132,7 +132,7 @@ public enum Components { case ._public: return "public" } } - public static var allCases: [PetKind] { + public static var allCases: [Self] { [.cat, .dog, .ELEPHANT, .BIG_ELEPHANT_1, ._nake, ._public] } } @@ -250,7 +250,7 @@ public enum Components { case .weekly: return "weekly" } } - public static var allCases: [schedulePayload] { [.hourly, .daily, .weekly] } + public static var allCases: [Self] { [.hourly, .daily, .weekly] } } /// - Remark: Generated from `#/components/schemas/PetFeeding/schedule`. public var schedule: Components.Schemas.PetFeeding.schedulePayload? @@ -766,7 +766,7 @@ public enum Operations { case ._empty: return "" } } - public static var allCases: [habitatPayload] { [.water, .land, .air, ._empty] } + public static var allCases: [Self] { [.water, .land, .air, ._empty] } } public var habitat: Operations.listPets.Input.Query.habitatPayload? /// - Remark: Generated from `#/paths/pets/GET/query/feedsPayload`. @@ -795,9 +795,7 @@ public enum Operations { case .herbivore: return "herbivore" } } - public static var allCases: [feedsPayloadPayload] { - [.omnivore, .carnivore, .herbivore] - } + public static var allCases: [Self] { [.omnivore, .carnivore, .herbivore] } } /// - Remark: Generated from `#/paths/pets/GET/query/feeds`. public typealias feedsPayload = [Operations.listPets.Input.Query @@ -826,12 +824,23 @@ public enum Operations { public var query: Operations.listPets.Input.Query public struct Headers: Sendable, Equatable, Hashable { public var My_Request_UUID: Swift.String? + public var accept: + [OpenAPIRuntime.AcceptHeaderContentType< + Operations.listPets.AcceptableContentType + >] /// Creates a new `Headers`. /// /// - Parameters: /// - My_Request_UUID: - public init(My_Request_UUID: Swift.String? = nil) { + /// - accept: + public init( + My_Request_UUID: Swift.String? = nil, + accept: [OpenAPIRuntime.AcceptHeaderContentType< + Operations.listPets.AcceptableContentType + >] = .defaultValues() + ) { self.My_Request_UUID = My_Request_UUID + self.accept = accept } } public var headers: Operations.listPets.Input.Headers @@ -940,6 +949,23 @@ public enum Operations { /// HTTP response code: `default`. case `default`(statusCode: Int, Operations.listPets.Output.Default) } + @frozen public enum AcceptableContentType: AcceptableProtocol { + case json + case other(String) + public init?(rawValue: String) { + switch rawValue.lowercased() { + case "application/json": self = .json + default: self = .other(rawValue) + } + } + public var rawValue: String { + switch self { + case let .other(string): return string + case .json: return "application/json" + } + } + public static var allCases: [Self] { [.json] } + } } /// Create a pet /// @@ -960,12 +986,23 @@ public enum Operations { public var query: Operations.createPet.Input.Query public struct Headers: Sendable, Equatable, Hashable { public var X_Extra_Arguments: Components.Schemas.CodeError? + public var accept: + [OpenAPIRuntime.AcceptHeaderContentType< + Operations.createPet.AcceptableContentType + >] /// Creates a new `Headers`. /// /// - Parameters: /// - X_Extra_Arguments: - public init(X_Extra_Arguments: Components.Schemas.CodeError? = nil) { + /// - accept: + public init( + X_Extra_Arguments: Components.Schemas.CodeError? = nil, + accept: [OpenAPIRuntime.AcceptHeaderContentType< + Operations.createPet.AcceptableContentType + >] = .defaultValues() + ) { self.X_Extra_Arguments = X_Extra_Arguments + self.accept = accept } } public var headers: Operations.createPet.Input.Headers @@ -1049,6 +1086,23 @@ public enum Operations { /// A response with a code that is not documented in the OpenAPI document. case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) } + @frozen public enum AcceptableContentType: AcceptableProtocol { + case json + case other(String) + public init?(rawValue: String) { + switch rawValue.lowercased() { + case "application/json": self = .json + default: self = .other(rawValue) + } + } + public var rawValue: String { + switch self { + case let .other(string): return string + case .json: return "application/json" + } + } + public static var allCases: [Self] { [.json] } + } } /// - Remark: HTTP `GET /pets/stats`. /// - Remark: Generated from `#/paths//pets/stats/get(getStats)`. @@ -1066,8 +1120,19 @@ public enum Operations { } public var query: Operations.getStats.Input.Query public struct Headers: Sendable, Equatable, Hashable { + public var accept: + [OpenAPIRuntime.AcceptHeaderContentType< + Operations.getStats.AcceptableContentType + >] /// Creates a new `Headers`. - public init() {} + /// + /// - Parameters: + /// - accept: + public init( + accept: [OpenAPIRuntime.AcceptHeaderContentType< + Operations.getStats.AcceptableContentType + >] = .defaultValues() + ) { self.accept = accept } } public var headers: Operations.getStats.Input.Headers public struct Cookies: Sendable, Equatable, Hashable { @@ -1136,6 +1201,23 @@ public enum Operations { /// A response with a code that is not documented in the OpenAPI document. case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) } + @frozen public enum AcceptableContentType: AcceptableProtocol { + case json + case other(String) + public init?(rawValue: String) { + switch rawValue.lowercased() { + case "application/json": self = .json + default: self = .other(rawValue) + } + } + public var rawValue: String { + switch self { + case let .other(string): return string + case .json: return "application/json" + } + } + public static var allCases: [Self] { [.json] } + } } /// - Remark: HTTP `POST /pets/stats`. /// - Remark: Generated from `#/paths//pets/stats/post(postStats)`. @@ -1331,8 +1413,19 @@ public enum Operations { } public var query: Operations.updatePet.Input.Query public struct Headers: Sendable, Equatable, Hashable { + public var accept: + [OpenAPIRuntime.AcceptHeaderContentType< + Operations.updatePet.AcceptableContentType + >] /// Creates a new `Headers`. - public init() {} + /// + /// - Parameters: + /// - accept: + public init( + accept: [OpenAPIRuntime.AcceptHeaderContentType< + Operations.updatePet.AcceptableContentType + >] = .defaultValues() + ) { self.accept = accept } } public var headers: Operations.updatePet.Input.Headers public struct Cookies: Sendable, Equatable, Hashable { @@ -1440,6 +1533,23 @@ public enum Operations { /// A response with a code that is not documented in the OpenAPI document. case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) } + @frozen public enum AcceptableContentType: AcceptableProtocol { + case json + case other(String) + public init?(rawValue: String) { + switch rawValue.lowercased() { + case "application/json": self = .json + default: self = .other(rawValue) + } + } + public var rawValue: String { + switch self { + case let .other(string): return string + case .json: return "application/json" + } + } + public static var allCases: [Self] { [.json] } + } } /// Upload an avatar /// @@ -1463,8 +1573,19 @@ public enum Operations { } public var query: Operations.uploadAvatarForPet.Input.Query public struct Headers: Sendable, Equatable, Hashable { + public var accept: + [OpenAPIRuntime.AcceptHeaderContentType< + Operations.uploadAvatarForPet.AcceptableContentType + >] /// Creates a new `Headers`. - public init() {} + /// + /// - Parameters: + /// - accept: + public init( + accept: [OpenAPIRuntime.AcceptHeaderContentType< + Operations.uploadAvatarForPet.AcceptableContentType + >] = .defaultValues() + ) { self.accept = accept } } public var headers: Operations.uploadAvatarForPet.Input.Headers public struct Cookies: Sendable, Equatable, Hashable { @@ -1593,5 +1714,28 @@ public enum Operations { /// A response with a code that is not documented in the OpenAPI document. case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) } + @frozen public enum AcceptableContentType: AcceptableProtocol { + case binary + case json + case text + case other(String) + public init?(rawValue: String) { + switch rawValue.lowercased() { + case "application/octet-stream": self = .binary + case "application/json": self = .json + case "text/plain": self = .text + default: self = .other(rawValue) + } + } + public var rawValue: String { + switch self { + case let .other(string): return string + case .binary: return "application/octet-stream" + case .json: return "application/json" + case .text: return "text/plain" + } + } + public static var allCases: [Self] { [.binary, .json, .text] } + } } } diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Client.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Client.swift index dfe75b40..256b9983 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Client.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Client.swift @@ -83,10 +83,9 @@ public struct Client: APIProtocol { name: "since", value: input.query.since ) - try converter.setHeaderFieldAsText( + converter.setAcceptHeader( in: &request.headerFields, - name: "accept", - value: "application/json" + contentTypes: input.headers.accept ) return request }, @@ -171,10 +170,9 @@ public struct Client: APIProtocol { name: "X-Extra-Arguments", value: input.headers.X_hyphen_Extra_hyphen_Arguments ) - try converter.setHeaderFieldAsText( + converter.setAcceptHeader( in: &request.headerFields, - name: "accept", - value: "application/json" + contentTypes: input.headers.accept ) switch input.body { case let .json(value): @@ -262,7 +260,7 @@ public struct Client: APIProtocol { ) var request: OpenAPIRuntime.Request = .init(path: path, method: .get) suppressMutabilityWarning(&request) - try converter.setAcceptHeader( + converter.setAcceptHeader( in: &request.headerFields, contentTypes: input.headers.accept ) @@ -402,10 +400,9 @@ public struct Client: APIProtocol { ) var request: OpenAPIRuntime.Request = .init(path: path, method: .patch) suppressMutabilityWarning(&request) - try converter.setHeaderFieldAsText( + converter.setAcceptHeader( in: &request.headerFields, - name: "accept", - value: "application/json" + contentTypes: input.headers.accept ) switch input.body { case .none: request.body = nil @@ -466,10 +463,9 @@ public struct Client: APIProtocol { ) var request: OpenAPIRuntime.Request = .init(path: path, method: .put) suppressMutabilityWarning(&request) - try converter.setHeaderFieldAsText( + converter.setAcceptHeader( in: &request.headerFields, - name: "accept", - value: "application/octet-stream, application/json, text/plain" + contentTypes: input.headers.accept ) switch input.body { case let .binary(value): diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Server.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Server.swift index 624fc42e..9de1ab39 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Server.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Server.swift @@ -119,7 +119,8 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { in: request.headerFields, name: "My-Request-UUID", as: Swift.String.self - ) + ), + accept: try converter.extractAcceptHeaderIfPresent(in: request.headerFields) ) let cookies: Operations.listPets.Input.Cookies = .init() return Operations.listPets.Input( @@ -197,7 +198,8 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { in: request.headerFields, name: "X-Extra-Arguments", as: Components.Schemas.CodeError.self - ) + ), + accept: try converter.extractAcceptHeaderIfPresent(in: request.headerFields) ) let cookies: Operations.createPet.Input.Cookies = .init() let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) @@ -286,9 +288,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { deserializer: { request, metadata in let path: Operations.getStats.Input.Path = .init() let query: Operations.getStats.Input.Query = .init() let headers: Operations.getStats.Input.Headers = .init( - accept: try converter.extractAcceptHeaderIfPresent( - in: request.headerFields - ) + accept: try converter.extractAcceptHeaderIfPresent(in: request.headerFields) ) let cookies: Operations.getStats.Input.Cookies = .init() return Operations.getStats.Input( @@ -462,7 +462,9 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { ) ) let query: Operations.updatePet.Input.Query = .init() - let headers: Operations.updatePet.Input.Headers = .init() + let headers: Operations.updatePet.Input.Headers = .init( + accept: try converter.extractAcceptHeaderIfPresent(in: request.headerFields) + ) let cookies: Operations.updatePet.Input.Cookies = .init() let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) let body: Components.RequestBodies.UpdatePetRequest? @@ -538,7 +540,9 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { ) ) let query: Operations.uploadAvatarForPet.Input.Query = .init() - let headers: Operations.uploadAvatarForPet.Input.Headers = .init() + let headers: Operations.uploadAvatarForPet.Input.Headers = .init( + accept: try converter.extractAcceptHeaderIfPresent(in: request.headerFields) + ) let cookies: Operations.uploadAvatarForPet.Input.Cookies = .init() let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) let body: Operations.uploadAvatarForPet.Input.Body diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Types.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Types.swift index 8b87b38a..119f74e1 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Types.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Types.swift @@ -132,7 +132,7 @@ public enum Components { case ._public: return "public" } } - public static var allCases: [PetKind] { + public static var allCases: [Self] { [.cat, .dog, .ELEPHANT, .BIG_ELEPHANT_1, ._dollar_nake, ._public] } } @@ -250,7 +250,7 @@ public enum Components { case .weekly: return "weekly" } } - public static var allCases: [schedulePayload] { [.hourly, .daily, .weekly] } + public static var allCases: [Self] { [.hourly, .daily, .weekly] } } /// - Remark: Generated from `#/components/schemas/PetFeeding/schedule`. public var schedule: Components.Schemas.PetFeeding.schedulePayload? @@ -768,7 +768,7 @@ public enum Operations { case ._empty: return "" } } - public static var allCases: [habitatPayload] { [.water, .land, .air, ._empty] } + public static var allCases: [Self] { [.water, .land, .air, ._empty] } } public var habitat: Operations.listPets.Input.Query.habitatPayload? /// - Remark: Generated from `#/paths/pets/GET/query/feedsPayload`. @@ -797,9 +797,7 @@ public enum Operations { case .herbivore: return "herbivore" } } - public static var allCases: [feedsPayloadPayload] { - [.omnivore, .carnivore, .herbivore] - } + public static var allCases: [Self] { [.omnivore, .carnivore, .herbivore] } } /// - Remark: Generated from `#/paths/pets/GET/query/feeds`. public typealias feedsPayload = [Operations.listPets.Input.Query @@ -828,12 +826,23 @@ public enum Operations { public var query: Operations.listPets.Input.Query public struct Headers: Sendable, Equatable, Hashable { public var My_hyphen_Request_hyphen_UUID: Swift.String? + public var accept: + [OpenAPIRuntime.AcceptHeaderContentType< + Operations.listPets.AcceptableContentType + >] /// Creates a new `Headers`. /// /// - Parameters: /// - My_hyphen_Request_hyphen_UUID: - public init(My_hyphen_Request_hyphen_UUID: Swift.String? = nil) { + /// - accept: + public init( + My_hyphen_Request_hyphen_UUID: Swift.String? = nil, + accept: [OpenAPIRuntime.AcceptHeaderContentType< + Operations.listPets.AcceptableContentType + >] = .defaultValues() + ) { self.My_hyphen_Request_hyphen_UUID = My_hyphen_Request_hyphen_UUID + self.accept = accept } } public var headers: Operations.listPets.Input.Headers @@ -942,6 +951,23 @@ public enum Operations { /// HTTP response code: `default`. case `default`(statusCode: Int, Operations.listPets.Output.Default) } + @frozen public enum AcceptableContentType: AcceptableProtocol { + case json + case other(String) + public init?(rawValue: String) { + switch rawValue.lowercased() { + case "application/json": self = .json + default: self = .other(rawValue) + } + } + public var rawValue: String { + switch self { + case let .other(string): return string + case .json: return "application/json" + } + } + public static var allCases: [Self] { [.json] } + } } /// Create a pet /// @@ -962,12 +988,23 @@ public enum Operations { public var query: Operations.createPet.Input.Query public struct Headers: Sendable, Equatable, Hashable { public var X_hyphen_Extra_hyphen_Arguments: Components.Schemas.CodeError? + public var accept: + [OpenAPIRuntime.AcceptHeaderContentType< + Operations.createPet.AcceptableContentType + >] /// Creates a new `Headers`. /// /// - Parameters: /// - X_hyphen_Extra_hyphen_Arguments: - public init(X_hyphen_Extra_hyphen_Arguments: Components.Schemas.CodeError? = nil) { + /// - accept: + public init( + X_hyphen_Extra_hyphen_Arguments: Components.Schemas.CodeError? = nil, + accept: [OpenAPIRuntime.AcceptHeaderContentType< + Operations.createPet.AcceptableContentType + >] = .defaultValues() + ) { self.X_hyphen_Extra_hyphen_Arguments = X_hyphen_Extra_hyphen_Arguments + self.accept = accept } } public var headers: Operations.createPet.Input.Headers @@ -1051,6 +1088,23 @@ public enum Operations { /// A response with a code that is not documented in the OpenAPI document. case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) } + @frozen public enum AcceptableContentType: AcceptableProtocol { + case json + case other(String) + public init?(rawValue: String) { + switch rawValue.lowercased() { + case "application/json": self = .json + default: self = .other(rawValue) + } + } + public var rawValue: String { + switch self { + case let .other(string): return string + case .json: return "application/json" + } + } + public static var allCases: [Self] { [.json] } + } } /// - Remark: HTTP `GET /pets/stats`. /// - Remark: Generated from `#/paths//pets/stats/get(getStats)`. @@ -1068,47 +1122,19 @@ public enum Operations { } public var query: Operations.getStats.Input.Query public struct Headers: Sendable, Equatable, Hashable { - public enum AcceptableContentType: AcceptableProtocol { - case json - case plainText - case binary - case other(String) - public static var defaultValues: [Operations.getStats.Input.Headers.AcceptableContentType] { - [.json, .plainText, .binary] - } - public init?(rawValue: String) { - let lowercasedRawValue = rawValue.lowercased() - switch lowercasedRawValue { - case "application/json": - self = .json - case "text/plain": - self = .plainText - case "application/octet-stream": - self = .binary - default: - self = .other(rawValue) - } - } - public var rawValue: String { - switch self { - case .json: - return "application/json" - case .plainText: - return "text/plain" - case .binary: - return "application/octet-stream" - case .other(let value): - return value - } - } - } - public var accept: [AcceptHeaderContentType] + public var accept: + [OpenAPIRuntime.AcceptHeaderContentType< + Operations.getStats.AcceptableContentType + >] /// Creates a new `Headers`. + /// + /// - Parameters: + /// - accept: public init( - accept: [AcceptHeaderContentType] = .defaultValues() - ) { - self.accept = accept - } + accept: [OpenAPIRuntime.AcceptHeaderContentType< + Operations.getStats.AcceptableContentType + >] = .defaultValues() + ) { self.accept = accept } } public var headers: Operations.getStats.Input.Headers public struct Cookies: Sendable, Equatable, Hashable { @@ -1179,6 +1205,29 @@ public enum Operations { /// A response with a code that is not documented in the OpenAPI document. case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) } + @frozen public enum AcceptableContentType: AcceptableProtocol { + case json + case plainText + case binary + case other(String) + public init?(rawValue: String) { + switch rawValue.lowercased() { + case "application/json": self = .json + case "text/plain": self = .plainText + case "application/octet-stream": self = .binary + default: self = .other(rawValue) + } + } + public var rawValue: String { + switch self { + case let .other(string): return string + case .json: return "application/json" + case .plainText: return "text/plain" + case .binary: return "application/octet-stream" + } + } + public static var allCases: [Self] { [.json, .plainText, .binary] } + } } /// - Remark: HTTP `POST /pets/stats`. /// - Remark: Generated from `#/paths//pets/stats/post(postStats)`. @@ -1376,8 +1425,19 @@ public enum Operations { } public var query: Operations.updatePet.Input.Query public struct Headers: Sendable, Equatable, Hashable { + public var accept: + [OpenAPIRuntime.AcceptHeaderContentType< + Operations.updatePet.AcceptableContentType + >] /// Creates a new `Headers`. - public init() {} + /// + /// - Parameters: + /// - accept: + public init( + accept: [OpenAPIRuntime.AcceptHeaderContentType< + Operations.updatePet.AcceptableContentType + >] = .defaultValues() + ) { self.accept = accept } } public var headers: Operations.updatePet.Input.Headers public struct Cookies: Sendable, Equatable, Hashable { @@ -1485,6 +1545,23 @@ public enum Operations { /// A response with a code that is not documented in the OpenAPI document. case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) } + @frozen public enum AcceptableContentType: AcceptableProtocol { + case json + case other(String) + public init?(rawValue: String) { + switch rawValue.lowercased() { + case "application/json": self = .json + default: self = .other(rawValue) + } + } + public var rawValue: String { + switch self { + case let .other(string): return string + case .json: return "application/json" + } + } + public static var allCases: [Self] { [.json] } + } } /// Upload an avatar /// @@ -1508,8 +1585,19 @@ public enum Operations { } public var query: Operations.uploadAvatarForPet.Input.Query public struct Headers: Sendable, Equatable, Hashable { + public var accept: + [OpenAPIRuntime.AcceptHeaderContentType< + Operations.uploadAvatarForPet.AcceptableContentType + >] /// Creates a new `Headers`. - public init() {} + /// + /// - Parameters: + /// - accept: + public init( + accept: [OpenAPIRuntime.AcceptHeaderContentType< + Operations.uploadAvatarForPet.AcceptableContentType + >] = .defaultValues() + ) { self.accept = accept } } public var headers: Operations.uploadAvatarForPet.Input.Headers public struct Cookies: Sendable, Equatable, Hashable { @@ -1640,5 +1728,28 @@ public enum Operations { /// A response with a code that is not documented in the OpenAPI document. case undocumented(statusCode: Int, OpenAPIRuntime.UndocumentedPayload) } + @frozen public enum AcceptableContentType: AcceptableProtocol { + case binary + case json + case plainText + case other(String) + public init?(rawValue: String) { + switch rawValue.lowercased() { + case "application/octet-stream": self = .binary + case "application/json": self = .json + case "text/plain": self = .plainText + default: self = .other(rawValue) + } + } + public var rawValue: String { + switch self { + case let .other(string): return string + case .binary: return "application/octet-stream" + case .json: return "application/json" + case .plainText: return "text/plain" + } + } + public static var allCases: [Self] { [.binary, .json, .plainText] } + } } } diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index e3bd26d5..10908906 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -584,7 +584,7 @@ final class SnippetBasedReferenceTests: XCTestCase { case ._public: return "public" } } - public static var allCases: [MyEnum] { [.one, ._empty, ._tart, ._public] } + public static var allCases: [Self] { [.one, ._empty, ._tart, ._public] } } } """ diff --git a/Tests/PetstoreConsumerTests/Test_Client.swift b/Tests/PetstoreConsumerTests/Test_Client.swift index 31e6ecfc..5c8e12ec 100644 --- a/Tests/PetstoreConsumerTests/Test_Client.swift +++ b/Tests/PetstoreConsumerTests/Test_Client.swift @@ -347,6 +347,9 @@ final class Test_Client: XCTestCase { XCTAssertEqual(operationID, "getStats") XCTAssertEqual(request.path, "/pets/stats") XCTAssertEqual(request.method, .get) + XCTAssertEqual(request.headerFields, [ + .init(name: "accept", value: "application/json") + ]) XCTAssertNil(request.body) return .init( statusCode: 200, diff --git a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift index def34f1f..1e629eb1 100644 --- a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift +++ b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift @@ -66,6 +66,47 @@ final class Test_Client: XCTestCase { XCTFail("Unexpected content type") } } + + func testGetStats_200_text_requestedSpecific() async throws { + transport = .init { request, baseURL, operationID in + XCTAssertEqual(operationID, "getStats") + XCTAssertEqual(request.path, "/pets/stats") + XCTAssertEqual(request.method, .get) + XCTAssertEqual( + request.headerFields, + [ + .init(name: "accept", value: "text/plain, application/json; q=0.500") + ] + ) + XCTAssertNil(request.body) + return .init( + statusCode: 200, + headers: [ + .init(name: "content-type", value: "text/plain") + ], + encodedBody: #""" + count is 1 + """# + ) + } + let response = try await client.getStats(.init( + headers: .init(accept: [ + .init(contentType: .plainText), + .init(contentType: .json, quality: 0.5) + ]) + )) + guard case let .ok(value) = response else { + XCTFail("Unexpected response: \(response)") + return + } + switch value.body { + case .plainText(let stats): + XCTAssertEqual(stats, "count is 1") + default: + XCTFail("Unexpected content type") + } + } + func testGetStats_200_text_customAccept() async throws { transport = .init { request, baseURL, operationID in diff --git a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift index d8ee53fc..52aabcf5 100644 --- a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift +++ b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift @@ -59,6 +59,42 @@ final class Test_Server: XCTestCase { """# ) } + + func testGetStats_200_text_requestedSpecific() async throws { + client = .init( + getStatsBlock: { input in + XCTAssertEqual(input.headers.accept, [ + .init(contentType: .plainText), + .init(contentType: .json, quality: 0.5) + ]) + return .ok(.init(body: .plainText("count is 1"))) + } + ) + let response = try await server.getStats( + .init( + path: "/api/pets/stats", + method: .patch, + headerFields: [ + .init(name: "accept", value: "text/plain, application/json; q=0.500") + ] + ), + .init() + ) + XCTAssertEqual(response.statusCode, 200) + XCTAssertEqual( + response.headerFields, + [ + .init(name: "content-type", value: "text/plain") + ] + ) + XCTAssertEqualStringifiedData( + response.body, + #""" + count is 1 + """# + ) + } + func testGetStats_200_text_customAccept() async throws { client = .init( From 6cefe2b3c56cd875320b445431d575350a14dfb9 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 23 Aug 2023 13:13:39 +0200 Subject: [PATCH 4/7] Fixed up bad merges --- .../translateRawRepresentableEnum.swift | 250 +++++++++--------- .../translateStringEnum.swift | 7 +- .../CommonTypes/CommentExtensions.swift | 4 +- 3 files changed, 136 insertions(+), 125 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawRepresentableEnum.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawRepresentableEnum.swift index 3ec263eb..8f92dc04 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawRepresentableEnum.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawRepresentableEnum.swift @@ -31,156 +31,170 @@ extension FileTranslator { conformances: [String], userDescription: String?, cases: [(caseName: String, rawValue: String)], - unknownCaseName: String, + unknownCaseName: String?, unknownCaseDescription: String?, customSwitchedExpression: (Expression) -> Expression = { $0 } ) throws -> Declaration { + + let generateUnknownCases = unknownCaseName != nil let knownCases: [Declaration] = cases - .map { caseName, _ in + .map { caseName, rawValue in .enumCase( name: caseName, - kind: .nameOnly + kind: generateUnknownCases ? .nameOnly : .nameWithRawValue(rawValue) ) } - let undocumentedCase: Declaration = .commentable( - unknownCaseDescription.flatMap { .doc($0) }, - .enumCase( - name: unknownCaseName, - kind: .nameWithAssociatedValues([ - .init(type: "String") - ]) + + let otherMembers: [Declaration] + if let unknownCaseName { + let undocumentedCase: Declaration = .commentable( + unknownCaseDescription.flatMap { .doc($0) }, + .enumCase( + name: unknownCaseName, + kind: .nameWithAssociatedValues([ + .init(type: "String") + ]) + ) ) - ) - let rawRepresentableInitializer: Declaration - do { - let knownCases: [SwitchCaseDescription] = cases.map { caseName, rawValue in - .init( - kind: .case(.literal(rawValue)), + let rawRepresentableInitializer: Declaration + do { + let knownCases: [SwitchCaseDescription] = cases.map { caseName, rawValue in + .init( + kind: .case(.literal(rawValue)), + body: [ + .expression( + .assignment( + Expression + .identifier("self") + .equals( + .dot(caseName) + ) + ) + ) + ] + ) + } + let unknownCase = SwitchCaseDescription( + kind: .default, body: [ .expression( .assignment( Expression .identifier("self") .equals( - .dot(caseName) + .functionCall( + calledExpression: .dot( + unknownCaseName + ), + arguments: [ + .identifier("rawValue") + ] + ) ) ) ) ] ) + rawRepresentableInitializer = .function( + .init( + accessModifier: config.access, + kind: .initializer(failable: true), + parameters: [ + .init(label: "rawValue", type: "String") + ], + body: [ + .expression( + .switch( + switchedExpression: customSwitchedExpression( + .identifier("rawValue") + ), + cases: knownCases + [unknownCase] + ) + ) + ] + ) + ) } - let unknownCase = SwitchCaseDescription( - kind: .default, - body: [ - .expression( - .assignment( - Expression - .identifier("self") - .equals( - .functionCall( - calledExpression: .dot( - unknownCaseName - ), - arguments: [ - .identifier("rawValue") - ] - ) + + let rawValueGetter: Declaration + do { + let knownCases: [SwitchCaseDescription] = cases.map { caseName, rawValue in + .init( + kind: .case(.dot(caseName)), + body: [ + .expression( + .return(.literal(rawValue)) ) + ] ) - ) - ] - ) - rawRepresentableInitializer = .function( - .init( - accessModifier: config.access, - kind: .initializer(failable: true), - parameters: [ - .init(label: "rawValue", type: "String") - ], - body: [ - .expression( - .switch( - switchedExpression: customSwitchedExpression( - .identifier("rawValue") + } + let unknownCase = SwitchCaseDescription( + kind: .case( + .valueBinding( + kind: .let, + value: .init( + calledExpression: .dot( + unknownCaseName ), - cases: knownCases + [unknownCase] + arguments: [ + .identifier("string") + ] ) ) - ] - ) - ) - } - - let rawValueGetter: Declaration - do { - let knownCases: [SwitchCaseDescription] = cases.map { caseName, rawValue in - .init( - kind: .case(.dot(caseName)), + ), body: [ .expression( - .return(.literal(rawValue)) + .return(.identifier("string")) ) ] ) - } - let unknownCase = SwitchCaseDescription( - kind: .case( - .valueBinding( - kind: .let, - value: .init( - calledExpression: .dot( - unknownCaseName - ), - arguments: [ - .identifier("string") - ] - ) - ) - ), - body: [ - .expression( - .return(.identifier("string")) - ) - ] - ) - - let variableDescription = VariableDescription( - accessModifier: config.access, - kind: .var, - left: "rawValue", - type: "String", - body: [ - .expression( - .switch( - switchedExpression: .identifier("self"), - cases: [unknownCase] + knownCases - ) - ) - ] - ) - - rawValueGetter = .variable( - variableDescription - ) - } - - let allCasesGetter: Declaration - do { - let caseExpressions: [Expression] = cases.map { caseName, _ in - .memberAccess(.init(right: caseName)) - } - allCasesGetter = .variable( - .init( + + let variableDescription = VariableDescription( accessModifier: config.access, - isStatic: true, kind: .var, - left: "allCases", - type: "[Self]", + left: "rawValue", + type: "String", body: [ - .expression(.literal(.array(caseExpressions))) + .expression( + .switch( + switchedExpression: .identifier("self"), + cases: [unknownCase] + knownCases + ) + ) ] ) - ) + + rawValueGetter = .variable( + variableDescription + ) + } + + let allCasesGetter: Declaration + do { + let caseExpressions: [Expression] = cases.map { caseName, _ in + .memberAccess(.init(right: caseName)) + } + allCasesGetter = .variable( + .init( + accessModifier: config.access, + isStatic: true, + kind: .var, + left: "allCases", + type: "[Self]", + body: [ + .expression(.literal(.array(caseExpressions))) + ] + ) + ) + } + otherMembers = [ + undocumentedCase, + rawRepresentableInitializer, + rawValueGetter, + allCasesGetter, + ] + } else { + otherMembers = [] } let enumDescription = EnumDescription( @@ -188,14 +202,8 @@ extension FileTranslator { accessModifier: config.access, name: typeName.shortSwiftName, conformances: conformances, - members: knownCases + [ - undocumentedCase, - rawRepresentableInitializer, - rawValueGetter, - allCasesGetter, - ] + members: knownCases + otherMembers ) - let comment: Comment? = typeName .docCommentWithUserDescription(userDescription) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift index 9657dd3d..fbcf1133 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift @@ -45,12 +45,15 @@ extension FileTranslator { let caseName = swiftSafeName(for: rawValue) return (caseName, rawValue) } + let generateUnknownCases = shouldGenerateUndocumentedCaseForEnumsAndOneOfs + let baseConformance = generateUnknownCases ? Constants.StringEnum.baseConformanceOpen : Constants.StringEnum.baseConformanceClosed + let unknownCaseName = generateUnknownCases ? Constants.StringEnum.undocumentedCaseName : nil return try translateRawRepresentableEnum( typeName: typeName, - conformances: Constants.StringEnum.conformances, + conformances: [baseConformance] + Constants.StringEnum.conformances, userDescription: userDescription, cases: cases, - unknownCaseName: Constants.StringEnum.undocumentedCaseName, + unknownCaseName: unknownCaseName, unknownCaseDescription: "Parsed a raw value that was not defined in the OpenAPI document." ) } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/CommentExtensions.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/CommentExtensions.swift index b12be738..9ba10355 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/CommentExtensions.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/CommentExtensions.swift @@ -105,12 +105,12 @@ extension TypeName { /// - Parameter subPath: A subpath appended to the JSON path of this /// type name. func docCommentWithUserDescription(_ userDescription: String?, subPath: String) -> Comment? { - guard let fullyQualifiedJSONPath else { + guard let jsonPath = appending(jsonComponent: subPath).fullyQualifiedJSONPath else { return Comment.doc(prefix: userDescription, suffix: nil) } return Comment.doc( prefix: userDescription, - suffix: "- Remark: Generated from `\(fullyQualifiedJSONPath)/\(subPath)`." + suffix: "- Remark: Generated from `\(jsonPath)`." ) } } From 4220717d9d2b2b296b8bd3c9a9465701f16dff69 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 23 Aug 2023 13:14:00 +0200 Subject: [PATCH 5/7] Formatting --- .../translateRawRepresentableEnum.swift | 61 ++++++++++--------- .../translateStringEnum.swift | 3 +- .../Translator/CommonTypes/Constants.swift | 10 +-- .../Operations/OperationDescription.swift | 12 ++-- .../translateServerMethod.swift | 21 ++++--- .../Translator/TypeAssignment/TypeUsage.swift | 4 +- .../TypesTranslator/translateOperations.swift | 10 +-- Tests/PetstoreConsumerTests/Test_Client.swift | 9 ++- .../Test_Client.swift | 17 +++--- .../Test_Server.swift | 14 +++-- 10 files changed, 87 insertions(+), 74 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawRepresentableEnum.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawRepresentableEnum.swift index 8f92dc04..1ce8a258 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawRepresentableEnum.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawRepresentableEnum.swift @@ -14,7 +14,7 @@ import OpenAPIKit30 extension FileTranslator { - + /// Returns a declaration of the specified raw representable enum. /// - Parameters: /// - typeName: The name of the type to give to the declared enum. @@ -35,16 +35,17 @@ extension FileTranslator { unknownCaseDescription: String?, customSwitchedExpression: (Expression) -> Expression = { $0 } ) throws -> Declaration { - + let generateUnknownCases = unknownCaseName != nil - let knownCases: [Declaration] = cases + let knownCases: [Declaration] = + cases .map { caseName, rawValue in .enumCase( name: caseName, kind: generateUnknownCases ? .nameOnly : .nameWithRawValue(rawValue) ) } - + let otherMembers: [Declaration] if let unknownCaseName { let undocumentedCase: Declaration = .commentable( @@ -59,20 +60,20 @@ extension FileTranslator { let rawRepresentableInitializer: Declaration do { let knownCases: [SwitchCaseDescription] = cases.map { caseName, rawValue in - .init( - kind: .case(.literal(rawValue)), - body: [ - .expression( - .assignment( - Expression - .identifier("self") - .equals( - .dot(caseName) - ) - ) + .init( + kind: .case(.literal(rawValue)), + body: [ + .expression( + .assignment( + Expression + .identifier("self") + .equals( + .dot(caseName) + ) ) - ] - ) + ) + ] + ) } let unknownCase = SwitchCaseDescription( kind: .default, @@ -115,18 +116,18 @@ extension FileTranslator { ) ) } - + let rawValueGetter: Declaration do { let knownCases: [SwitchCaseDescription] = cases.map { caseName, rawValue in - .init( - kind: .case(.dot(caseName)), - body: [ - .expression( - .return(.literal(rawValue)) - ) - ] - ) + .init( + kind: .case(.dot(caseName)), + body: [ + .expression( + .return(.literal(rawValue)) + ) + ] + ) } let unknownCase = SwitchCaseDescription( kind: .case( @@ -148,7 +149,7 @@ extension FileTranslator { ) ] ) - + let variableDescription = VariableDescription( accessModifier: config.access, kind: .var, @@ -163,16 +164,16 @@ extension FileTranslator { ) ] ) - + rawValueGetter = .variable( variableDescription ) } - + let allCasesGetter: Declaration do { let caseExpressions: [Expression] = cases.map { caseName, _ in - .memberAccess(.init(right: caseName)) + .memberAccess(.init(right: caseName)) } allCasesGetter = .variable( .init( diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift index fbcf1133..f663fe94 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift @@ -46,7 +46,8 @@ extension FileTranslator { return (caseName, rawValue) } let generateUnknownCases = shouldGenerateUndocumentedCaseForEnumsAndOneOfs - let baseConformance = generateUnknownCases ? Constants.StringEnum.baseConformanceOpen : Constants.StringEnum.baseConformanceClosed + let baseConformance = + generateUnknownCases ? Constants.StringEnum.baseConformanceOpen : Constants.StringEnum.baseConformanceClosed let unknownCaseName = generateUnknownCases ? Constants.StringEnum.undocumentedCaseName : nil return try translateRawRepresentableEnum( typeName: typeName, diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index 8a507672..9956ff3d 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -277,23 +277,23 @@ enum Constants { /// The name of the undocumented payload type. static let undocumentedCaseAssociatedValueTypeName = "UndocumentedPayload" } - + /// Constants related to every OpenAPI operation's AcceptableContentType /// type. enum AcceptableContentType { - + /// The name of the type. static let typeName: String = "AcceptableContentType" /// The types that the AcceptableContentType type conforms to. static let conformances: [String] = [ - "AcceptableProtocol", + "AcceptableProtocol" ] - + /// The name of the variable on Input given to the acceptable /// content types array. static let variableName: String = "accept" - + /// The name of the wrapper type. static let headerTypeName: String = "AcceptHeaderContentType" diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift b/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift index 5cfba682..a6f5b98f 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift @@ -140,7 +140,7 @@ extension OperationDescription { jsonComponent: "responses" ) } - + /// Returns the name of the AcceptableContentType type. var acceptableContentTypeName: TypeName { operationNamespace.appending( @@ -151,14 +151,16 @@ extension OperationDescription { jsonComponent: nil ) } - + /// Returns the name of the array of wrapped AcceptableContentType type. var acceptableArrayName: TypeUsage { acceptableContentTypeName .asUsage - .asWrapped(in: .runtime( - Constants.Operation.AcceptableContentType.headerTypeName - )) + .asWrapped( + in: .runtime( + Constants.Operation.AcceptableContentType.headerTypeName + ) + ) .asArray } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift index fd18cc07..b53f6ee6 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift @@ -39,14 +39,15 @@ extension ServerFileTranslator { type: type.fullyQualifiedSwiftName, right: .dot("init") .call( - try parameters.compactMap { - try parseAsTypedParameter( - from: $0, - inParent: operation.inputTypeName - ) - } - .compactMap(translateParameterInServer(_:)) - + extraArguments + try parameters + .compactMap { + try parseAsTypedParameter( + from: $0, + inParent: operation.inputTypeName + ) + } + .compactMap(translateParameterInServer(_:)) + + extraArguments ) ) } @@ -66,13 +67,13 @@ extension ServerFileTranslator { .init( label: "in", expression: .identifier("request").dot("headerFields") - ), + ) ]) ) ) ] } - + var inputMemberCodeBlocks = try [ ( .path, diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeUsage.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeUsage.swift index 002498e1..e4553289 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeUsage.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeUsage.swift @@ -61,7 +61,7 @@ struct TypeUsage { /// /// For example: `Wrapped` becomes `[String: Wrapped]`. case dictionaryValue - + /// A generic type wrapper for the underlying type. /// /// For example, `Wrapped` becomes `Wrapper`. @@ -200,7 +200,7 @@ extension TypeUsage { var asDictionaryValue: Self { TypeUsage(wrapped: .usage(self), usage: .dictionaryValue) } - + /// A type usage created by wrapping the current type usage inside the /// wrapper type, where the wrapper type is generic over the current type. func asWrapped(in wrapper: TypeName) -> Self { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateOperations.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateOperations.swift index 03c244aa..648db594 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateOperations.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateOperations.swift @@ -31,7 +31,8 @@ extension TypesFileTranslator { ) throws -> PropertyBlueprint { let inputTypeName = description.inputTypeName let structTypeName = location.typeName(in: inputTypeName) - let structProperties: [PropertyBlueprint] = try parameters + let structProperties: [PropertyBlueprint] = + try parameters .compactMap { parameter in try parseParameterAsProperty( for: parameter, @@ -98,7 +99,7 @@ extension TypesFileTranslator { ) extraHeaderProperties = [acceptPropertyBlueprint] } - + let inputStructDecl = translateStructBlueprint( .init( comment: nil, @@ -190,7 +191,7 @@ extension TypesFileTranslator { ) return enumDecl } - + /// Returns a declaration of the AcceptableContentType type for the specified /// operation. /// - Parameter description: The OpenAPI operation. @@ -205,7 +206,8 @@ extension TypesFileTranslator { guard !contentTypes.isEmpty else { return nil } - let cases: [(caseName: String, rawValue: String)] = contentTypes + let cases: [(caseName: String, rawValue: String)] = + contentTypes .map { contentType in (contentSwiftName(contentType), contentType.lowercasedTypeAndSubtype) } diff --git a/Tests/PetstoreConsumerTests/Test_Client.swift b/Tests/PetstoreConsumerTests/Test_Client.swift index 21ac30c8..96bfe271 100644 --- a/Tests/PetstoreConsumerTests/Test_Client.swift +++ b/Tests/PetstoreConsumerTests/Test_Client.swift @@ -348,9 +348,12 @@ final class Test_Client: XCTestCase { XCTAssertEqual(operationID, "getStats") XCTAssertEqual(request.path, "/pets/stats") XCTAssertEqual(request.method, .get) - XCTAssertEqual(request.headerFields, [ - .init(name: "accept", value: "application/json") - ]) + XCTAssertEqual( + request.headerFields, + [ + .init(name: "accept", value: "application/json") + ] + ) XCTAssertNil(request.body) return .init( statusCode: 200, diff --git a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift index 1e629eb1..5f0b2b30 100644 --- a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift +++ b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift @@ -66,7 +66,7 @@ final class Test_Client: XCTestCase { XCTFail("Unexpected content type") } } - + func testGetStats_200_text_requestedSpecific() async throws { transport = .init { request, baseURL, operationID in XCTAssertEqual(operationID, "getStats") @@ -89,12 +89,14 @@ final class Test_Client: XCTestCase { """# ) } - let response = try await client.getStats(.init( - headers: .init(accept: [ - .init(contentType: .plainText), - .init(contentType: .json, quality: 0.5) - ]) - )) + let response = try await client.getStats( + .init( + headers: .init(accept: [ + .init(contentType: .plainText), + .init(contentType: .json, quality: 0.5), + ]) + ) + ) guard case let .ok(value) = response else { XCTFail("Unexpected response: \(response)") return @@ -107,7 +109,6 @@ final class Test_Client: XCTestCase { } } - func testGetStats_200_text_customAccept() async throws { transport = .init { request, baseURL, operationID in XCTAssertEqual(operationID, "getStats") diff --git a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift index 52aabcf5..f5545bff 100644 --- a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift +++ b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift @@ -59,14 +59,17 @@ final class Test_Server: XCTestCase { """# ) } - + func testGetStats_200_text_requestedSpecific() async throws { client = .init( getStatsBlock: { input in - XCTAssertEqual(input.headers.accept, [ - .init(contentType: .plainText), - .init(contentType: .json, quality: 0.5) - ]) + XCTAssertEqual( + input.headers.accept, + [ + .init(contentType: .plainText), + .init(contentType: .json, quality: 0.5), + ] + ) return .ok(.init(body: .plainText("count is 1"))) } ) @@ -95,7 +98,6 @@ final class Test_Server: XCTestCase { ) } - func testGetStats_200_text_customAccept() async throws { client = .init( getStatsBlock: { input in From b9ace9a1bcb9b9b9ec9fc96a40ffa4c6533efbe0 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 24 Aug 2023 14:10:59 +0200 Subject: [PATCH 6/7] PR feedback --- .../CommonTranslations/translateRawRepresentableEnum.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawRepresentableEnum.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawRepresentableEnum.swift index 1ce8a258..a1c18014 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawRepresentableEnum.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawRepresentableEnum.swift @@ -22,7 +22,8 @@ extension FileTranslator { /// - userDescription: The contents of the documentation comment. /// - cases: The list of cases to generate. /// - unknownCaseName: The name of the extra unknown case that preserves - /// the string value that doesn't fit any of the cases. + /// the string value that doesn't fit any of the cases. If nil is + /// passed, the unknown case is not generated. /// - unknownCaseDescription: The contents of the documentation comment /// for the unknown case. /// - customSwitchedExpression: A closure From 2f961231a965ee0aa71e28e6400d5266f6013d05 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 24 Aug 2023 14:56:49 +0200 Subject: [PATCH 7/7] Bump runtime to 0.1.10 --- Package.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 9ec851ee..368e45a1 100644 --- a/Package.swift +++ b/Package.swift @@ -77,8 +77,10 @@ let package = Package( from: "1.0.1" ), - // Tests-only: Runtime library linked by generated code - .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.1.9")), + // Tests-only: Runtime library linked by generated code, and also + // helps keep the runtime library new enough to work with the generated + // code. + .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.1.10")), // Build and preview docs .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),