From 37118769f5d88c1432774d39fe833c69666b411c Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 7 Oct 2024 17:20:44 +0200 Subject: [PATCH 01/20] [CI] Disable the API breakage check (#647) ### Motivation This package doesn't have any library products that would need to maintain API stability, so we don't need to run the CI. We only have `_OpenAPIGeneratorCore`, which is underscored and not expected to be API stable (so it can only be used using an `exact: "..."` constraint). But we don't want to fail CI when we change that API (right now it's introducing noise in: https://github.com/apple/swift-openapi-generator/pull/627) ### Modifications Disable API breakage check. ### Result Disabled that CI step. ### Test Plan N/A --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 8c598a5a..5d6d85c3 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -9,7 +9,7 @@ jobs: name: Soundness uses: apple/swift-nio/.github/workflows/soundness.yml@main with: - api_breakage_check_enabled: true + api_breakage_check_enabled: false broken_symlink_check_enabled: true docs_check_enabled: true format_check_enabled: true From 7893a6c542c9be486378011ca90e8487c8159cbf Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 10 Oct 2024 14:03:07 +0200 Subject: [PATCH 02/20] ci: Use soundness workflow from swiftlang/github-workflows (#648) This moves to the reusable soundness workflow in the swiftlang organization. --- .github/workflows/pull_request.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 5d6d85c3..206c129e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -7,7 +7,7 @@ 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: false broken_symlink_check_enabled: true @@ -17,6 +17,7 @@ jobs: license_header_check_project_name: "SwiftOpenAPIGenerator" shell_check_enabled: true unacceptable_language_check_enabled: true + yamllint_enabled: false unit-tests: name: Unit tests From a8e142e77e23e74aa471a7811d651e1ded3457c5 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 11 Oct 2024 18:38:24 +0200 Subject: [PATCH 03/20] Fix CI - renamed upstream GHA workflow (#650) ### Motivation The parameter got renamed: https://github.com/swiftlang/github-workflows/pull/18 ### Modifications Update our workflow file. ### Result Hopefully working CI. ### Test Plan Will see once this is merged. --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 206c129e..83d09104 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -17,7 +17,7 @@ jobs: license_header_check_project_name: "SwiftOpenAPIGenerator" shell_check_enabled: true unacceptable_language_check_enabled: true - yamllint_enabled: false + yamllint_check_enabled: false unit-tests: name: Unit tests From ef6d07f1e615423c1cfdbf9d25a98458eaddbd19 Mon Sep 17 00:00:00 2001 From: Joshua Asbury <1377564+theoriginalbit@users.noreply.github.com> Date: Sat, 12 Oct 2024 04:17:16 +1100 Subject: [PATCH 04/20] Generate enums for server variables (#618) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Motivation Refer to proposal https://github.com/apple/swift-openapi-generator/pull/629
PR description prior to raising proposal ### Motivation Recently in a project I was using a spec which defined variables similar to below ```yaml servers: - url: https://{environment}.example.com/api/{version} variables: environment: default: prod enum: - prod - staging - dev version: default: v1 ``` The generated code to create the default server URL was easy enough being able to utilise the default parameters ```swift let serverURL = try Servers.server1() ``` But when I wanted to use a different variable I noticed that the parameter was generated as a string and it didn't expose the other allowed values that were defined in the OpenAPI document. It generated the following code: ```swift /// Server URLs defined in the OpenAPI document. internal enum Servers { /// /// - 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 meant usage needed to involve runtime checks whether the supplied variable was valid and 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 ``` Looking into the OpenAPI spec for server templating and the implementation of the extension `URL.init(validatingOpenAPIServerURL:variables:)` I realised that the variables could very easily be represented by an enum in the generated code. By doing so it would also provide a compiler checked way to use a non-default variable. ### Modifications I have introduced a new set of types translator functions in the file `translateServersVariables.swift` which can create the enum declarations for the variables. If there are no variables defined then no declaration is generated. Each variable defined in the OpenAPI document is generated as an enum with a case that represents each enum in the document. Each enum is also generated with a static computed property with the name `default` which returns the default value as required by the OpenAPI spec. These individual variable enums are then namespaced according to the server they are applicable for, for example `Server1`, allowing servers to have identically named variables with different enum values. Finally each of the server namespace enums are members of a final namespace, `Variables`, which exists as a member of the pre-existing `Servers` namespace. A truncated example: ```swift enum Servers { // enum generated prior to this PR enum Variables { enum Server1 { enum VariableName1 { // ... } enum VariableName2 { // ... } } } static func server1(/* ... */) throws -> Foundation.URL { /* declaration prior to this PR */ } } ``` To use the new translator functions the `translateServers` function has been modified to call the `translateServersVariables` function and insert the declarations as a member alongside the existing static functions for each of the servers. The `translateServer(index:server:)` function was also edited to make use of the generated variable enums, and the code which generated the string array for `allowedValues` has been removed; runtime validation should no longer be required, as the `rawValue` of a variable enum is the value defined in the OpenAPI document. ### Result The following spec ```yaml servers: - url: https://{environment}.example.com/api/ variables: environment: default: prod enum: - prod - staging - dev ``` Would currently generate to the output ```swift /// Server URLs defined in the OpenAPI document. internal enum Servers { /// /// - Parameters: /// - environment: internal static func server1(environment: Swift.String = "prod") throws -> Foundation.URL { try Foundation.URL( validatingOpenAPIServerURL: "https://{environment}.example.com/api/", variables: [ .init( name: "environment", value: environment, allowedValues: [ "prod", "staging", "dev" ] ) ] ) } } ``` But with this PR it would generate to be ```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 { /// 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 } } } } /// /// - Parameters: /// - environment: internal static func server1(environment: Variables.Server1.Environment = Variables.Server1.Environment.default) throws -> Foundation.URL { try Foundation.URL( validatingOpenAPIServerURL: "https://{environment}.example.com/api/", variables: [ .init( name: "environment", value: environment.rawValue ) ] ) } } ``` Now when it comes to usage ```swift let url = try Servers.server1() // ✅ works let url = try Servers.server1(environment: .default) // ✅ works let url = try Servers.server1(environment: .staging) // ✅ works let url = try Servers.server1(environment: .stg) // ❌ compiler error, stg not defined on the enum // some time later staging gets removed from OpenAPI document let url = try Servers.server1(environment: . staging) // ❌ compiler error, staging not defined on the enum ``` If the document does not define enum values for the variable, an enum is still generated with a single member (the default required by the spec). ```yaml servers: - url: https://example.com/api/{version} variables: version: default: v1 ``` Before this PR: ```swift /// Server URLs defined in the OpenAPI document. internal enum Servers { /// /// - Parameters: /// - version: internal static func server1(version: Swift.String = "v1") throws -> Foundation.URL { try Foundation.URL( validatingOpenAPIServerURL: "https://example.com/api/{version}", variables: [ .init( name: "version", value: version ) ] ) } } ``` With this PR: ```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 { /// 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 ) ] ) } } ```
### Result Refer to https://github.com/apple/swift-openapi-generator/pull/618#issuecomment-2346037222 ### Test Plan I have updated the petstore unit tests to reflect the changes made in this PR, see diff. --------- Co-authored-by: Honza Dvorsky --- .../StructuredSwiftRepresentation.swift | 10 +- .../Translator/CommonTypes/Constants.swift | 12 + .../TypesTranslator/translateServers.swift | 147 ++++++-- .../translateServersVariables.swift | 237 +++++++++++++ .../Tutorials/ClientSwiftPM.tutorial | 2 +- .../Tutorials/ClientXcode.tutorial | 2 +- .../Tutorials/_Resources/client.main.2.swift | 2 +- .../Tutorials/_Resources/client.main.3.swift | 2 +- .../Tutorials/_Resources/client.main.4.swift | 2 +- .../Tutorials/_Resources/client.main.5.swift | 2 +- .../Tutorials/_Resources/client.main.6.swift | 2 +- .../Tutorials/_Resources/client.main.7.swift | 2 +- .../Tutorials/_Resources/client.xcode.2.swift | 2 +- .../Tutorials/_Resources/client.xcode.3.swift | 2 +- .../Tutorials/_Resources/client.xcode.4.swift | 2 +- .../Tutorials/_Resources/client.xcode.5.swift | 2 +- .../_Resources/client.xcode.6.2.swift | 2 +- .../Tutorials/_Resources/client.xcode.6.swift | 2 +- .../server-openapi-endpoints.main.0.swift | 2 +- .../server-openapi-endpoints.main.1.swift | 2 +- .../server-openapi-endpoints.main.2.swift | 2 +- .../_Resources/server.main.1.2.swift | 2 +- .../Tutorials/_Resources/server.main.2.swift | 2 +- .../ReferenceSources/Petstore/Types.swift | 64 ++++ .../SnippetBasedReferenceTests.swift | 318 ++++++++++++++++++ Tests/PetstoreConsumerTests/Test_Types.swift | 20 +- 26 files changed, 773 insertions(+), 73 deletions(-) create mode 100644 Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServersVariables.swift 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/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/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/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..d9987b94 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/ClientXcode.tutorial +++ b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/ClientXcode.tutorial @@ -119,7 +119,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..6e1b07ac 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 @@ -26,7 +26,7 @@ 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..525132bd 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 @@ -26,7 +26,7 @@ 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..c1df5d52 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 @@ -26,7 +26,7 @@ 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..6e1b07ac 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 @@ -26,7 +26,7 @@ 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..eee61a52 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 @@ -34,7 +34,7 @@ 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/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift index 818aff50..2baf58a2 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", diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index f4811396..ba5e5905 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -5181,6 +5181,297 @@ 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 { @@ -5206,6 +5497,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, @@ -5469,6 +5772,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"]) - } } } From dffd9d100f1137803ac43d13714861b00f0acef7 Mon Sep 17 00:00:00 2001 From: Joshua Asbury <1377564+theoriginalbit@users.noreply.github.com> Date: Sat, 12 Oct 2024 05:30:44 +1100 Subject: [PATCH 05/20] [Proposal] SOAR-0012 Generate enums for server variables (#629) ### Motivation As requested by @czechboy0 in #618 I have created this proposal for community feedback. ### Modifications Added the proposal. Also fixed a typo in the document for the proposal process. ### Result N/A ### Test Plan N/A --------- Co-authored-by: Honza Dvorsky --- .../Documentation.docc/Proposals/Proposals.md | 3 +- .../Documentation.docc/Proposals/SOAR-0012.md | 428 ++++++++++++++++++ 2 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0012.md diff --git a/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md b/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md index 752cc0a6..43b79807 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,4 @@ If you have any questions, tag [Honza Dvorsky](https://github.com/czechboy0) or - - - +- 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. From 58a276ac955bd50e3d252a9e79ebabdfe0d54b03 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 15 Oct 2024 11:21:48 +0100 Subject: [PATCH 06/20] [CI] Update to Swift 6 CI (#652) # Motivation We just updated our CI matrix in NIO to only support 5.9, 5.10 and 6. # Modification This PR updates the trigger files in this repo. Since this repo was always 5.9+ this is easy. # Result Up to date CI --- .github/workflows/pull_request.yml | 10 +--------- .github/workflows/scheduled.yml | 4 +--- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 83d09104..930f97ea 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -10,22 +10,16 @@ jobs: uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main with: api_breakage_check_enabled: false - broken_symlink_check_enabled: true - docs_check_enabled: true - format_check_enabled: true - license_header_check_enabled: true 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 @@ -35,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: @@ -63,7 +56,6 @@ 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: diff --git a/.github/workflows/scheduled.yml b/.github/workflows/scheduled.yml index 6c367b83..ccbb203b 100644 --- a/.github/workflows/scheduled.yml +++ b/.github/workflows/scheduled.yml @@ -9,9 +9,9 @@ jobs: 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_arguments_override: "--explicit-target-dependency-import-check error" @@ -21,7 +21,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 example-packages: name: Example packages @@ -29,4 +28,3 @@ jobs: with: name: "Example packages" matrix_linux_command: "./scripts/test-examples.sh" - matrix_linux_5_8_enabled: false From fa0d335f674db94c574225959d53a04d282319be Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 17 Oct 2024 11:18:41 +0200 Subject: [PATCH 07/20] [Docs] Explain how to enable plugins in Xcode and Xcode Cloud (#653) ### Motivation By default, adopters hit this error both in Xcode and Xcode Cloud and might not be familiar with how to resolve it. ### Modifications Expand the FAQ to cover how to enable plugins in Xcode and Xcode Cloud. ### Result Better docs, adopters can find a solution easier. ### Test Plan N/A --- .../Articles/Frequently-asked-questions.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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..41eb825b 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 @@ -71,3 +71,23 @@ 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 + +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). From ecdaa49ac3810c9caa1058c1e81809d429e42d2b Mon Sep 17 00:00:00 2001 From: Joshua Asbury <1377564+theoriginalbit@users.noreply.github.com> Date: Fri, 18 Oct 2024 16:24:15 +1100 Subject: [PATCH 08/20] Update FAQ script to include note on correctness of spelling mistake (#654) --- .../Articles/Frequently-asked-questions.md | 5 +++-- .../Articles/Practicing-spec-driven-API-development.md | 2 +- .../Adding-openapi-and-swagger-ui-endpoints.tutorial | 4 ++-- .../_Resources/server-openapi-endpoints.main.0.swift | 2 +- .../_Resources/server-openapi-endpoints.main.1.swift | 2 +- .../_Resources/server-openapi-endpoints.main.2.swift | 2 +- .../Tutorials/_Resources/server.main.1.2.swift | 2 +- .../Tutorials/_Resources/server.main.2.swift | 2 +- 8 files changed, 11 insertions(+), 10 deletions(-) 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 41eb825b..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. @@ -87,6 +87,7 @@ In Xcode Cloud, add the script `ci_scripts/ci_post_clone.sh` next to your Xcode 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 ``` 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/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/_Resources/server-openapi-endpoints.main.0.swift b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/_Resources/server-openapi-endpoints.main.0.swift index 6e1b07ac..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,7 +21,7 @@ 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 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 525132bd..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,7 +21,7 @@ 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 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 c1df5d52..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,7 +21,7 @@ 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 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 6e1b07ac..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,7 +21,7 @@ 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 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 eee61a52..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,7 +29,7 @@ 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 From 9727261219af435e7e668b8813187e9bf5d61927 Mon Sep 17 00:00:00 2001 From: Arthur Crocquevieille Date: Mon, 21 Oct 2024 19:43:24 +0200 Subject: [PATCH 09/20] Generate a static property to create a responses without any parameters (#656) --- .../Responses/translateResponseOutcome.swift | 47 ++++++++++++++----- .../TypesTranslator/translateOperations.swift | 3 +- .../ReferenceSources/Petstore/Types.swift | 40 ++++++++++++++++ 3 files changed, 77 insertions(+), 13 deletions(-) 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/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift index 2baf58a2..47b0390d 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift @@ -2279,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`. @@ -2499,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`. @@ -2541,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`. @@ -2626,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`. @@ -3098,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`. From 924cf2f7e39c74aec51ef7eb0c2732c7c5e4b04b Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 25 Oct 2024 20:14:30 +0200 Subject: [PATCH 10/20] Fix printing to stderr - add a missing newline (#657) --- Sources/_OpenAPIGeneratorCore/Diagnostics.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 7b4dd6b781f8e3658f75273fc2a8d11767f119d7 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 25 Oct 2024 20:48:20 +0200 Subject: [PATCH 11/20] Include path item parameters in filter (#658) ### Motivation When filtering by a specific operationId, any path item-level parameters were not included (only operation-level parameters). ### Modifications Include path item level parameters. ### Result No errors for missing references when filtering for operations that have a path item-level parameter with a reference. ### Test Plan Adapted unit tests to cover this. --- .../Hooks/FilteredDocument.swift | 1 + .../Hooks/Test_FilteredDocument.swift | 51 ++++++++++++++++--- 2 files changed, 44 insertions(+), 8 deletions(-) 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/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 ) From 827fa729ae786e74d06e279efac2b90a66261c72 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 4 Nov 2024 10:42:33 +0100 Subject: [PATCH 12/20] [Docs] Update Xcode tutorial for Xcode 16 (#662) ### Motivation Xcode 16 shipped and our Xcode tutorial wasn't accurate anymore. ### Modifications Update the tutorial to match Xcode 16's UI. ### Result Tutorial is more accurate for newcomers who install Xcode 16 and try out Swift OpenAPI Generator. ### Test Plan Manually went through it. --- .../Documentation.docc/Tutorials/ClientXcode.tutorial | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Sources/swift-openapi-generator/Documentation.docc/Tutorials/ClientXcode.tutorial b/Sources/swift-openapi-generator/Documentation.docc/Tutorials/ClientXcode.tutorial index d9987b94..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. } From 759c30bdc7438089cab7274b2aaa8389c3c27ed5 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 4 Nov 2024 11:27:13 +0100 Subject: [PATCH 13/20] Add support for OpenAPI 3.0.4 and 3.1.1 (#665) ### Motivation OpenAPI 3.0.4 and 3.1.1 dropped, mostly containing clarifications: https://www.openapis.org/blog/2024/10/25/announcing-openapi-specification-patch-releases ### Modifications - Bumped the OpenAPIKit dependency to get the new versions parsed. - Added the two new versions to our enum. - Added unit tests. ### Result We can parse the new versions. ### Test Plan Added unit tests. --- Package.swift | 2 +- Sources/_OpenAPIGeneratorCore/Parser/YamsParser.swift | 4 ++-- Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 25151e5d..1043f555 100644 --- a/Package.swift +++ b/Package.swift @@ -51,7 +51,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"), // 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 diff --git a/Sources/_OpenAPIGeneratorCore/Parser/YamsParser.swift b/Sources/_OpenAPIGeneratorCore/Parser/YamsParser.swift index dfd605f0..467b001b 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)", diff --git a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift index 3f9373b9..7b681bce 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." From 8cbeb8bad89c0793bacbb6ce600ee596b9d7fffd Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Thu, 14 Nov 2024 11:11:27 +0000 Subject: [PATCH 14/20] unify scheduled and main yamls (#670) --- .github/workflows/main.yml | 32 ++++++++++++++++++++++++++++++++ .github/workflows/scheduled.yml | 30 ------------------------------ 2 files changed, 32 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/main.yml delete mode 100644 .github/workflows/scheduled.yml 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/scheduled.yml b/.github/workflows/scheduled.yml deleted file mode 100644 index ccbb203b..00000000 --- a/.github/workflows/scheduled.yml +++ /dev/null @@ -1,30 +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_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" From 7fcef1d0f6b17a5dd2e3213615328578265dc8ab Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Fri, 15 Nov 2024 09:54:50 +0000 Subject: [PATCH 15/20] remove unused Swift 6 language mode workflow (#672) remove unused Swift 6 language mode workflow --- .github/workflows/pull_request.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 930f97ea..1935f77a 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -57,8 +57,3 @@ jobs: name: "Example packages" matrix_linux_command: "./scripts/test-examples.sh" 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. From ecce1b7b765168898f7dd3e984848e3fc96de2ad Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 19 Nov 2024 20:13:40 +0100 Subject: [PATCH 16/20] Skip duplicate enum values (#674) ### Motivation Some OpenAPI docs, usually through imperfect conversion from another representation, end up with duplicate raw values in `enum` schemas. The OpenAPI specification (by referencing the JSON Schema specification) [says](https://datatracker.ietf.org/doc/html/draft-wright-json-schema-validation-00#section-5.20): ``` The value of this keyword MUST be an array. This array SHOULD have at least one element. Elements in the array SHOULD be unique. ``` So elements **should** be unique, but don't have to be. Today the generator fails to generate documents with such duplicate values. ### Modifications Gracefully handle duplicate values by emitting a warning diagnostic and skipping it. ### Result This unblocks generating OpenAPI documents with duplicate enum values, such as the [Bluesky OpenAPI doc](https://github.com/bluesky-social/bsky-docs/blob/main/atproto-openapi-types/spec/api.json). ### Test Plan Added a unit test for duplicates. --- Package.swift | 5 +- .../CommonTranslations/translateRawEnum.swift | 140 ++++++++++++++++++ .../translateStringEnum.swift | 86 ----------- .../SnippetBasedReferenceTests.swift | 27 ++++ 4 files changed, 171 insertions(+), 87 deletions(-) create mode 100644 Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateRawEnum.swift delete mode 100644 Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift diff --git a/Package.swift b/Package.swift index 1043f555..36b1d24c 100644 --- a/Package.swift +++ b/Package.swift @@ -49,6 +49,7 @@ 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.3.0"), @@ -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/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/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index ba5e5905..05e9c70e 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( """ From 31fa50a3f213d855b9425e3de9aa91ffa89acd5d Mon Sep 17 00:00:00 2001 From: Deirdre Saoirse Moen Date: Thu, 21 Nov 2024 22:27:59 -0800 Subject: [PATCH 17/20] Make error message for missing "openapi" key clearer. (#678) --- Sources/_OpenAPIGeneratorCore/Parser/YamsParser.swift | 2 +- Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Parser/YamsParser.swift b/Sources/_OpenAPIGeneratorCore/Parser/YamsParser.swift index 467b001b..4e462091 100644 --- a/Sources/_OpenAPIGeneratorCore/Parser/YamsParser.swift +++ b/Sources/_OpenAPIGeneratorCore/Parser/YamsParser.swift @@ -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/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift index 7b681bce..f7638d87 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift @@ -56,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) } From 3ecfcfac7c046717fe3ed1bc5f129996aa1173c1 Mon Sep 17 00:00:00 2001 From: gayathrisairam <168187165+gayathrisairam@users.noreply.github.com> Date: Fri, 22 Nov 2024 07:26:56 +0000 Subject: [PATCH 18/20] [Proposal] SOAR-0011: Improved error handling (#626) ### Motivation Swift OpenAPI runtime doesn't allow for fine grained error handling. This PR adds a proposal for improved error handling in Swift OpenAPI runtime. ### Modifications - Add SOAR-011: Improved error handling. (See the [proposal](https://github.com/gayathrisairam/swift-openapi-generator/blob/error_handling/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0011.md) for details) ### Result n/a ### Test Plan n/a --------- Co-authored-by: Gayathri Sairamkrishnan Co-authored-by: Honza Dvorsky --- .../Documentation.docc/Proposals/Proposals.md | 1 + .../Documentation.docc/Proposals/SOAR-0011.md | 159 ++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0011.md diff --git a/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md b/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md index 43b79807..0bc8421e 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md @@ -52,4 +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. From a99ba2fe76f601c8a109e2a539d48d6c793ed7d5 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Fri, 22 Nov 2024 07:49:51 +0000 Subject: [PATCH 19/20] add .editorconfig file (#671) add .editorconfig file Co-authored-by: George Barnett --- .editorconfig | 8 ++++++++ .licenseignore | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .editorconfig 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/.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 From e971af5c5374e4ea83c20d0ad0a0df2cc84d39ad Mon Sep 17 00:00:00 2001 From: Tobias Date: Sat, 23 Nov 2024 15:20:27 +0100 Subject: [PATCH 20/20] Add a note about an open issue with code generation and SourceKit-LSP (#642) Following the discussion on Slack, this adds a note for users of SourceKit-LSP informing them that the default code generation might not work as expected and offers a possible workaround. https://swift-open-source.slack.com/archives/C02PV8T5HQD/p1726815970093299 Only updates documentation. Let me know if you feel this is the right place for this information? --- README.md | 3 +++ .../Documentation.docc/Swift-OpenAPI-Generator.md | 2 ++ 2 files changed, 5 insertions(+) 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/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.