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"]) - } } }