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"), diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift index 4ee21f8f..108e3088 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..a1c18014 --- /dev/null +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawRepresentableEnum.swift @@ -0,0 +1,217 @@ +//===----------------------------------------------------------------------===// +// +// 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. If nil is + /// passed, the unknown case is not generated. + /// - 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 generateUnknownCases = unknownCaseName != nil + 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( + 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))) + ] + ) + ) + } + otherMembers = [ + undocumentedCase, + rawRepresentableInitializer, + rawValueGetter, + allCasesGetter, + ] + } else { + otherMembers = [] + } + + let enumDescription = EnumDescription( + isFrozen: true, + accessModifier: config.access, + name: typeName.shortSwiftName, + conformances: conformances, + members: knownCases + otherMembers + ) + 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 76226046..f663fe94 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,185 +41,21 @@ extension FileTranslator { } return string } - - let generateUnknownCases = shouldGenerateUndocumentedCaseForEnumsAndOneOfs - let knownCases: [Declaration] = - rawValues - .map { rawValue in - let caseName = swiftSafeName(for: rawValue) - return .enumCase( - name: caseName, - kind: generateUnknownCases ? .nameOnly : .nameWithRawValue(rawValue) - ) - } - - let otherMembers: [Declaration] - if generateUnknownCases { - 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 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))) - ] - ) - ) - } - otherMembers = [ - undocumentedCase, - rawRepresentableInitializer, - rawValueGetter, - allCasesGetter, - ] - } else { - otherMembers = [] + let cases = rawValues.map { rawValue in + let caseName = swiftSafeName(for: rawValue) + return (caseName, rawValue) } - + let generateUnknownCases = shouldGenerateUndocumentedCaseForEnumsAndOneOfs let baseConformance = generateUnknownCases ? Constants.StringEnum.baseConformanceOpen : Constants.StringEnum.baseConformanceClosed - let enumDescription = EnumDescription( - isFrozen: true, - accessModifier: config.access, - name: typeName.shortSwiftName, + let unknownCaseName = generateUnknownCases ? Constants.StringEnum.undocumentedCaseName : nil + return try translateRawRepresentableEnum( + typeName: typeName, conformances: [baseConformance] + Constants.StringEnum.conformances, - members: knownCases + otherMembers - ) - - let comment: Comment? = - typeName - .docCommentWithUserDescription(userDescription) - return .commentable( - comment, - .enum(enumDescription) + userDescription: userDescription, + cases: cases, + 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)`." ) } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index f623c3af..9956ff3d 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -277,6 +277,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 3a7990df..a6f5b98f 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift @@ -141,6 +141,29 @@ extension OperationDescription { ) } + /// 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/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/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift index 1580298e..b53f6ee6 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) @@ -38,36 +39,64 @@ extension ServerFileTranslator { type: type.fullyQualifiedSwiftName, right: .dot("init") .call( - try parameters.compactMap { - try parseAsTypedParameter( - from: $0, - inParent: operation.inputTypeName - ) - } - .compactMap(translateParameterInServer(_:)) + try parameters + .compactMap { + try parseAsTypedParameter( + from: $0, + inParent: operation.inputTypeName + ) + } + .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 f3bd6b3e..5de90c6f 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..e4553289 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 b5805ab1..648db594 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateOperations.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateOperations.swift @@ -26,16 +26,19 @@ 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 = .commentable( structTypeName.docCommentWithUserDescription(nil), translateStructBlueprint( @@ -54,7 +57,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()`. @@ -82,6 +85,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, @@ -99,7 +117,8 @@ extension TypesFileTranslator { ), try propertyBlueprintForNamespacedStruct( locatedIn: .header, - withPropertiesFrom: description.allHeaderParameters + withPropertiesFrom: description.allHeaderParameters, + extraProperties: extraHeaderProperties ), try propertyBlueprintForNamespacedStruct( locatedIn: .cookie, @@ -173,6 +192,39 @@ 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. /// /// The namespace type is the parent type of the operation's Input and @@ -197,6 +249,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( @@ -209,7 +262,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 0792d25c..f92208d4 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): @@ -265,10 +263,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 }, @@ -376,10 +373,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 @@ -440,10 +436,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 3ae82f94..b2665df5 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 e838ab17..a977e24b 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? @@ -782,7 +782,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] } } /// - Remark: Generated from `#/paths/pets/GET/query/habitat`. public var habitat: Operations.listPets.Input.Query.habitatPayload? @@ -812,9 +812,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 @@ -851,12 +849,23 @@ public enum Operations { /// /// - Remark: Generated from `#/paths/pets/GET/header/My-Request-UUID`. public var My_Request_UUID: Swift.String? + public var accept: + [OpenAPIRuntime.AcceptHeaderContentType< + Operations.listPets.AcceptableContentType + >] /// Creates a new `Headers`. /// /// - Parameters: /// - My_Request_UUID: Request identifier - 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 @@ -979,6 +988,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 /// @@ -1005,12 +1031,23 @@ public enum Operations { /// /// - Remark: Generated from `#/paths/pets/POST/header/X-Extra-Arguments`. public var X_Extra_Arguments: Components.Schemas.CodeError? + public var accept: + [OpenAPIRuntime.AcceptHeaderContentType< + Operations.createPet.AcceptableContentType + >] /// Creates a new `Headers`. /// /// - Parameters: /// - X_Extra_Arguments: A description here. - 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 @@ -1103,6 +1140,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)`. @@ -1123,8 +1177,19 @@ public enum Operations { public var query: Operations.getStats.Input.Query /// - Remark: Generated from `#/paths/pets/stats/GET/header`. public struct Headers: Sendable, 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 /// - Remark: Generated from `#/paths/pets/stats/GET/cookie`. @@ -1198,6 +1263,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)`. @@ -1414,8 +1496,19 @@ public enum Operations { public var query: Operations.updatePet.Input.Query /// - Remark: Generated from `#/paths/pets/{petId}/PATCH/header`. public struct Headers: Sendable, 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 /// - Remark: Generated from `#/paths/pets/{petId}/PATCH/cookie`. @@ -1529,6 +1622,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 /// @@ -1558,8 +1668,19 @@ public enum Operations { public var query: Operations.uploadAvatarForPet.Input.Query /// - Remark: Generated from `#/paths/pets/{petId}/avatar/PUT/header`. public struct Headers: Sendable, 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 /// - Remark: Generated from `#/paths/pets/{petId}/avatar/PUT/cookie`. @@ -1706,5 +1827,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 42a13c81..ce768275 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): @@ -265,10 +263,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, text/plain, application/octet-stream" + contentTypes: input.headers.accept ) return request }, @@ -406,10 +403,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 @@ -470,10 +466,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 400c73e3..69d4d2e0 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) @@ -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, @@ -458,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? @@ -534,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 4e24b3f5..bcb17a21 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Types.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Types.swift @@ -761,12 +761,23 @@ public enum Operations { /// /// - Remark: Generated from `#/paths/pets/GET/header/My-Request-UUID`. 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: Request identifier - 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 @@ -889,6 +900,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 /// @@ -915,12 +943,23 @@ public enum Operations { /// /// - Remark: Generated from `#/paths/pets/POST/header/X-Extra-Arguments`. 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: A description here. - 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 @@ -1013,6 +1052,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)`. @@ -1033,8 +1089,19 @@ public enum Operations { public var query: Operations.getStats.Input.Query /// - Remark: Generated from `#/paths/pets/stats/GET/header`. public struct Headers: Sendable, 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 /// - Remark: Generated from `#/paths/pets/stats/GET/cookie`. @@ -1112,6 +1179,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)`. @@ -1332,8 +1422,19 @@ public enum Operations { public var query: Operations.updatePet.Input.Query /// - Remark: Generated from `#/paths/pets/{petId}/PATCH/header`. public struct Headers: Sendable, 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 /// - Remark: Generated from `#/paths/pets/{petId}/PATCH/cookie`. @@ -1447,6 +1548,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 /// @@ -1476,8 +1594,19 @@ public enum Operations { public var query: Operations.uploadAvatarForPet.Input.Query /// - Remark: Generated from `#/paths/pets/{petId}/avatar/PUT/header`. public struct Headers: Sendable, 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 /// - Remark: Generated from `#/paths/pets/{petId}/avatar/PUT/cookie`. @@ -1624,5 +1753,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 e71526a8..9c08530b 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -717,7 +717,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 1b7b9176..96bfe271 100644 --- a/Tests/PetstoreConsumerTests/Test_Client.swift +++ b/Tests/PetstoreConsumerTests/Test_Client.swift @@ -348,6 +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") + ] + ) XCTAssertNil(request.body) return .init( statusCode: 200, diff --git a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Client.swift index 18adbefe..5f0b2b30 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,90 @@ final class Test_Client: XCTestCase { } } + 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 + 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(contentType: .json, quality: 0.8), + .init(contentType: .plainText), + ]) + ) + ) + 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_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 6f0433e0..f5545bff 100644 --- a/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift +++ b/Tests/PetstoreConsumerTestsFFMultipleContentTypes/Test_Server.swift @@ -60,6 +60,82 @@ 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( + getStatsBlock: { input in + XCTAssertEqual( + input.headers.accept, + [ + .init(contentType: .json, quality: 0.8), + .init(contentType: .plainText), + ] + ) + 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: "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