diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..08891d83 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..514f398c --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,32 @@ +name: Main + +on: + push: + branches: [main] + schedule: + - cron: "0 8,20 * * *" + +jobs: + unit-tests: + name: Unit tests + uses: apple/swift-nio/.github/workflows/unit_tests.yml@main + with: + linux_5_9_arguments_override: "--explicit-target-dependency-import-check error" + linux_5_10_arguments_override: "--explicit-target-dependency-import-check error" + linux_6_0_arguments_override: "--explicit-target-dependency-import-check error" + linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error" + linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error" + + integration-test: + name: Integration test + uses: apple/swift-nio/.github/workflows/swift_matrix.yml@main + with: + name: "Integration test" + matrix_linux_command: "apt-get update -yq && apt-get install -yq jq && SWIFT_OPENAPI_GENERATOR_REPO_URL=file://${GITHUB_WORKSPACE} ./scripts/run-integration-test.sh" + + example-packages: + name: Example packages + uses: apple/swift-nio/.github/workflows/swift_matrix.yml@main + with: + name: "Example packages" + matrix_linux_command: "./scripts/test-examples.sh" diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 8c598a5a..1935f77a 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -7,24 +7,19 @@ on: jobs: soundness: name: Soundness - uses: apple/swift-nio/.github/workflows/soundness.yml@main + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main with: - api_breakage_check_enabled: true - broken_symlink_check_enabled: true - docs_check_enabled: true - format_check_enabled: true - license_header_check_enabled: true + api_breakage_check_enabled: false license_header_check_project_name: "SwiftOpenAPIGenerator" - shell_check_enabled: true - unacceptable_language_check_enabled: true + yamllint_check_enabled: false unit-tests: name: Unit tests uses: apple/swift-nio/.github/workflows/unit_tests.yml@main with: - linux_5_8_enabled: false linux_5_9_arguments_override: "--explicit-target-dependency-import-check error" linux_5_10_arguments_override: "--explicit-target-dependency-import-check error" + linux_6_0_arguments_override: "--explicit-target-dependency-import-check error" linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error" linux_nightly_main_enabled: false @@ -34,7 +29,6 @@ jobs: with: name: "Integration test" matrix_linux_command: "apt-get update -yq && apt-get install -yq jq && SWIFT_OPENAPI_GENERATOR_REPO_URL=file://${GITHUB_WORKSPACE} ./scripts/run-integration-test.sh" - matrix_linux_5_8_enabled: false matrix_linux_nightly_main_enabled: false compatibility-test: @@ -62,10 +56,4 @@ jobs: with: name: "Example packages" matrix_linux_command: "./scripts/test-examples.sh" - matrix_linux_5_8_enabled: false matrix_linux_nightly_main_enabled: false - - swift-6-language-mode: - name: Swift 6 Language Mode - uses: apple/swift-nio/.github/workflows/swift_6_language_mode.yml@main - if: false # Disabled for now. diff --git a/.github/workflows/scheduled.yml b/.github/workflows/scheduled.yml deleted file mode 100644 index 6c367b83..00000000 --- a/.github/workflows/scheduled.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Scheduled - -on: - schedule: - - cron: "0 8,20 * * *" - -jobs: - unit-tests: - name: Unit tests - uses: apple/swift-nio/.github/workflows/unit_tests.yml@main - with: - linux_5_8_enabled: false - linux_5_9_arguments_override: "--explicit-target-dependency-import-check error" - linux_5_10_arguments_override: "--explicit-target-dependency-import-check error" - linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error" - linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error" - - integration-test: - name: Integration test - uses: apple/swift-nio/.github/workflows/swift_matrix.yml@main - with: - name: "Integration test" - matrix_linux_command: "apt-get update -yq && apt-get install -yq jq && SWIFT_OPENAPI_GENERATOR_REPO_URL=file://${GITHUB_WORKSPACE} ./scripts/run-integration-test.sh" - matrix_linux_5_8_enabled: false - - example-packages: - name: Example packages - uses: apple/swift-nio/.github/workflows/swift_matrix.yml@main - with: - name: "Example packages" - matrix_linux_command: "./scripts/test-examples.sh" - matrix_linux_5_8_enabled: false diff --git a/.licenseignore b/.licenseignore index b107d3cb..0c37091e 100644 --- a/.licenseignore +++ b/.licenseignore @@ -38,4 +38,5 @@ Examples/HelloWorldiOSClientAppExample/HelloWorldiOSClientApp/Assets.xcassets/* Examples/HelloWorldiOSClientAppExample/HelloWorldiOSClientApp/Preview* Examples/**/Generated* **/Makefile -**/*.html \ No newline at end of file +**/*.html +.editorconfig diff --git a/Package.swift b/Package.swift index 25151e5d..36b1d24c 100644 --- a/Package.swift +++ b/Package.swift @@ -49,9 +49,10 @@ let package = Package( // General algorithms .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"), + .package(url: "https://github.com/apple/swift-collections", from: "1.1.4"), // Read OpenAPI documents - .package(url: "https://github.com/mattpolzin/OpenAPIKit", from: "3.1.2"), + .package(url: "https://github.com/mattpolzin/OpenAPIKit", from: "3.3.0"), .package(url: "https://github.com/jpsim/Yams", "4.0.0"..<"6.0.0"), // CLI Tool @@ -72,7 +73,9 @@ let package = Package( .product(name: "OpenAPIKit", package: "OpenAPIKit"), .product(name: "OpenAPIKit30", package: "OpenAPIKit"), .product(name: "OpenAPIKitCompat", package: "OpenAPIKit"), - .product(name: "Algorithms", package: "swift-algorithms"), .product(name: "Yams", package: "Yams"), + .product(name: "Algorithms", package: "swift-algorithms"), + .product(name: "OrderedCollections", package: "swift-collections"), + .product(name: "Yams", package: "Yams"), ], swiftSettings: swiftSettings ), diff --git a/README.md b/README.md index 5c934926..5ee9647d 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,9 @@ platforms, listed below. | Generator plugin and CLI | ✅ 10.15+ | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | | Generated code and runtime library | ✅ 10.15+ | ✅ | ✅ 13+ | ✅ 13+ | ✅ 6+ | ✅ 1+ | +> [!NOTE] +> When using Visual Studio Code or other editors that rely on [SourceKit-LSP](https://github.com/swiftlang/sourcekit-lsp), the editor may not correctly recognize generated code within the same module. As a workaround, consider creating a separate target for code generation and then importing it into your main module. For more details, see the discussion in [swiftlang/sourcekit-lsp#665](https://github.com/swiftlang/sourcekit-lsp/issues/665#issuecomment-2093169169). + ## Documentation and example projects To get started, check out the [documentation][docs-generator], which contains diff --git a/Sources/_OpenAPIGeneratorCore/Diagnostics.swift b/Sources/_OpenAPIGeneratorCore/Diagnostics.swift index f85c991d..08b561a0 100644 --- a/Sources/_OpenAPIGeneratorCore/Diagnostics.swift +++ b/Sources/_OpenAPIGeneratorCore/Diagnostics.swift @@ -322,7 +322,7 @@ public struct StdErrPrintingDiagnosticCollector: DiagnosticCollector, Sendable { /// Emits a diagnostic message to standard error. /// /// - Parameter diagnostic: The diagnostic message to emit. - public func emit(_ diagnostic: Diagnostic) { stdErrHandle.write(diagnostic.description) } + public func emit(_ diagnostic: Diagnostic) { stdErrHandle.write(diagnostic.description + "\n") } } /// A no-op collector, silently ignores all diagnostics. diff --git a/Sources/_OpenAPIGeneratorCore/Hooks/FilteredDocument.swift b/Sources/_OpenAPIGeneratorCore/Hooks/FilteredDocument.swift index 59198c53..613aa26d 100644 --- a/Sources/_OpenAPIGeneratorCore/Hooks/FilteredDocument.swift +++ b/Sources/_OpenAPIGeneratorCore/Hooks/FilteredDocument.swift @@ -313,6 +313,7 @@ private extension FilteredDocumentBuilder { guard predicate(endpoint) else { continue } if requiredEndpoints[path] == nil { requiredEndpoints[path] = Set() } if requiredEndpoints[path]!.insert(endpoint.method).inserted { + for parameter in originalPathItem.parameters { try includeParameter(parameter) } try includeComponentsReferencedBy(endpoint.operation) } } diff --git a/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift b/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift index 4cf1dab0..6be4a80f 100644 --- a/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift +++ b/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift @@ -1628,9 +1628,15 @@ extension KeywordKind { } extension Declaration { + /// Returns a new deprecated variant of the declaration if the provided `description` is not `nil`. + func deprecate(if description: DeprecationDescription?) -> Self { + if let description { return .deprecated(description, self) } + return self + } + /// Returns a new deprecated variant of the declaration if `shouldDeprecate` is true. - func deprecate(if shouldDeprecate: Bool) -> Self { - if shouldDeprecate { return .deprecated(.init(), self) } + func deprecate(if shouldDeprecate: Bool, description: @autoclosure () -> DeprecationDescription = .init()) -> Self { + if shouldDeprecate { return .deprecated(description(), self) } return self } diff --git a/Sources/_OpenAPIGeneratorCore/Parser/YamsParser.swift b/Sources/_OpenAPIGeneratorCore/Parser/YamsParser.swift index dfd605f0..4e462091 100644 --- a/Sources/_OpenAPIGeneratorCore/Parser/YamsParser.swift +++ b/Sources/_OpenAPIGeneratorCore/Parser/YamsParser.swift @@ -70,10 +70,10 @@ public struct YamsParser: ParserProtocol { do { let document: OpenAPIKit.OpenAPI.Document switch openAPIVersion { - case "3.0.0", "3.0.1", "3.0.2", "3.0.3": + case "3.0.0", "3.0.1", "3.0.2", "3.0.3", "3.0.4": let openAPI30Document = try decoder.decode(OpenAPIKit30.OpenAPI.Document.self, from: input.contents) document = openAPI30Document.convert(to: .v3_1_0) - case "3.1.0": document = try decoder.decode(OpenAPIKit.OpenAPI.Document.self, from: input.contents) + case "3.1.0", "3.1.1": document = try decoder.decode(OpenAPIKit.OpenAPI.Document.self, from: input.contents) default: throw Diagnostic.openAPIVersionError( versionString: "openapi: \(openAPIVersion)", @@ -139,7 +139,7 @@ extension Diagnostic { static func openAPIMissingVersionError(location: Location) -> Diagnostic { error( message: - "No openapi key found, please provide a valid OpenAPI document with OpenAPI versions in the 3.0.x or 3.1.x sets.", + "No key named openapi found. Please provide a valid OpenAPI document with OpenAPI versions in the 3.0.x or 3.1.x sets.", location: location ) } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawEnum.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawEnum.swift new file mode 100644 index 00000000..ed513551 --- /dev/null +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawEnum.swift @@ -0,0 +1,140 @@ +//===----------------------------------------------------------------------===// +// +// 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 OpenAPIKit +import OrderedCollections + +/// The backing type of a raw enum. +enum RawEnumBackingType { + + /// Backed by a `String`. + case string + + /// Backed by an `Int`. + case integer +} + +/// The extracted enum value's identifier. +private enum EnumCaseID: Hashable, CustomStringConvertible { + + /// A string value. + case string(String) + + /// An integer value. + case integer(Int) + + var description: String { + switch self { + case .string(let value): return "\"\(value)\"" + case .integer(let value): return String(value) + } + } +} + +/// A wrapper for the metadata about the raw enum case. +private struct EnumCase { + + /// Used for checking uniqueness. + var id: EnumCaseID + + /// The raw Swift-safe name for the case. + var caseName: String + + /// The literal value of the enum case. + var literal: LiteralDescription +} + +extension EnumCase: Equatable { static func == (lhs: EnumCase, rhs: EnumCase) -> Bool { lhs.id == rhs.id } } + +extension EnumCase: Hashable { func hash(into hasher: inout Hasher) { hasher.combine(id) } } + +extension FileTranslator { + + /// Returns a declaration of the specified raw value-based enum schema. + /// - Parameters: + /// - backingType: The backing type of the enum. + /// - typeName: The name of the type to give to the declared enum. + /// - userDescription: A user-specified description from the OpenAPI + /// document. + /// - isNullable: Whether the enum schema is nullable. + /// - allowedValues: The enumerated allowed values. + /// - Throws: A `GenericError` if a disallowed value is encountered. + /// - Returns: A declaration of the specified raw value-based enum schema. + func translateRawEnum( + backingType: RawEnumBackingType, + typeName: TypeName, + userDescription: String?, + isNullable: Bool, + allowedValues: [AnyCodable] + ) throws -> Declaration { + var cases: OrderedSet = [] + func addIfUnique(id: EnumCaseID, caseName: String) throws { + let literal: LiteralDescription + switch id { + case .string(let string): literal = .string(string) + case .integer(let int): literal = .int(int) + } + guard cases.append(.init(id: id, caseName: caseName, literal: literal)).inserted else { + try diagnostics.emit( + .warning( + message: "Duplicate enum value, skipping", + context: ["id": "\(id)", "foundIn": typeName.description] + ) + ) + return + } + } + for anyValue in allowedValues.map(\.value) { + switch backingType { + case .string: + // In nullable enum schemas, empty strings are parsed as Void. + // This is unlikely to be fixed, so handling that case here. + // https://github.com/apple/swift-openapi-generator/issues/118 + if isNullable && anyValue is Void { + try addIfUnique(id: .string(""), caseName: context.asSwiftSafeName("")) + } else { + guard let rawValue = anyValue as? String else { + throw GenericError(message: "Disallowed value for a string enum '\(typeName)': \(anyValue)") + } + let caseName = context.asSwiftSafeName(rawValue) + try addIfUnique(id: .string(rawValue), caseName: caseName) + } + case .integer: + let rawValue: Int + if let intRawValue = anyValue as? Int { + rawValue = intRawValue + } else if let stringRawValue = anyValue as? String, let intRawValue = Int(stringRawValue) { + rawValue = intRawValue + } else { + throw GenericError(message: "Disallowed value for an integer enum '\(typeName)': \(anyValue)") + } + let caseName = rawValue < 0 ? "_n\(abs(rawValue))" : "_\(rawValue)" + try addIfUnique(id: .integer(rawValue), caseName: caseName) + } + } + let baseConformance: String + switch backingType { + case .string: baseConformance = Constants.RawEnum.baseConformanceString + case .integer: baseConformance = Constants.RawEnum.baseConformanceInteger + } + let conformances = [baseConformance] + Constants.RawEnum.conformances + return try translateRawRepresentableEnum( + typeName: typeName, + conformances: conformances, + userDescription: userDescription, + cases: cases.map { ($0.caseName, $0.literal) }, + unknownCaseName: nil, + unknownCaseDescription: nil + ) + } +} diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift deleted file mode 100644 index 9add9482..00000000 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift +++ /dev/null @@ -1,86 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftOpenAPIGenerator open source project -// -// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// -import OpenAPIKit - -/// The backing type of a raw enum. -enum RawEnumBackingType { - - /// Backed by a `String`. - case string - - /// Backed by an `Int`. - case integer -} - -extension FileTranslator { - - /// Returns a declaration of the specified raw value-based enum schema. - /// - Parameters: - /// - backingType: The backing type of the enum. - /// - typeName: The name of the type to give to the declared enum. - /// - userDescription: A user-specified description from the OpenAPI - /// document. - /// - isNullable: Whether the enum schema is nullable. - /// - allowedValues: The enumerated allowed values. - /// - Throws: A `GenericError` if a disallowed value is encountered. - /// - Returns: A declaration of the specified raw value-based enum schema. - func translateRawEnum( - backingType: RawEnumBackingType, - typeName: TypeName, - userDescription: String?, - isNullable: Bool, - allowedValues: [AnyCodable] - ) throws -> Declaration { - let cases: [(String, LiteralDescription)] = try allowedValues.map(\.value) - .map { anyValue in - switch backingType { - case .string: - // In nullable enum schemas, empty strings are parsed as Void. - // This is unlikely to be fixed, so handling that case here. - // https://github.com/apple/swift-openapi-generator/issues/118 - if isNullable && anyValue is Void { return (context.asSwiftSafeName(""), .string("")) } - guard let rawValue = anyValue as? String else { - throw GenericError(message: "Disallowed value for a string enum '\(typeName)': \(anyValue)") - } - let caseName = context.asSwiftSafeName(rawValue) - return (caseName, .string(rawValue)) - case .integer: - let rawValue: Int - if let intRawValue = anyValue as? Int { - rawValue = intRawValue - } else if let stringRawValue = anyValue as? String, let intRawValue = Int(stringRawValue) { - rawValue = intRawValue - } else { - throw GenericError(message: "Disallowed value for an integer enum '\(typeName)': \(anyValue)") - } - let caseName = rawValue < 0 ? "_n\(abs(rawValue))" : "_\(rawValue)" - return (caseName, .int(rawValue)) - } - } - let baseConformance: String - switch backingType { - case .string: baseConformance = Constants.RawEnum.baseConformanceString - case .integer: baseConformance = Constants.RawEnum.baseConformanceInteger - } - let conformances = [baseConformance] + Constants.RawEnum.conformances - return try translateRawRepresentableEnum( - typeName: typeName, - conformances: conformances, - userDescription: userDescription, - cases: cases, - unknownCaseName: nil, - unknownCaseDescription: nil - ) - } -} diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index 52270eb6..d1fbedcf 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -56,6 +56,18 @@ enum Constants { /// The prefix of each generated method name. static let propertyPrefix: String = "server" + /// The name of each generated static function. + static let urlStaticFunc: String = "url" + + /// The prefix of the namespace that contains server specific variables. + static let serverNamespacePrefix: String = "Server" + + /// Constants related to the OpenAPI server variable object. + enum Variable { + + /// The types that the protocol conforms to. + static let conformances: [String] = [TypeName.string.fullyQualifiedSwiftName, "Sendable"] + } } /// Constants related to the configuration type, which is used by both diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift index a5187b4d..5fb29ce3 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseOutcome.swift @@ -23,17 +23,18 @@ extension TypesFileTranslator { /// - operation: The OpenAPI operation. /// - operationJSONPath: The JSON path to the operation in the OpenAPI /// document. - /// - Returns: A declaration of the enum case and a declaration of the + /// - Returns: A tuple containing a declaration of the enum case, a declaration of the /// structure unique to the response that contains the response headers - /// and a body payload. + /// and a body payload, a declaration of a throwing getter and, an optional convenience static property. /// - Throws: An error if there's an issue generating the declarations, such /// as unsupported response types or invalid definitions. func translateResponseOutcomeInTypes( _ outcome: OpenAPI.Operation.ResponseOutcome, operation: OperationDescription, operationJSONPath: String - ) throws -> (payloadStruct: Declaration?, enumCase: Declaration, throwingGetter: Declaration) { - + ) throws -> ( + payloadStruct: Declaration?, enumCase: Declaration, staticMember: Declaration?, throwingGetter: Declaration + ) { let typedResponse = try typedResponse(from: outcome, operation: operation) let responseStructTypeName = typedResponse.typeUsage.typeName let responseKind = outcome.status.value.asKind @@ -55,14 +56,36 @@ extension TypesFileTranslator { } associatedValues.append(.init(type: .init(responseStructTypeName))) - let enumCaseDesc = EnumCaseDescription(name: enumCaseName, kind: .nameWithAssociatedValues(associatedValues)) - let enumCaseDecl: Declaration = .commentable( - responseKind.docComment( - userDescription: typedResponse.response.description, - jsonPath: operationJSONPath + "/responses/" + responseKind.jsonPathComponent - ), - .enumCase(enumCaseDesc) + let enumCaseDocComment = responseKind.docComment( + userDescription: typedResponse.response.description, + jsonPath: operationJSONPath + "/responses/" + responseKind.jsonPathComponent ) + let enumCaseDesc = EnumCaseDescription(name: enumCaseName, kind: .nameWithAssociatedValues(associatedValues)) + let enumCaseDecl: Declaration = .commentable(enumCaseDocComment, .enumCase(enumCaseDesc)) + + let staticMemberDecl: Declaration? + let responseHasNoHeaders = typedResponse.response.headers?.isEmpty ?? true + let responseHasNoContent = typedResponse.response.content.isEmpty + if responseHasNoContent && responseHasNoHeaders && !responseKind.wantsStatusCode { + let staticMemberDesc = VariableDescription( + accessModifier: config.access, + isStatic: true, + kind: .var, + left: .identifier(.pattern(enumCaseName)), + type: .member(["Self"]), + getter: [ + .expression( + .functionCall( + calledExpression: .dot(enumCaseName), + arguments: [.functionCall(calledExpression: .dot("init"))] + ) + ) + ] + ) + staticMemberDecl = .commentable(enumCaseDocComment, .variable(staticMemberDesc)) + } else { + staticMemberDecl = nil + } let throwingGetterDesc = VariableDescription( accessModifier: config.access, @@ -113,7 +136,7 @@ extension TypesFileTranslator { ) let throwingGetterDecl = Declaration.commentable(throwingGetterComment, .variable(throwingGetterDesc)) - return (responseStructDecl, enumCaseDecl, throwingGetterDecl) + return (responseStructDecl, enumCaseDecl, staticMemberDecl, throwingGetterDecl) } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateOperations.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateOperations.swift index 55d01890..8beaae76 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateOperations.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateOperations.swift @@ -143,7 +143,8 @@ extension TypesFileTranslator { let documentedMembers: [Declaration] = documentedOutcomes.flatMap { inlineResponseDecl, caseDecl, - throwingGetter in [inlineResponseDecl, caseDecl, throwingGetter].compactMap { $0 } + staticDecl, + throwingGetter in [inlineResponseDecl, caseDecl, staticDecl, throwingGetter].compactMap { $0 } } let allMembers: [Declaration] diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServers.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServers.swift index 84ed698e..cf9927a7 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServers.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServers.swift @@ -14,47 +14,47 @@ import OpenAPIKit extension TypesFileTranslator { - - /// Returns a declaration of a server URL static method defined in - /// the OpenAPI document. + /// Returns a declaration of a server URL static function defined in + /// the OpenAPI document using the supplied name identifier and + /// variable generators. + /// + /// If the `deprecated` parameter is supplied the static function + /// will be generated with a name that matches the previous, now + /// deprecated API. + /// + /// - Important: The variable generators provided should all + /// be ``RawStringTranslatedServerVariable`` to ensure + /// the generated function matches the previous implementation, this + /// is **not** asserted by this translate function. + /// + /// If the `deprecated` parameter is `nil` then the function will + /// be generated with the identifier `url` and must be a member + /// of a namespace to avoid conflicts with other server URL static + /// functions. + /// /// - Parameters: /// - index: The index of the server in the list of servers defined /// in the OpenAPI document. /// - server: The server URL information. + /// - deprecated: A deprecation `@available` annotation to attach + /// to this declaration, or `nil` if the declaration should not be deprecated. + /// - variables: The generators for variables the server has defined. /// - Returns: A static method declaration, and a name for the variable to /// declare the method under. - func translateServer(index: Int, server: OpenAPI.Server) -> Declaration { - let methodName = "\(Constants.ServerURL.propertyPrefix)\(index+1)" - let safeVariables = server.variables.map { (key, value) in - (originalKey: key, swiftSafeKey: context.asSwiftSafeName(key), value: value) - } - let parameters: [ParameterDescription] = safeVariables.map { (originalKey, swiftSafeKey, value) in - .init(label: swiftSafeKey, type: .init(TypeName.string), defaultValue: .literal(value.default)) - } - let variableInitializers: [Expression] = safeVariables.map { (originalKey, swiftSafeKey, value) in - let allowedValuesArg: FunctionArgumentDescription? - if let allowedValues = value.enum { - allowedValuesArg = .init( - label: "allowedValues", - expression: .literal(.array(allowedValues.map { .literal($0) })) - ) - } else { - allowedValuesArg = nil - } - return .dot("init") - .call( - [ - .init(label: "name", expression: .literal(originalKey)), - .init(label: "value", expression: .identifierPattern(swiftSafeKey)), - ] + (allowedValuesArg.flatMap { [$0] } ?? []) - ) - } - let methodDecl = Declaration.commentable( - .functionComment(abstract: server.description, parameters: safeVariables.map { ($1, $2.description) }), + private func translateServerStaticFunction( + index: Int, + server: OpenAPI.Server, + deprecated: DeprecationDescription?, + variableGenerators variables: [any ServerVariableGenerator] + ) -> Declaration { + let name = + deprecated == nil ? Constants.ServerURL.urlStaticFunc : "\(Constants.ServerURL.propertyPrefix)\(index + 1)" + return .commentable( + .functionComment(abstract: server.description, parameters: variables.map(\.functionComment)), .function( accessModifier: config.access, - kind: .function(name: methodName, isStatic: true), - parameters: parameters, + kind: .function(name: name, isStatic: true), + parameters: variables.map(\.parameter), keywords: [.throws], returnType: .identifierType(TypeName.url), body: [ @@ -65,14 +65,78 @@ extension TypesFileTranslator { .init( label: "validatingOpenAPIServerURL", expression: .literal(.string(server.urlTemplate.absoluteString)) - ), .init(label: "variables", expression: .literal(.array(variableInitializers))), + ), + .init( + label: "variables", + expression: .literal(.array(variables.map(\.initializer))) + ), ]) ) ) ] ) + .deprecate(if: deprecated) + ) + } + + /// Returns a declaration of a server URL static function defined in + /// the OpenAPI document. The function is marked as deprecated + /// with a message informing the adopter to use the new type-safe + /// API. + /// - Parameters: + /// - index: The index of the server in the list of servers defined + /// in the OpenAPI document. + /// - server: The server URL information. + /// - pathToReplacementSymbol: The Swift path of the symbol + /// which has resulted in the deprecation of this symbol. + /// - Returns: A static function declaration. + func translateServerAsDeprecated(index: Int, server: OpenAPI.Server, renamedTo pathToReplacementSymbol: String) + -> Declaration + { + let serverVariables = translateServerVariables(index: index, server: server, generateAsEnum: false) + return translateServerStaticFunction( + index: index, + server: server, + deprecated: DeprecationDescription(renamed: pathToReplacementSymbol), + variableGenerators: serverVariables + ) + } + + /// Returns a namespace (enum) declaration for a server defined in + /// the OpenAPI document. Within the namespace are enums to + /// represent any variables that also have enum values defined in the + /// OpenAPI document, and a single static function named 'url' which + /// at runtime returns the resolved server URL. + /// + /// The server's namespace is named to identify the human-friendly + /// index of the enum (e.g. Server1) and is present to ensure each + /// server definition's variables do not conflict with one another. + /// - Parameters: + /// - index: The index of the server in the list of servers defined + /// in the OpenAPI document. + /// - server: The server URL information. + /// - Returns: A static function declaration. + func translateServer(index: Int, server: OpenAPI.Server) -> (pathToStaticFunction: String, decl: Declaration) { + let serverVariables = translateServerVariables(index: index, server: server, generateAsEnum: true) + let methodDecl = translateServerStaticFunction( + index: index, + server: server, + deprecated: nil, + variableGenerators: serverVariables + ) + let namespaceName = "\(Constants.ServerURL.serverNamespacePrefix)\(index + 1)" + let typeName = TypeName(swiftKeyPath: [ + Constants.ServerURL.namespace, namespaceName, Constants.ServerURL.urlStaticFunc, + ]) + let decl = Declaration.commentable( + server.description.map(Comment.doc(_:)), + .enum( + accessModifier: config.access, + name: namespaceName, + members: serverVariables.compactMap(\.declaration) + CollectionOfOne(methodDecl) + ) ) - return methodDecl + return (pathToStaticFunction: typeName.fullyQualifiedSwiftName, decl: decl) } /// Returns a declaration of a namespace (enum) called "Servers" that @@ -81,7 +145,18 @@ extension TypesFileTranslator { /// - Parameter servers: The servers to include in the extension. /// - Returns: A declaration of an enum namespace of the server URLs type. func translateServers(_ servers: [OpenAPI.Server]) -> Declaration { - let serverDecls = servers.enumerated().map(translateServer) + var serverDecls: [Declaration] = [] + for (index, server) in servers.enumerated() { + let translatedServer = translateServer(index: index, server: server) + serverDecls.append(contentsOf: [ + translatedServer.decl, + translateServerAsDeprecated( + index: index, + server: server, + renamedTo: translatedServer.pathToStaticFunction + ), + ]) + } return .commentable( .doc("Server URLs defined in the OpenAPI document."), .enum(accessModifier: config.access, name: Constants.ServerURL.namespace, members: serverDecls) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServersVariables.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServersVariables.swift new file mode 100644 index 00000000..c8b96ff0 --- /dev/null +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServersVariables.swift @@ -0,0 +1,237 @@ +//===----------------------------------------------------------------------===// +// +// 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 OpenAPIKit + +/// Represents a server variable and the function of generation that should be applied. +protocol ServerVariableGenerator { + /// Returns the declaration (enum) that should be added to the server's namespace. + /// If the server variable does not require any codegen then it should return `nil`. + var declaration: Declaration? { get } + + /// Returns the description of the parameter that will be used to define the variable + /// in the static method for a given server. + var parameter: ParameterDescription { get } + + /// Returns an expression for the variable initializer that is used in the body of a server's + /// static method by passing it along to the URL resolver. + var initializer: Expression { get } + + /// Returns the description of this variables documentation for the function comment of + /// the server's static method. + var functionComment: (name: String, comment: String?) { get } +} + +extension TypesFileTranslator { + /// Returns a declaration of a namespace (enum) for a specific server and will define + /// one enum member for each of the server's variables in the OpenAPI Document. + /// If the server does not define variables, no declaration will be generated. + /// - Parameters: + /// - index: The index of the server in the list of servers defined + /// in the OpenAPI document. + /// - server: The server variables information. + /// - generateAsEnum: Whether the enum generator is allowed, if `false` + /// only `RawStringTranslatedServerVariable` generators will be returned. + /// - Returns: A declaration of the server variables namespace, or `nil` if no + /// variables are declared. + func translateServerVariables(index: Int, server: OpenAPI.Server, generateAsEnum: Bool) + -> [any ServerVariableGenerator] + { + server.variables.map { key, variable in + guard generateAsEnum, let enumValues = variable.enum else { + return RawStringTranslatedServerVariable(key: key, variable: variable, context: context) + } + return GeneratedEnumTranslatedServerVariable( + key: key, + variable: variable, + enumValues: enumValues, + accessModifier: config.access, + context: context + ) + } + } + + // MARK: Generators + + /// Represents a variable that is required to be represented as a `Swift.String`. + private struct RawStringTranslatedServerVariable: ServerVariableGenerator { + /// The key of the variable defined in the Open API document. + let key: String + + /// The ``key`` after being santized for use as an identifier. + let swiftSafeKey: String + + /// The server variable information. + let variable: OpenAPI.Server.Variable + + /// Create a generator for an Open API "Server Variable Object" that is represented + /// by a `Swift.String` in the generated output. + /// + /// - Parameters: + /// - key: The key of the variable defined in the Open API document. + /// - variable: The server variable information. + /// - context: The translator context the generator should use to create + /// Swift safe identifiers. + init(key: String, variable: OpenAPI.Server.Variable, context: TranslatorContext) { + self.key = key + swiftSafeKey = context.asSwiftSafeName(key) + self.variable = variable + } + + /// Returns the declaration (enum) that should be added to the server's namespace. + /// If the server variable does not require any codegen then it should return `nil`. + var declaration: Declaration? { + // A variable being represented by a `Swift.String` does not have a declaration that needs to + // be added to the server's namespace. + nil + } + + /// Returns the description of the parameter that will be used to define the variable + /// in the static method for a given server. + var parameter: ParameterDescription { + .init(label: swiftSafeKey, type: .init(TypeName.string), defaultValue: .literal(variable.default)) + } + + /// Returns an expression for the variable initializer that is used in the body of a server's + /// static method by passing it along to the URL resolver. + var initializer: Expression { + var arguments: [FunctionArgumentDescription] = [ + .init(label: "name", expression: .literal(key)), + .init(label: "value", expression: .identifierPattern(swiftSafeKey)), + ] + if let allowedValues = variable.enum { + arguments.append( + .init(label: "allowedValues", expression: .literal(.array(allowedValues.map { .literal($0) }))) + ) + } + return .dot("init").call(arguments) + } + + /// Returns the description of this variables documentation for the function comment of + /// the server's static method. + var functionComment: (name: String, comment: String?) { (name: swiftSafeKey, comment: variable.description) } + } + + /// Represents an Open API "Server Variable Object" that will be generated as an enum and added + /// to the server's namespace. + private struct GeneratedEnumTranslatedServerVariable: ServerVariableGenerator { + /// The key of the variable defined in the Open API document. + let key: String + + /// The ``key`` after being santized for use as an identifier. + let swiftSafeKey: String + + /// The ``key`` after being santized for use as the enum identifier. + let enumName: String + + /// The server variable information. + let variable: OpenAPI.Server.Variable + + /// The 'enum' values of the variable as defined in the Open API document. + let enumValues: [String] + + /// The access modifier to use for generated declarations. + let accessModifier: AccessModifier + + /// The translator context the generator should use to create Swift safe identifiers. + let context: TranslatorContext + + /// Create a generator for an Open API "Server Variable Object" that is represented + /// by an enumeration in the generated output. + /// + /// - Parameters: + /// - key: The key of the variable defined in the Open API document. + /// - variable: The server variable information. + /// - enumValues: The 'enum' values of the variable as defined in the Open API document. + /// - accessModifier: The access modifier to use for generated declarations. + /// - context: The translator context the generator should use to create + /// Swift safe identifiers. + init( + key: String, + variable: OpenAPI.Server.Variable, + enumValues: [String], + accessModifier: AccessModifier, + context: TranslatorContext + ) { + self.key = key + swiftSafeKey = context.asSwiftSafeName(key) + enumName = context.asSwiftSafeName(key.localizedCapitalized) + self.variable = variable + self.enumValues = enumValues + self.context = context + self.accessModifier = accessModifier + } + + /// Returns the declaration (enum) that should be added to the server's namespace. + /// If the server variable does not require any codegen then it should return `nil`. + var declaration: Declaration? { + let description: String = if let description = variable.description { description + "\n\n" } else { "" } + + return .commentable( + .doc( + """ + \(description)The "\(key)" variable defined in the OpenAPI document. The default value is "\(variable.default)". + """ + ), + .enum( + isFrozen: true, + accessModifier: accessModifier, + name: enumName, + conformances: Constants.ServerURL.Variable.conformances, + members: enumValues.map(translateVariableCase) + ) + ) + } + + /// Returns the description of the parameter that will be used to define the variable + /// in the static method for a given server. + var parameter: ParameterDescription { + .init( + label: swiftSafeKey, + type: .member([enumName]), + defaultValue: .memberAccess(.dot(context.asSwiftSafeName(variable.default))) + ) + } + + /// Returns an expression for the variable initializer that is used in the body of a server's + /// static method by passing it along to the URL resolver. + var initializer: Expression { + .dot("init") + .call([ + .init(label: "name", expression: .literal(key)), + .init( + label: "value", + expression: .memberAccess(.init(left: .identifierPattern(swiftSafeKey), right: "rawValue")) + ), + ]) + } + + /// Returns the description of this variables documentation for the function comment of + /// the server's static method. + var functionComment: (name: String, comment: String?) { (name: swiftSafeKey, comment: variable.description) } + + /// Returns an enum case declaration for a raw string enum. + /// + /// If the name does not need to be converted to a Swift safe identifier then the + /// enum case will not define a raw value and rely on the implicit generation from + /// Swift. Otherwise the enum case name will be the Swift safe name and a string + /// raw value will be set to the original name. + /// + /// - Parameter name: The original name. + /// - Returns: A declaration of an enum case. + private func translateVariableCase(_ name: String) -> Declaration { + let caseName = context.asSwiftSafeName(name) + return .enumCase(name: caseName, kind: caseName == name ? .nameOnly : .nameWithRawValue(.string(name))) + } + } +} diff --git a/Sources/swift-openapi-generator/Documentation.docc/Articles/Frequently-asked-questions.md b/Sources/swift-openapi-generator/Documentation.docc/Articles/Frequently-asked-questions.md index 22f1ee91..f9a17b03 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Articles/Frequently-asked-questions.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Articles/Frequently-asked-questions.md @@ -16,13 +16,13 @@ This article includes some commonly asked questions and answers. Check out . -### What OpenAPI featues does the generator support? +### What OpenAPI features does the generator support? Check out . ### Which underlying HTTP library does the generated code use? -Swift OpenAPI Generator is not tied to any particular HTTP library. Instead, the generated code utilizies a general protocol called [`ClientTransport`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/clienttransport) for client code, and [`ServerTransport`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/servertransport) for server code. +Swift OpenAPI Generator is not tied to any particular HTTP library. Instead, the generated code utilizes a general protocol called [`ClientTransport`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/clienttransport) for client code, and [`ServerTransport`](https://swiftpackageindex.com/apple/swift-openapi-runtime/documentation/openapiruntime/servertransport) for server code. The user of the generated code provides one of the concrete transport implementations, based on what's appropriate for their use case. @@ -71,3 +71,24 @@ For Xcode projects, make sure the target that uses the Swift OpenAPI Generator b Alternatively, change the access modifier of the generated code to either `internal` (if no code outside of that module needs to use it) or `public` (if the generated code is exported to other modules and packages.) You can do so by setting `accessModifier: internal` in the generator configuration file, or by providing `--access-modifier internal` to the `swift-openapi-generator` CLI. For details, check out . + +### How do I enable the build plugin in Xcode and Xcode Cloud? + +By default, you must explicitly enable build plugins before they are allowed to run. + +Before a plugin is enabled, you will encounter a build error with the message `"OpenAPIGenerator" is disabled`. + +In Xcode, enable the plugin by clicking the "Enable Plugin" button next to the build error and confirm the dialog by clicking "Trust & Enable". + +In Xcode Cloud, add the script `ci_scripts/ci_post_clone.sh` next to your Xcode project or workspace, containing: + +```bash +#!/usr/bin/env bash + +set -e + +# NOTE: the misspelling of validation as "validatation" is intentional and the spelling Xcode expects. +defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES +``` + +Learn more about Xcode Cloud custom scripts in the [documentation](https://developer.apple.com/documentation/xcode/writing-custom-build-scripts). diff --git a/Sources/swift-openapi-generator/Documentation.docc/Articles/Practicing-spec-driven-API-development.md b/Sources/swift-openapi-generator/Documentation.docc/Articles/Practicing-spec-driven-API-development.md index c900b6a4..9df7bc06 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Articles/Practicing-spec-driven-API-development.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Articles/Practicing-spec-driven-API-development.md @@ -113,7 +113,7 @@ Publishing the source of truth is preferable to relying on transcoding from code By inferring the specification from existing code, the resulting OpenAPI spec is often lossy and incomplete. And even with annotated code, it can be difficult to predict the OpenAPI output. -Additionally, any feature unsupported by the transcoder cannot be represented in the generated OpenAPI document, further limiting the creativity and expressivness of the API. +Additionally, any feature unsupported by the transcoder cannot be represented in the generated OpenAPI document, further limiting the creativity and expressiveness of the API. > Tip: Publish the source of truth, not a representation transcoded from the source of truth. diff --git a/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md b/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md index 752cc0a6..0bc8421e 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md @@ -4,7 +4,7 @@ Collaborate on API changes to Swift OpenAPI Generator by writing a proposal. ## Overview -For non-trivial changes that affect the public API, the Swift OpenAPI Generator project adopts a ligthweight version of the [Swift Evolution](https://github.com/apple/swift-evolution/blob/main/process.md) process. +For non-trivial changes that affect the public API, the Swift OpenAPI Generator project adopts a lightweight version of the [Swift Evolution](https://github.com/apple/swift-evolution/blob/main/process.md) process. Writing a proposal first helps discuss multiple possible solutions early, apply useful feedback from other contributors, and avoid reimplementing the same feature multiple times. @@ -52,3 +52,5 @@ If you have any questions, tag [Honza Dvorsky](https://github.com/czechboy0) or - - - +- +- diff --git a/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0011.md b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0011.md new file mode 100644 index 00000000..1ebe5af7 --- /dev/null +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0011.md @@ -0,0 +1,159 @@ +# SOAR-0011: Improved Error Handling + +Improve error handling by adding the ability for mapping application errors to HTTP responses. + +## Overview + +- Proposal: SOAR-0011 +- Author(s): [Gayathri Sairamkrishnan](https://github.com/gayathrisairam) +- Status: **Implemented** +- Issue: [apple/swift-openapi-generator#609](https://github.com/apple/swift-openapi-generator/issues/609) +- Affected components: + - runtime +- Versions: + - v1.0 (2024-09-19): Initial version + - v1.1(2024-10-07): + - Replace the proposed solution to have a single error handling protocol, with the status being required and + headers/body being optional. + +### Introduction + +The goal of this proposal to improve the current error handling mechanism in Swift OpenAPI runtime. The proposal +introduces a way for users to map errors thrown by their handlers to specific HTTP responses. + +### Motivation + + When implementing a server with Swift OpenAPI Generator, users implement a type that conforms to a generated protocol, + providing one method for each API operation defined in the OpenAPI document. At runtime, if this function throws, it's up to the server transport to transform it into an HTTP response status code – for example, some transport use `500 Internal Error`. + +Instead, server developers may want to map errors thrown by the application to a more specific HTTP response. +Currently, this can be achieved by checking for each error type in each handler's catch block, converting it to an +appropriate HTTP response and returning it. + +For example, +```swift +func getGreeting(_ input: Operations.getGreeting.Input) async throws -> Operations.getGreeting.Output { + do { + let response = try callGreetingLib() + return .ok(.init(body: response)) + } catch let error { + switch error { + case GreetingError.authorizationError: + return .unauthorized(.init()) + case GreetingError.timeout: + return ... + } + } +} +``` +If a user wishes to map many errors, the error handling block scales linearly and introduces a lot of ceremony. + +### Proposed solution + +The proposed solution is twofold. + +1. Provide a protocol in `OpenAPIRuntime` to allow users to extend their error types with mappings to HTTP responses. + +2. Provide an (opt-in) middleware in OpenAPIRuntime that will call the conversion function on conforming error types when +constructing the HTTP response. + +Vapor has a similar mechanism called [AbortError](https://docs.vapor.codes/basics/errors/). + +Hummingbird also has an [error handling mechanism](https://docs.hummingbird.codes/2.0/documentation/hummingbird/errorhandling/) +by allowing users to define a [HTTPError](https://docs.hummingbird.codes/2.0/documentation/hummingbird/httperror) + +The proposal aims to provide a transport agnostic error handling mechanism for OpenAPI users. + +### Detailed design + +#### Proposed Error protocols + +Users can choose to conform to the error handling protocol below and optionally provide the optional fields depending on +the level of specificity they would like to have in the response. + +```swift +public protocol HTTPResponseConvertible { + var httpStatus: HTTPResponse.Status { get } + var httpHeaderFields: HTTPTypes.HTTPFields { get } + var httpBody: OpenAPIRuntime.HTTPBody? { get } +} + +extension HTTPResponseConvertible { + var httpHeaderFields: HTTPTypes.HTTPFields { [:] } + var httpBody: OpenAPIRuntime.HTTPBody? { nil } +} +``` + +#### Proposed Error Middleware + +The proposed error middleware in OpenAPIRuntime will convert the application error to the appropriate error response. + It returns 500 for application error(s) that do not conform to HTTPResponseConvertible protocol. + +```swift +public struct ErrorHandlingMiddleware: ServerMiddleware { + func intercept(_ request: HTTPTypes.HTTPRequest, + body: OpenAPIRuntime.HTTPBody?, + metadata: OpenAPIRuntime.ServerRequestMetadata, + operationID: String, + next: @Sendable (HTTPTypes.HTTPRequest, OpenAPIRuntime.HTTPBody?, OpenAPIRuntime.ServerRequestMetadata) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?)) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) { + do { + return try await next(request, body, metadata) + } catch let error as ServerError { + if let appError = error.underlyingError as? HTTPResponseConvertible else { + return (HTTPResponse(status: appError.httpStatus, headerFields: appError.httpHeaderFields), + appError.httpBody) + } else { + throw error + } + } + } +} +``` + +Please note that the proposal places the responsibility to conform to the documented API in the hands of the user. +There's no mechanism to prevent the users from inadvertently transforming a thrown error into an undocumented response. + +#### Example usage + +1. Create an error type that conforms to the error protocol +```swift +extension MyAppError: HTTPResponseConvertible { + var httpStatus: HTTPResponse.Status { + switch self { + case .invalidInputFormat: + .badRequest + case .authorizationError: + .forbidden + } + } +} +``` + +2. Opt in to the error middleware while registering the handler + +```swift +let handler = try await RequestHandler() +try handler.registerHandlers(on: transport, middlewares: [ErrorHandlingMiddleware()]) + +``` + +### API stability + +This feature is purely additive: +- Additional APIs in the runtime library + + +### Future directions + +A possible future direction is to add the error middleware by default by changing the [default value for the middlewares](https://github.com/apple/swift-openapi-runtime/blob/main/Sources/OpenAPIRuntime/Interface/UniversalServer.swift#L56) +argument in handler initialisation. + +### Alternatives considered + +An alternative here is to invoke the error conversion function directly from OpenAPIRuntime's handler. The feature would +still be opt-in as users have to explicitly conform to the new error protocols. + +However, there is a rare case where an application might depend on a library (for eg: an auth library) which in turn +depends on OpenAPIRuntime. If the authentication library conforms to the new error protocols, this would result in a +breaking change for the application, whereas an error middleware provides flexibility to the user on whether they +want to subscribe to the new behaviour or not. diff --git a/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0012.md b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0012.md new file mode 100644 index 00000000..11b1c4cb --- /dev/null +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0012.md @@ -0,0 +1,428 @@ +# SOAR-0012: Generate enums for server variables + +Introduce generator logic to generate Swift enums for server variables that define the 'enum' field. + +## Overview + +- Proposal: SOAR-0012 +- Author(s): [Joshua Asbury](https://github.com/theoriginalbit) +- Status: **Implemented (1.4.0)** +- Issue: [apple/swift-openapi-generator#628](https://github.com/apple/swift-openapi-generator/issues/628) +- Implementation: + - [apple/swift-openapi-generator#618](https://github.com/apple/swift-openapi-generator/pull/618) +- Affected components: + - generator +- Related links: + - [Server variable object](https://spec.openapis.org/oas/latest.html#server-variable-object) +- Versions: + - v1.0 (2024-09-19): Initial version + - v1.1 (2024-10-01): + - Replace the the proposed solution to a purely additive API so it is no longer a breaking change requiring a feature flag + - Moved previous proposed solution to alternatives considered section titled "Replace generation of `serverN` static functions, behind feature flag" + - Moved generation of static computed-property `default` on variable enums to future direction + +### Introduction + +Add generator logic to generate Swift enums for server variables that define the 'enum' field and use Swift String for server variables that only define the 'default' field. + +### Motivation + +The OpenAPI specification for server URL templating defines that fields can define an 'enum' field if substitution options should be restricted to a limited set. + +> | Field Name | Type | Description | +> | --- | --- | --- | +> | enum | [string] | An enumeration of string values to be used if the substitution options are from a limited set. The array MUST NOT be empty. | +> | default | string | REQUIRED. The default value to use for substitution, which SHALL be sent if an alternate value is not supplied. Note this behavior is different the Schema Object’s treatment of default values, because in those cases parameter values are optional. If the enum is defined, the value MUST exist the enum’s values. | +> | description | string | An optional description for the server variable. [CommonMark] syntax MAY be used for rich text representation. | +> +> — source: https://spec.openapis.org/oas/latest.html#server-variable-object + +The current implementation of the generator component offer the enum field values via strings that are embedded within the static function implementation and not exposed to the adopter. Relying on the runtime extension `URL.init(validatingOpenAPIServerURL:variables:)` to verify the string provided matches the allowed values. + +Consider the following example +```yaml +servers: + - url: https://{environment}.example.com/api/{version} + description: Example service deployment. + variables: + environment: + description: Server environment. + default: prod + enum: + - prod + - staging + - dev + version: + default: v1 +``` + +The currently generated code: +```swift +/// Server URLs defined in the OpenAPI document. +internal enum Servers { + /// Server environment. + /// + /// - Parameters: + /// - environment: + /// - version: + internal static func server1( + environment: Swift.String = "prod", + version: Swift.String = "v1" + ) throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://{environment}.example.com/api/{version}", + variables: [ + .init( + name: "environment", + value: environment, + allowedValues: [ + "prod", + "staging", + "dev" + ] + ), + .init( + name: "version", + value: version + ) + ] + ) + } +} +``` + +This means the adopter needs to rely on the runtime checks as to whether their supplied string was valid. Additionally if the OpenAPI document were to ever remove an option it could only be discovered at runtime. + +```swift +let serverURL = try Servers.server1(environment: "stg") // might be a valid environment, might not +``` + +### Proposed solution + +Server variables that define enum values can instead be generated as Swift enums. Providing important information (including code completion) about allowed values to adopters, and providing compile-time guarantees that a valid variable has been supplied. + +Using the same configuration example, from the motivation section above, the newly generated code would be: +```swift +/// Server URLs defined in the OpenAPI document. +internal enum Servers { + /// Example service deployment. + internal enum Server1 { + /// Server environment. + /// + /// The "environment" variable defined in the OpenAPI document. The default value is ``prod``. + internal enum Environment: Swift.String { + case prod + case staging + case dev + } + /// + /// - Parameters: + /// - environment: Server environment. + /// - version: + internal static func url( + environment: Environment = Environment.prod, + version: Swift.String = "v1" + ) throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://{environment}.example.com/api/{version}", + variables: [ + .init( + name: "environment", + value: environment.rawValue + ), + .init( + name: "version", + value: version + ) + ] + ) + } + } + /// Example service deployment. + /// + /// - Parameters: + /// - environment: Server environment. + /// - version: + @available(*, deprecated, message: "Migrate to the new type-safe API for server URLs.") + internal static func server1( + environment: Swift.String = "prod", + version: Swift.String = "v1" + ) throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://{environment}.example.com/api/{version}", + variables: [ + .init( + name: "environment", + value: environment, + allowedValues: [ + "prod", + "staging", + "dev" + ] + ), + .init( + name: "version", + value: version + ) + ] + ) + } +} +``` + +This leaves the existing implementation untouched, except for the addition of a deprecation message, and introduces a new type-safe structure that allows the compiler to validate the provided arguments. + +```swift +let url = try Servers.Server1.url() // ✅ compiles + +let url = try Servers.Server1.url(environment: .default) // ✅ compiles + +let url = try Servers.Server1.url(environment: .staging) // ✅ compiles + +let url = try Servers.Server1.url(environment: .stg) // ❌ compiler error, 'stg' not defined on the enum +``` + +Later if the OpenAPI document removes an enum value that was previously allowed, the compiler will be able to alert the adopter. +```swift +// some time later "staging" gets removed from OpenAPI document +let url = try Servers.Server1.url(environment: . staging) // ❌ compiler error, 'staging' not defined on the enum +``` + +#### Default only variables + +As seen in the generated code example, variables that do not define an 'enum' field will still remain a string (see the 'version' variable). + +### Detailed design + +Implementation: https://github.com/apple/swift-openapi-generator/pull/618 + +The implementation of `translateServers(_:)` is modified to generate the relevant namespaces (enums) for each server, deprecate the existing generated functions, and generate a new more type-safe function. A new file `translateServersVariables` has been created to contain implementations of the two generator kinds; enum and string. + +The server namespace contains a newly named `url` static function which serves the same purpose as the `serverN` static functions generated as members of the `Servers` namespace; it has been named `url` to both be more expressive and because the containing namespace already provides the server context. + +The server namespace also lends the purpose of containing the variable enums, should they be required, since servers may declare variables that are named the same but contain different enum values. e.g. +```yaml +servers: + - url: https://{env}.example.com + variables: + environment: + default: prod + enum: + - prod + - staging + - url: https://{env}.example2.com + variables: + environment: + default: prod + enum: + - prod + - dev +``` +The above would generate the following (simplified for clarity) output +```swift +enum Servers { + enum Server1 { + enum Environment: String { + // ... + } + static func url(/* ... */) throws -> Foundation.URL { /* omitted for brevity */ } + } + enum Server2 { + enum Environment: String { + // ... + } + static func url(/* ... */) throws -> Foundation.URL { /* omitted for brevity */ } + } + + static func server1(/* ... */) throws -> Foundation.URL { /* existing implementation omitted for brevity */ } + static func server2(/* ... */) throws -> Foundation.URL { /* existing implementation omitted for brevity */ } +} +``` + +Server variables that have names or enum values that are not safe to be used as a Swift identifier will be converted. E.g. +```swift +enum Servers { + enum Server1 { + enum _Protocol: String { + case https + case https + } + enum Port: String { + case _443 = "443" + case _8443 = "8443" + } + static func url(/* ... */) throws -> Foundation.URL { /* omitted for brevity */ } + } +} +``` + +#### Deeper into the implementation + +To handle the branching logic of whether a variable will be generated as a string or an enum a new protocol, `TranslatedServerVariable`, defines the common behaviours that may need to occur within each branch. This includes: +- any required declarations +- the parameters for the server's static function +- the expression for the variable initializer in the static function's body +- the parameter description for the static function's documentation + +There are two concrete implementations of this protocol to handle the two branching paths in logic + +##### `RawStringTranslatedServerVariable` + +This concrete implementation will not provide a declaration for generated enum. + +It will define the parameter using `Swift.String` and a default value that is a String representation of the OpenAPI document defined default field. + +The generated initializer expression will match the existing implementation of a variable that does not define an enum field. + +Note: While the feature flag for this proposal is disabled this type is also used to generate the initializer expression to include the enum field as the allowed values parameter. + +##### `GeneratedEnumTranslatedServerVariable` + +This concrete implementation will provide an enum declaration which represents the variable's enum field and a static computed property to access the default. + +The parameter will reference a fully-qualified path to the generated enum declaration and have a default value of the fully qualified path to the static property accessor. + +The initializer expression will never need to provide the allowed values parameter and only needs to provide the `rawValue` of the enum. + +### API stability + +This proposal creates new generated types and modifies the existing generated static functions to include a deprecation, therefore is a non-breaking change for adopters. + +#### Other components + +No API changes are required to other components, though once this proposal is adopted the runtime component _could_ remove the runtime validation of allowed values since the generated code guarantees the `rawValue` is in the document. + +### Future directions + +#### Variable enums could have a static computed-property convenience, called `default`, generated + +Each server variable enum could generate a static computed-property with the name `default` which returns the case as defined by the OpenAPI document. e.g. +```swift +enum Servers { + enum Variables { + enum Server1 { + enum Environment: Swift.String { + case prod + case staging + case dev + static var `default`: Environment { + return Environment.prod + } + } + } + } +``` +This would allow the server's static function to use `default` as the default parameter instead of using a specific case. + +### Alternatives considered + +#### Generate all variables as Swift enums + +A previous implementation had generated all variables as a swift enum, even if the 'enum' field was not defined in the document. An example +```yaml +servers: + - url: https://example.com/api/{version} + variables: + version: + default: v1 +``` +Would have been generated as +```swift +/// Server URLs defined in the OpenAPI document. +internal enum Servers { + internal enum Variables { + /// The variables for Server1 defined in the OpenAPI document. + internal enum Server1 { + /// The "version" variable defined in the OpenAPI document. + /// + /// The default value is "v1". + internal enum Version: Swift.String { + case v1 + /// The default variable. + internal static var `default`: Version { + return Version.v1 + } + } + } + } + /// + /// - Parameters: + /// - version: + internal static func server1(version: Variables.Server1.Version = Variables.Server1.Version.default) throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://example.com/api/{version}", + variables: [ + .init( + name: "version", + value: version.rawValue + ) + ] + ) + } +} +``` +This approach was reconsidered due to the wording in the OpenAPI specification of both the 'enum' and 'default' fields. + +> An enumeration of string values to be used if the substitution options are from a limited set. The array MUST NOT be empty. | +> +> The default value to use for substitution, which SHALL be sent if an alternate value is not supplied. +> +> — source: https://spec.openapis.org/oas/latest.html#server-variable-object + +This indicates that by providing enum values the options are restricted, whereas a default value is provided when no other value is supplied. + +#### Replace generation of `serverN` static functions, behind feature flag + +This approach was considered to be added behind a feature flag as it would introduce breaking changes for adopters that didn't use default values; it would completely rewrite the static functions to accept enum variables as Swift enums. + +An example of the output, using the same configuration example from the motivation section above, this approach would generate the following code: +```swift +/// Server URLs defined in the OpenAPI document. +internal enum Servers { + /// Server URL variables defined in the OpenAPI document. + internal enum Variables { + /// The variables for Server1 defined in the OpenAPI document. + internal enum Server1 { + /// Server environment. + /// + /// The "environment" variable defined in the OpenAPI document. The default value is "prod". + internal enum Environment: Swift.String { + case prod + case staging + case dev + /// The default variable. + internal static var `default`: Environment { + return Environment.prod + } + } + } + } + /// Example service deployment. + /// + /// - Parameters: + /// - environment: Server environment. + /// - version: + internal static func server1( + environment: Variables.Server1.Environment = Variables.Server1.Environment.default, + version: Swift.String = "v1" + ) throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://example.com/api", + variables: [ + .init( + name: "environment", + value: environment.rawValue + ), + .init( + name: "version", + value: version + ) + ] + ) + } +} +``` + +The variables were scoped within a `Variables` namespace for clarity, and each server had its own namespace to avoid collisions of names between different servers. + +Ultimately this approach was decided against due to lack of discoverability since it would have to be feature flagged. diff --git a/Sources/swift-openapi-generator/Documentation.docc/Swift-OpenAPI-Generator.md b/Sources/swift-openapi-generator/Documentation.docc/Swift-OpenAPI-Generator.md index 79ef2abd..4584cf57 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Swift-OpenAPI-Generator.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Swift-OpenAPI-Generator.md @@ -107,6 +107,8 @@ The generated code, runtime library, and transports are supported on more platfo | Generator plugin and CLI | ✅ 10.15+ | ✅ | ✖️ | ✖️ | ✖️ | ✖️ | | Generated code and runtime library | ✅ 10.15+ | ✅ | ✅ 13+ | ✅ 13+ | ✅ 6+ | ✅ 1+ | +> Note: When using Visual Studio Code or other editors that rely on [SourceKit-LSP](https://github.com/swiftlang/sourcekit-lsp), the editor may not correctly recognize generated code within the same module. As a workaround, consider creating a separate target for code generation and then importing it into your main module. For more details, see the discussion in [swiftlang/sourcekit-lsp#665](https://github.com/swiftlang/sourcekit-lsp/issues/665#issuecomment-2093169169). + ### Documentation and example projects To get started, check out the topics below, or one of the tutorials. diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/Adding-openapi-and-swagger-ui-endpoints.tutorial b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/Adding-openapi-and-swagger-ui-endpoints.tutorial index f6cbef00..d09639ba 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/Adding-openapi-and-swagger-ui-endpoints.tutorial +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/Adding-openapi-and-swagger-ui-endpoints.tutorial @@ -2,7 +2,7 @@ @Intro(title: "Adding OpenAPI and Swagger UI endpoints") { One of the most popular ways to share your OpenAPI document with your users is to host it alongside your API server itself. - Typically this is at `/openapi.yaml` or similar, which serves a plain-text OpenAPI document for consumpion by clients. + Typically this is at `/openapi.yaml` or similar, which serves a plain-text OpenAPI document for consumption by clients. Additionally, you can host an HTML page that renders the OpenAPI document as interactive documentation that you can use from the browser, for example using [swagger-ui](https://github.com/swagger-api/swagger-ui). @@ -37,7 +37,7 @@ This step is necessary because the Vapor middleware serves files relative to the current working directory for the running server process. } @Step { - Test this enpoint in your browser, or using curl. + Test this endpoint in your browser, or using curl. @Code(name: "console", file: server-openapi-endpoints.console.2.txt, previousFile: server-openapi-endpoints.console.1.txt) } diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/ClientSwiftPM.tutorial b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/ClientSwiftPM.tutorial index 734920f3..11f1a22a 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/ClientSwiftPM.tutorial +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/ClientSwiftPM.tutorial @@ -122,7 +122,7 @@ @Step { Next we'll create an instance of our client. - Note: `Servers.server2()` is the localhost service, defined in the OpenAPI document. + Note: `Servers.Server2.url()` is the localhost service, defined in the OpenAPI document. @Code(name: "main.swift", file: client.main.2.swift) } @Step { diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/ClientXcode.tutorial b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/ClientXcode.tutorial index 7e7d81ab..4ace8297 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/ClientXcode.tutorial +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/ClientXcode.tutorial @@ -50,7 +50,7 @@ @Step { Add the two configuration files required by the Swift OpenAPI Generator build plugin. - The first is the OpenAPI document. Add it to to the "GreetingServiceClient" target by right-clicking on the "GreetingServiceClient" folder in the project navigator, and choosing Add Files to "GreetingServiceClient"… + The first is the OpenAPI document. Add it to to the "GreetingServiceClient" target by right-clicking on the "GreetingServiceClient" folder in the project navigator, choosing "New Empty File", and pasting the OpenAPI document on the right. @Code(name: "Sources/openapi.yaml", file: client.openapi.yaml) } @Step { @@ -64,6 +64,11 @@ @Code(name: "Sources/openapi-generator-config.yaml", file: client.openapi-generator-config.yaml) } + @Step { + Include the two files in the target by going to the Build Phases tab of the "GreetingServiceClient" target in the Project Editor, and adding the two files to the "Compile Sources" section. + + If you skip this step, you will see the error "Issues with required files: No config file found in the target...". + } @Step { With the configuration files in place, we will add the following three package dependencies: the build plugin, the Runtime library, and a concrete client transport that uses URLSession to send HTTP requests. @@ -91,7 +96,7 @@ Click the plus button and add the OpenAPIGenerator plugin. } @Step { - To verify everything is configured correctly, choose Product -> Build. If this is the first time using the plugin, you will be asked for confirmation that you trust the plugin. To continue, click Trust & Enable All. + To verify everything is configured correctly, choose Product -> Build. If this is the first time using the plugin, you will encounter a build error with the message `"OpenAPIGenerator" is disabled`. To continue, click on the error in the Issue Navigator, click "Trust & Enable", and choose Product -> Build again. Xcode now builds the Swift OpenAPI Generator plugin itself, and then runs it on the configuration files `openapi.yaml` and `openapi-generator-config.yaml` to generate a Swift client for GreetingService. Once it finishes, the `Client` type will become available in the GreetingServiceClient target. } @@ -119,7 +124,7 @@ @Step { Next we'll create an instance of the generated client. - Note: `Servers.server2()` is the localhost service, defined in the OpenAPI document. + Note: `Servers.Server2.url()` is the localhost service, defined in the OpenAPI document. @Code(name: "GreetingClient.swift", file: client.xcode.2.swift) } diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.2.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.2.swift index bb80bcc8..dad11f0a 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.2.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.2.swift @@ -2,6 +2,6 @@ import OpenAPIRuntime import OpenAPIURLSession let client = Client( - serverURL: try Servers.server2(), + serverURL: try Servers.Server2.url(), transport: URLSessionTransport() ) diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.3.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.3.swift index 36acbcba..9de0b4bd 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.3.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.3.swift @@ -2,7 +2,7 @@ import OpenAPIRuntime import OpenAPIURLSession let client = Client( - serverURL: try Servers.server2(), + serverURL: try Servers.Server2.url(), transport: URLSessionTransport() ) diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.4.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.4.swift index 1cb79a1b..82a02588 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.4.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.4.swift @@ -2,7 +2,7 @@ import OpenAPIRuntime import OpenAPIURLSession let client = Client( - serverURL: try Servers.server2(), + serverURL: try Servers.Server2.url(), transport: URLSessionTransport() ) diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.5.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.5.swift index 082116fc..ced62d28 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.5.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.5.swift @@ -2,7 +2,7 @@ import OpenAPIRuntime import OpenAPIURLSession let client = Client( - serverURL: try Servers.server2(), + serverURL: try Servers.Server2.url(), transport: URLSessionTransport() ) diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.6.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.6.swift index 7da6993b..f4983960 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.6.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.6.swift @@ -2,7 +2,7 @@ import OpenAPIRuntime import OpenAPIURLSession let client = Client( - serverURL: try Servers.server2(), + serverURL: try Servers.Server2.url(), transport: URLSessionTransport() ) diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.7.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.7.swift index 9b13e4e8..44c4f254 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.7.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.main.7.swift @@ -2,7 +2,7 @@ import OpenAPIRuntime import OpenAPIURLSession let client = Client( - serverURL: try Servers.server2(), + serverURL: try Servers.Server2.url(), transport: URLSessionTransport() ) diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.2.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.2.swift index 75971079..c6e25733 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.2.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.2.swift @@ -6,7 +6,7 @@ public struct GreetingClient { public func getGreeting(name: String?) async throws -> String { let client = Client( - serverURL: try Servers.server2(), + serverURL: try Servers.Server2.url(), transport: URLSessionTransport() ) } diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.3.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.3.swift index 721e2049..a5e94b38 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.3.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.3.swift @@ -6,7 +6,7 @@ public struct GreetingClient { public func getGreeting(name: String?) async throws -> String { let client = Client( - serverURL: try Servers.server2(), + serverURL: try Servers.Server2.url(), transport: URLSessionTransport() ) let response = try await client.getGreeting(query: .init(name: name)) diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.4.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.4.swift index 384f0eb2..7ac5c24b 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.4.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.4.swift @@ -6,7 +6,7 @@ public struct GreetingClient { public func getGreeting(name: String?) async throws -> String { let client = Client( - serverURL: try Servers.server2(), + serverURL: try Servers.Server2.url(), transport: URLSessionTransport() ) let response = try await client.getGreeting(query: .init(name: name)) diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.5.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.5.swift index da9d9511..3b3684ce 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.5.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.5.swift @@ -6,7 +6,7 @@ public struct GreetingClient { public func getGreeting(name: String?) async throws -> String { let client = Client( - serverURL: try Servers.server2(), + serverURL: try Servers.Server2.url(), transport: URLSessionTransport() ) let response = try await client.getGreeting(query: .init(name: name)) diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.6.2.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.6.2.swift index 33c14bd3..843dadb1 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.6.2.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.6.2.swift @@ -6,7 +6,7 @@ public struct GreetingClient { public func getGreeting(name: String?) async throws -> String { let client = Client( - serverURL: try Servers.server2(), + serverURL: try Servers.Server2.url(), transport: URLSessionTransport() ) let response = try await client.getGreeting(query: .init(name: name)) diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.6.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.6.swift index 5bd073eb..2967d27b 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.6.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/client.xcode.6.swift @@ -6,7 +6,7 @@ public struct GreetingClient { public func getGreeting(name: String?) async throws -> String { let client = Client( - serverURL: try Servers.server2(), + serverURL: try Servers.Server2.url(), transport: URLSessionTransport() ) let response = try await client.getGreeting(query: .init(name: name)) diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server-openapi-endpoints.main.0.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server-openapi-endpoints.main.0.swift index 86e1d8b1..53565ec0 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server-openapi-endpoints.main.0.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server-openapi-endpoints.main.0.swift @@ -21,12 +21,12 @@ let app = Vapor.Application() let transport = VaporTransport(routesBuilder: app) // Create an instance of your handler type that conforms the generated protocol -// defininig your service API. +// defining your service API. let handler = GreetingServiceAPIImpl() // Call the generated function on your implementation to add its request // handlers to the app. -try handler.registerHandlers(on: transport, serverURL: Servers.server1()) +try handler.registerHandlers(on: transport, serverURL: Servers.Server1.url()) // Start the app as you would normally. try await app.execute() diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server-openapi-endpoints.main.1.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server-openapi-endpoints.main.1.swift index 2fe97bc9..a26b7e0a 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server-openapi-endpoints.main.1.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server-openapi-endpoints.main.1.swift @@ -21,12 +21,12 @@ let app = Vapor.Application() let transport = VaporTransport(routesBuilder: app) // Create an instance of your handler type that conforms the generated protocol -// defininig your service API. +// defining your service API. let handler = GreetingServiceAPIImpl() // Call the generated function on your implementation to add its request // handlers to the app. -try handler.registerHandlers(on: transport, serverURL: Servers.server1()) +try handler.registerHandlers(on: transport, serverURL: Servers.Server1.url()) // Add Vapor middleware to serve the contents of the Public/ directory. app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server-openapi-endpoints.main.2.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server-openapi-endpoints.main.2.swift index 43b083d8..af9f16c4 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server-openapi-endpoints.main.2.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server-openapi-endpoints.main.2.swift @@ -21,12 +21,12 @@ let app = Vapor.Application() let transport = VaporTransport(routesBuilder: app) // Create an instance of your handler type that conforms the generated protocol -// defininig your service API. +// defining your service API. let handler = GreetingServiceAPIImpl() // Call the generated function on your implementation to add its request // handlers to the app. -try handler.registerHandlers(on: transport, serverURL: Servers.server1()) +try handler.registerHandlers(on: transport, serverURL: Servers.Server1.url()) // Add Vapor middleware to serve the contents of the Public/ directory. app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.main.1.2.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.main.1.2.swift index 86e1d8b1..53565ec0 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.main.1.2.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.main.1.2.swift @@ -21,12 +21,12 @@ let app = Vapor.Application() let transport = VaporTransport(routesBuilder: app) // Create an instance of your handler type that conforms the generated protocol -// defininig your service API. +// defining your service API. let handler = GreetingServiceAPIImpl() // Call the generated function on your implementation to add its request // handlers to the app. -try handler.registerHandlers(on: transport, serverURL: Servers.server1()) +try handler.registerHandlers(on: transport, serverURL: Servers.Server1.url()) // Start the app as you would normally. try await app.execute() diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.main.2.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.main.2.swift index 3d20764b..27ebe994 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.main.2.swift +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server.main.2.swift @@ -29,12 +29,12 @@ let app = Vapor.Application() let transport = VaporTransport(routesBuilder: app) // Create an instance of your handler type that conforms the generated protocol -// defininig your service API. +// defining your service API. let handler = GreetingServiceAPIImpl() // Call the generated function on your implementation to add its request // handlers to the app. -try handler.registerHandlers(on: transport, serverURL: Servers.server1()) +try handler.registerHandlers(on: transport, serverURL: Servers.Server1.url()) // Start the app as you would normally. try await app.execute() diff --git a/Tests/OpenAPIGeneratorCoreTests/Hooks/Test_FilteredDocument.swift b/Tests/OpenAPIGeneratorCoreTests/Hooks/Test_FilteredDocument.swift index ce41a7ac..5e86e060 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Hooks/Test_FilteredDocument.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Hooks/Test_FilteredDocument.swift @@ -28,6 +28,8 @@ final class Test_FilteredDocument: XCTestCase { - name: t paths: /things/a: + parameters: + - $ref: '#/components/parameters/A' get: operationId: getA tags: @@ -52,6 +54,12 @@ final class Test_FilteredDocument: XCTestCase { type: string B: $ref: '#/components/schemas/A' + parameters: + A: + in: query + schema: + type: string + name: A responses: A: description: success @@ -75,14 +83,16 @@ final class Test_FilteredDocument: XCTestCase { filter: DocumentFilter(tags: ["t"]), hasPaths: ["/things/a"], hasOperations: ["getA"], - hasSchemas: ["A"] + hasSchemas: ["A"], + hasParameters: ["A"] ) assert( filtering: document, filter: DocumentFilter(paths: ["/things/a"]), hasPaths: ["/things/a"], hasOperations: ["getA", "deleteA"], - hasSchemas: ["A"] + hasSchemas: ["A"], + hasParameters: ["A"] ) assert( filtering: document, @@ -96,7 +106,8 @@ final class Test_FilteredDocument: XCTestCase { filter: DocumentFilter(paths: ["/things/a", "/things/b"]), hasPaths: ["/things/a", "/things/b"], hasOperations: ["getA", "deleteA", "getB"], - hasSchemas: ["A", "B"] + hasSchemas: ["A", "B"], + hasParameters: ["A"] ) assert( filtering: document, @@ -117,21 +128,24 @@ final class Test_FilteredDocument: XCTestCase { filter: DocumentFilter(paths: ["/things/a"], schemas: ["B"]), hasPaths: ["/things/a"], hasOperations: ["getA", "deleteA"], - hasSchemas: ["A", "B"] + hasSchemas: ["A", "B"], + hasParameters: ["A"] ) assert( filtering: document, filter: DocumentFilter(tags: ["t"], schemas: ["B"]), hasPaths: ["/things/a"], hasOperations: ["getA"], - hasSchemas: ["A", "B"] + hasSchemas: ["A", "B"], + hasParameters: ["A"] ) assert( filtering: document, filter: DocumentFilter(operations: ["deleteA"]), hasPaths: ["/things/a"], hasOperations: ["deleteA"], - hasSchemas: [] + hasSchemas: [], + hasParameters: ["A"] ) } @@ -141,6 +155,7 @@ final class Test_FilteredDocument: XCTestCase { hasPaths paths: [OpenAPI.Path.RawValue], hasOperations operationIDs: [String], hasSchemas schemas: [String], + hasParameters parameters: [String] = [], file: StaticString = #filePath, line: UInt = #line ) { @@ -149,11 +164,31 @@ final class Test_FilteredDocument: XCTestCase { XCTFail("Filter threw error: \(error)", file: file, line: line) return } - XCTAssertUnsortedEqual(filteredDocument.paths.keys.map(\.rawValue), paths, file: file, line: line) - XCTAssertUnsortedEqual(filteredDocument.allOperationIds, operationIDs, file: file, line: line) + XCTAssertUnsortedEqual( + filteredDocument.paths.keys.map(\.rawValue), + paths, + "Paths don't match", + file: file, + line: line + ) + XCTAssertUnsortedEqual( + filteredDocument.allOperationIds, + operationIDs, + "Operations don't match", + file: file, + line: line + ) XCTAssertUnsortedEqual( filteredDocument.components.schemas.keys.map(\.rawValue), schemas, + "Schemas don't match", + file: file, + line: line + ) + XCTAssertUnsortedEqual( + filteredDocument.components.parameters.keys.map(\.rawValue), + parameters, + "Parameters don't match", file: file, line: line ) diff --git a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift index 3f9373b9..f7638d87 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift @@ -21,7 +21,9 @@ final class Test_YamsParser: Test_Core { XCTAssertNoThrow(try _test(openAPIVersionString: "3.0.1")) XCTAssertNoThrow(try _test(openAPIVersionString: "3.0.2")) XCTAssertNoThrow(try _test(openAPIVersionString: "3.0.3")) + XCTAssertNoThrow(try _test(openAPIVersionString: "3.0.4")) XCTAssertNoThrow(try _test(openAPIVersionString: "3.1.0")) + XCTAssertNoThrow(try _test(openAPIVersionString: "3.1.1")) let expected1 = "/foo.yaml: error: Unsupported document version: openapi: 3.2.0. Please provide a document with OpenAPI versions in the 3.0.x or 3.1.x sets." @@ -54,7 +56,7 @@ final class Test_YamsParser: Test_Core { """ let expected = - "/foo.yaml: error: No openapi key found, please provide a valid OpenAPI document with OpenAPI versions in the 3.0.x or 3.1.x sets." + "/foo.yaml: error: No key named openapi found. Please provide a valid OpenAPI document with OpenAPI versions in the 3.0.x or 3.1.x sets." assertThrownError(try _test(yaml), expectedDiagnostic: expected) } diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift index 818aff50..47b0390d 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift @@ -154,12 +154,32 @@ extension APIProtocol { /// Server URLs defined in the OpenAPI document. public enum Servers { /// Example Petstore implementation service + public enum Server1 { + /// Example Petstore implementation service + public static func url() throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://example.com/api", + variables: [] + ) + } + } + /// Example Petstore implementation service + @available(*, deprecated, renamed: "Servers.Server1.url") public static func server1() throws -> Foundation.URL { try Foundation.URL( validatingOpenAPIServerURL: "https://example.com/api", variables: [] ) } + public enum Server2 { + public static func url() throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "/api", + variables: [] + ) + } + } + @available(*, deprecated, renamed: "Servers.Server2.url") public static func server2() throws -> Foundation.URL { try Foundation.URL( validatingOpenAPIServerURL: "/api", @@ -167,12 +187,56 @@ public enum Servers { ) } /// A custom domain. + public enum Server3 { + /// The "port" variable defined in the OpenAPI document. The default value is "443". + @frozen public enum Port: Swift.String, Sendable { + case _443 = "443" + case _8443 = "8443" + } + /// A custom domain. + /// + /// - Parameters: + /// - _protocol: + /// - subdomain: A subdomain name. + /// - port: + /// - basePath: The base API path. + public static func url( + _protocol: Swift.String = "https", + subdomain: Swift.String = "test", + port: Port = ._443, + basePath: Swift.String = "v1" + ) throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "{protocol}://{subdomain}.example.com:{port}/{basePath}", + variables: [ + .init( + name: "protocol", + value: _protocol + ), + .init( + name: "subdomain", + value: subdomain + ), + .init( + name: "port", + value: port.rawValue + ), + .init( + name: "basePath", + value: basePath + ) + ] + ) + } + } + /// A custom domain. /// /// - Parameters: /// - _protocol: /// - subdomain: A subdomain name. /// - port: /// - basePath: The base API path. + @available(*, deprecated, renamed: "Servers.Server3.url") public static func server3( _protocol: Swift.String = "https", subdomain: Swift.String = "test", @@ -2215,6 +2279,14 @@ public enum Operations { /// /// HTTP response code: `204 noContent`. case noContent(Operations.createPetWithForm.Output.NoContent) + /// Successfully created pet using a url form + /// + /// - Remark: Generated from `#/paths//pets/create/post(createPetWithForm)/responses/204`. + /// + /// HTTP response code: `204 noContent`. + public static var noContent: Self { + .noContent(.init()) + } /// The associated value of the enum case if `self` is `.noContent`. /// /// - Throws: An error if `self` is not `.noContent`. @@ -2435,6 +2507,14 @@ public enum Operations { /// /// HTTP response code: `202 accepted`. case accepted(Operations.postStats.Output.Accepted) + /// Accepted data. + /// + /// - Remark: Generated from `#/paths//pets/stats/post(postStats)/responses/202`. + /// + /// HTTP response code: `202 accepted`. + public static var accepted: Self { + .accepted(.init()) + } /// The associated value of the enum case if `self` is `.accepted`. /// /// - Throws: An error if `self` is not `.accepted`. @@ -2477,6 +2557,14 @@ public enum Operations { /// /// HTTP response code: `204 noContent`. case noContent(Operations.probe.Output.NoContent) + /// Ack + /// + /// - Remark: Generated from `#/paths//probe//post(probe)/responses/204`. + /// + /// HTTP response code: `204 noContent`. + public static var noContent: Self { + .noContent(.init()) + } /// The associated value of the enum case if `self` is `.noContent`. /// /// - Throws: An error if `self` is not `.noContent`. @@ -2562,6 +2650,14 @@ public enum Operations { /// /// HTTP response code: `204 noContent`. case noContent(Operations.updatePet.Output.NoContent) + /// Successfully updated + /// + /// - Remark: Generated from `#/paths//pets/{petId}/patch(updatePet)/responses/204`. + /// + /// HTTP response code: `204 noContent`. + public static var noContent: Self { + .noContent(.init()) + } /// The associated value of the enum case if `self` is `.noContent`. /// /// - Throws: An error if `self` is not `.noContent`. @@ -3034,6 +3130,14 @@ public enum Operations { /// /// HTTP response code: `202 accepted`. case accepted(Operations.multipartUploadTyped.Output.Accepted) + /// Successfully accepted the data. + /// + /// - Remark: Generated from `#/paths//pets/multipart-typed/post(multipartUploadTyped)/responses/202`. + /// + /// HTTP response code: `202 accepted`. + public static var accepted: Self { + .accepted(.init()) + } /// The associated value of the enum case if `self` is `.accepted`. /// /// - Throws: An error if `self` is not `.accepted`. diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index 010e91c2..c46bbc43 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -1310,6 +1310,33 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } + func testComponentsSchemasStringEnumWithDuplicates() throws { + try self.assertSchemasTranslation( + ignoredDiagnosticMessages: ["Duplicate enum value, skipping"], + """ + schemas: + MyEnum: + type: string + enum: + - one + - two + - three + - two + - four + """, + """ + public enum Schemas { + @frozen public enum MyEnum: String, Codable, Hashable, Sendable, CaseIterable { + case one = "one" + case two = "two" + case three = "three" + case four = "four" + } + } + """ + ) + } + func testComponentsSchemasIntEnum() throws { try self.assertSchemasTranslation( """ @@ -5458,6 +5485,298 @@ final class SnippetBasedReferenceTests: XCTestCase { """ ) } + + func testServerWithNoVariables() throws { + try self.assertServersTranslation( + """ + - url: https://example.com/api + """, + """ + public enum Servers { + public enum Server1 { + public static func url() throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://example.com/api", + variables: [] + ) + } + } + @available(*, deprecated, renamed: "Servers.Server1.url") + public static func server1() throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://example.com/api", + variables: [] + ) + } + } + """ + ) + } + + func testServerWithDefaultVariable() throws { + try self.assertServersTranslation( + """ + - url: '{protocol}://example.com/api' + description: A custom domain. + variables: + protocol: + default: https + description: A network protocol. + """, + """ + public enum Servers { + public enum Server1 { + public static func url(_protocol: Swift.String = "https") throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "{protocol}://example.com/api", + variables: [ + .init( + name: "protocol", + value: _protocol + ) + ] + ) + } + } + @available(*, deprecated, renamed: "Servers.Server1.url") + public static func server1(_protocol: Swift.String = "https") throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "{protocol}://example.com/api", + variables: [ + .init( + name: "protocol", + value: _protocol + ) + ] + ) + } + } + """ + ) + } + + func testServerWithDefaultAndEnumVariables() throws { + try self.assertServersTranslation( + """ + - url: 'https://{environment}.example.com/api/{version}' + description: A custom domain. + variables: + environment: + enum: + - production + - sandbox + default: production + version: + default: v1 + """, + """ + public enum Servers { + public enum Server1 { + @frozen public enum Environment: Swift.String, Sendable { + case production + case sandbox + } + public static func url( + environment: Environment = .production, + version: Swift.String = "v1" + ) throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://{environment}.example.com/api/{version}", + variables: [ + .init( + name: "environment", + value: environment.rawValue + ), + .init( + name: "version", + value: version + ) + ] + ) + } + } + @available(*, deprecated, renamed: "Servers.Server1.url") + public static func server1( + environment: Swift.String = "production", + version: Swift.String = "v1" + ) throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://{environment}.example.com/api/{version}", + variables: [ + .init( + name: "environment", + value: environment, + allowedValues: [ + "production", + "sandbox" + ] + ), + .init( + name: "version", + value: version + ) + ] + ) + } + } + """ + ) + } + + func testServersMultipleServers() throws { + try self.assertServersTranslation( + """ + - url: 'https://{environment}.example.com/api/{version}' + description: A custom domain. + variables: + environment: + enum: + - production + - sandbox + default: production + version: + default: v1 + - url: 'https://{environment}.api.example.com/' + variables: + environment: + enum: + - sandbox + - develop + default: develop + - url: 'https://example.com/api/{version}' + description: Vanity URL for production.example.com/api/{version} + variables: + version: + default: v1 + - url: 'https://api.example.com/' + description: Vanity URL for production.api.example.com + """, + """ + public enum Servers { + public enum Server1 { + @frozen public enum Environment: Swift.String, Sendable { + case production + case sandbox + } + public static func url( + environment: Environment = .production, + version: Swift.String = "v1" + ) throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://{environment}.example.com/api/{version}", + variables: [ + .init( + name: "environment", + value: environment.rawValue + ), + .init( + name: "version", + value: version + ) + ] + ) + } + } + @available(*, deprecated, renamed: "Servers.Server1.url") + public static func server1( + environment: Swift.String = "production", + version: Swift.String = "v1" + ) throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://{environment}.example.com/api/{version}", + variables: [ + .init( + name: "environment", + value: environment, + allowedValues: [ + "production", + "sandbox" + ] + ), + .init( + name: "version", + value: version + ) + ] + ) + } + public enum Server2 { + @frozen public enum Environment: Swift.String, Sendable { + case sandbox + case develop + } + public static func url(environment: Environment = .develop) throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://{environment}.api.example.com/", + variables: [ + .init( + name: "environment", + value: environment.rawValue + ) + ] + ) + } + } + @available(*, deprecated, renamed: "Servers.Server2.url") + public static func server2(environment: Swift.String = "develop") throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://{environment}.api.example.com/", + variables: [ + .init( + name: "environment", + value: environment, + allowedValues: [ + "sandbox", + "develop" + ] + ) + ] + ) + } + public enum Server3 { + public static func url(version: Swift.String = "v1") throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://example.com/api/{version}", + variables: [ + .init( + name: "version", + value: version + ) + ] + ) + } + } + @available(*, deprecated, renamed: "Servers.Server3.url") + public static func server3(version: Swift.String = "v1") throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://example.com/api/{version}", + variables: [ + .init( + name: "version", + value: version + ) + ] + ) + } + public enum Server4 { + public static func url() throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://api.example.com/", + variables: [] + ) + } + } + @available(*, deprecated, renamed: "Servers.Server4.url") + public static func server4() throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://api.example.com/", + variables: [] + ) + } + } + """ + ) + } } extension SnippetBasedReferenceTests { @@ -5483,6 +5802,18 @@ extension SnippetBasedReferenceTests { components: components ) } + func makeTypesTranslator( + accessModifier: AccessModifier = .public, + featureFlags: FeatureFlags = [], + ignoredDiagnosticMessages: Set = [], + components: OpenAPI.Components = .noComponents + ) throws -> TypesFileTranslator { + TypesFileTranslator( + config: Config(mode: .types, access: accessModifier, featureFlags: featureFlags), + diagnostics: XCTestDiagnosticCollector(test: self, ignoredDiagnosticMessages: ignoredDiagnosticMessages), + components: components + ) + } func makeTranslators( components: OpenAPI.Components = .noComponents, @@ -5746,6 +6077,21 @@ extension SnippetBasedReferenceTests { let (registerHandlersDecl, _) = try translator.translateRegisterHandlers(operations) try XCTAssertSwiftEquivalent(registerHandlersDecl, expectedSwift, file: file, line: line) } + + func assertServersTranslation( + _ serversYAML: String, + _ expectedSwift: String, + accessModifier: AccessModifier = .public, + featureFlags: FeatureFlags = [], + file: StaticString = #filePath, + line: UInt = #line + ) throws { + continueAfterFailure = false + let servers = try YAMLDecoder().decode([OpenAPI.Server].self, from: serversYAML) + let translator = try makeTypesTranslator(accessModifier: accessModifier, featureFlags: featureFlags) + let translation = translator.translateServers(servers) + try XCTAssertSwiftEquivalent(translation, expectedSwift, file: file, line: line) + } } private func XCTAssertEqualWithDiff( diff --git a/Tests/PetstoreConsumerTests/Test_Types.swift b/Tests/PetstoreConsumerTests/Test_Types.swift index eb1822ab..7aaba0cc 100644 --- a/Tests/PetstoreConsumerTests/Test_Types.swift +++ b/Tests/PetstoreConsumerTests/Test_Types.swift @@ -222,25 +222,13 @@ final class Test_Types: XCTestCase { verifyingJSON: #"{"name":"C","parent":{"nested":{"name":"B","parent":{"nested":{"name":"A"}}}}}"# ) } - func testServers_1() throws { XCTAssertEqual(try Servers.server1(), URL(string: "https://example.com/api")) } - func testServers_2() throws { XCTAssertEqual(try Servers.server2(), URL(string: "/api")) } + func testServers_1() throws { XCTAssertEqual(try Servers.Server1.url(), URL(string: "https://example.com/api")) } + func testServers_2() throws { XCTAssertEqual(try Servers.Server2.url(), URL(string: "/api")) } func testServers_3() throws { - XCTAssertEqual(try Servers.server3(), URL(string: "https://test.example.com:443/v1")) + XCTAssertEqual(try Servers.Server3.url(), URL(string: "https://test.example.com:443/v1")) XCTAssertEqual( - try Servers.server3(subdomain: "bar", port: "8443", basePath: "v2/staging"), + try Servers.Server3.url(subdomain: "bar", port: ._8443, basePath: "v2/staging"), URL(string: "https://bar.example.com:8443/v2/staging") ) - XCTAssertThrowsError(try Servers.server3(port: "foo")) { error in - guard - case let .invalidServerVariableValue(name: name, value: value, allowedValues: allowedValues) = error - as? RuntimeError - else { - XCTFail("Expected error, but not this: \(error)") - return - } - XCTAssertEqual(name, "port") - XCTAssertEqual(value, "foo") - XCTAssertEqual(allowedValues, ["443", "8443"]) - } } }