Skip to content

Commit ef6d07f

Browse files
Generate enums for server variables (#618)
### Motivation Refer to proposal #629 <details> <summary>PR description prior to raising proposal</summary> ### 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 ) ] ) } } ``` </details> ### Result Refer to #618 (comment) ### Test Plan I have updated the petstore unit tests to reflect the changes made in this PR, see diff. --------- Co-authored-by: Honza Dvorsky <honza@apple.com>
1 parent a8e142e commit ef6d07f

26 files changed

+773
-73
lines changed

Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1628,9 +1628,15 @@ extension KeywordKind {
16281628
}
16291629

16301630
extension Declaration {
1631+
/// Returns a new deprecated variant of the declaration if the provided `description` is not `nil`.
1632+
func deprecate(if description: DeprecationDescription?) -> Self {
1633+
if let description { return .deprecated(description, self) }
1634+
return self
1635+
}
1636+
16311637
/// Returns a new deprecated variant of the declaration if `shouldDeprecate` is true.
1632-
func deprecate(if shouldDeprecate: Bool) -> Self {
1633-
if shouldDeprecate { return .deprecated(.init(), self) }
1638+
func deprecate(if shouldDeprecate: Bool, description: @autoclosure () -> DeprecationDescription = .init()) -> Self {
1639+
if shouldDeprecate { return .deprecated(description(), self) }
16341640
return self
16351641
}
16361642

Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,18 @@ enum Constants {
5656

5757
/// The prefix of each generated method name.
5858
static let propertyPrefix: String = "server"
59+
/// The name of each generated static function.
60+
static let urlStaticFunc: String = "url"
61+
62+
/// The prefix of the namespace that contains server specific variables.
63+
static let serverNamespacePrefix: String = "Server"
64+
65+
/// Constants related to the OpenAPI server variable object.
66+
enum Variable {
67+
68+
/// The types that the protocol conforms to.
69+
static let conformances: [String] = [TypeName.string.fullyQualifiedSwiftName, "Sendable"]
70+
}
5971
}
6072

6173
/// Constants related to the configuration type, which is used by both

Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServers.swift

Lines changed: 111 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,47 +14,47 @@
1414
import OpenAPIKit
1515

1616
extension TypesFileTranslator {
17-
18-
/// Returns a declaration of a server URL static method defined in
19-
/// the OpenAPI document.
17+
/// Returns a declaration of a server URL static function defined in
18+
/// the OpenAPI document using the supplied name identifier and
19+
/// variable generators.
20+
///
21+
/// If the `deprecated` parameter is supplied the static function
22+
/// will be generated with a name that matches the previous, now
23+
/// deprecated API.
24+
///
25+
/// - Important: The variable generators provided should all
26+
/// be ``RawStringTranslatedServerVariable`` to ensure
27+
/// the generated function matches the previous implementation, this
28+
/// is **not** asserted by this translate function.
29+
///
30+
/// If the `deprecated` parameter is `nil` then the function will
31+
/// be generated with the identifier `url` and must be a member
32+
/// of a namespace to avoid conflicts with other server URL static
33+
/// functions.
34+
///
2035
/// - Parameters:
2136
/// - index: The index of the server in the list of servers defined
2237
/// in the OpenAPI document.
2338
/// - server: The server URL information.
39+
/// - deprecated: A deprecation `@available` annotation to attach
40+
/// to this declaration, or `nil` if the declaration should not be deprecated.
41+
/// - variables: The generators for variables the server has defined.
2442
/// - Returns: A static method declaration, and a name for the variable to
2543
/// declare the method under.
26-
func translateServer(index: Int, server: OpenAPI.Server) -> Declaration {
27-
let methodName = "\(Constants.ServerURL.propertyPrefix)\(index+1)"
28-
let safeVariables = server.variables.map { (key, value) in
29-
(originalKey: key, swiftSafeKey: context.asSwiftSafeName(key), value: value)
30-
}
31-
let parameters: [ParameterDescription] = safeVariables.map { (originalKey, swiftSafeKey, value) in
32-
.init(label: swiftSafeKey, type: .init(TypeName.string), defaultValue: .literal(value.default))
33-
}
34-
let variableInitializers: [Expression] = safeVariables.map { (originalKey, swiftSafeKey, value) in
35-
let allowedValuesArg: FunctionArgumentDescription?
36-
if let allowedValues = value.enum {
37-
allowedValuesArg = .init(
38-
label: "allowedValues",
39-
expression: .literal(.array(allowedValues.map { .literal($0) }))
40-
)
41-
} else {
42-
allowedValuesArg = nil
43-
}
44-
return .dot("init")
45-
.call(
46-
[
47-
.init(label: "name", expression: .literal(originalKey)),
48-
.init(label: "value", expression: .identifierPattern(swiftSafeKey)),
49-
] + (allowedValuesArg.flatMap { [$0] } ?? [])
50-
)
51-
}
52-
let methodDecl = Declaration.commentable(
53-
.functionComment(abstract: server.description, parameters: safeVariables.map { ($1, $2.description) }),
44+
private func translateServerStaticFunction(
45+
index: Int,
46+
server: OpenAPI.Server,
47+
deprecated: DeprecationDescription?,
48+
variableGenerators variables: [any ServerVariableGenerator]
49+
) -> Declaration {
50+
let name =
51+
deprecated == nil ? Constants.ServerURL.urlStaticFunc : "\(Constants.ServerURL.propertyPrefix)\(index + 1)"
52+
return .commentable(
53+
.functionComment(abstract: server.description, parameters: variables.map(\.functionComment)),
5454
.function(
5555
accessModifier: config.access,
56-
kind: .function(name: methodName, isStatic: true),
57-
parameters: parameters,
56+
kind: .function(name: name, isStatic: true),
57+
parameters: variables.map(\.parameter),
5858
keywords: [.throws],
5959
returnType: .identifierType(TypeName.url),
6060
body: [
@@ -65,14 +65,78 @@ extension TypesFileTranslator {
6565
.init(
6666
label: "validatingOpenAPIServerURL",
6767
expression: .literal(.string(server.urlTemplate.absoluteString))
68-
), .init(label: "variables", expression: .literal(.array(variableInitializers))),
68+
),
69+
.init(
70+
label: "variables",
71+
expression: .literal(.array(variables.map(\.initializer)))
72+
),
6973
])
7074
)
7175
)
7276
]
7377
)
78+
.deprecate(if: deprecated)
79+
)
80+
}
81+
82+
/// Returns a declaration of a server URL static function defined in
83+
/// the OpenAPI document. The function is marked as deprecated
84+
/// with a message informing the adopter to use the new type-safe
85+
/// API.
86+
/// - Parameters:
87+
/// - index: The index of the server in the list of servers defined
88+
/// in the OpenAPI document.
89+
/// - server: The server URL information.
90+
/// - pathToReplacementSymbol: The Swift path of the symbol
91+
/// which has resulted in the deprecation of this symbol.
92+
/// - Returns: A static function declaration.
93+
func translateServerAsDeprecated(index: Int, server: OpenAPI.Server, renamedTo pathToReplacementSymbol: String)
94+
-> Declaration
95+
{
96+
let serverVariables = translateServerVariables(index: index, server: server, generateAsEnum: false)
97+
return translateServerStaticFunction(
98+
index: index,
99+
server: server,
100+
deprecated: DeprecationDescription(renamed: pathToReplacementSymbol),
101+
variableGenerators: serverVariables
102+
)
103+
}
104+
105+
/// Returns a namespace (enum) declaration for a server defined in
106+
/// the OpenAPI document. Within the namespace are enums to
107+
/// represent any variables that also have enum values defined in the
108+
/// OpenAPI document, and a single static function named 'url' which
109+
/// at runtime returns the resolved server URL.
110+
///
111+
/// The server's namespace is named to identify the human-friendly
112+
/// index of the enum (e.g. Server1) and is present to ensure each
113+
/// server definition's variables do not conflict with one another.
114+
/// - Parameters:
115+
/// - index: The index of the server in the list of servers defined
116+
/// in the OpenAPI document.
117+
/// - server: The server URL information.
118+
/// - Returns: A static function declaration.
119+
func translateServer(index: Int, server: OpenAPI.Server) -> (pathToStaticFunction: String, decl: Declaration) {
120+
let serverVariables = translateServerVariables(index: index, server: server, generateAsEnum: true)
121+
let methodDecl = translateServerStaticFunction(
122+
index: index,
123+
server: server,
124+
deprecated: nil,
125+
variableGenerators: serverVariables
126+
)
127+
let namespaceName = "\(Constants.ServerURL.serverNamespacePrefix)\(index + 1)"
128+
let typeName = TypeName(swiftKeyPath: [
129+
Constants.ServerURL.namespace, namespaceName, Constants.ServerURL.urlStaticFunc,
130+
])
131+
let decl = Declaration.commentable(
132+
server.description.map(Comment.doc(_:)),
133+
.enum(
134+
accessModifier: config.access,
135+
name: namespaceName,
136+
members: serverVariables.compactMap(\.declaration) + CollectionOfOne(methodDecl)
137+
)
74138
)
75-
return methodDecl
139+
return (pathToStaticFunction: typeName.fullyQualifiedSwiftName, decl: decl)
76140
}
77141

78142
/// Returns a declaration of a namespace (enum) called "Servers" that
@@ -81,7 +145,18 @@ extension TypesFileTranslator {
81145
/// - Parameter servers: The servers to include in the extension.
82146
/// - Returns: A declaration of an enum namespace of the server URLs type.
83147
func translateServers(_ servers: [OpenAPI.Server]) -> Declaration {
84-
let serverDecls = servers.enumerated().map(translateServer)
148+
var serverDecls: [Declaration] = []
149+
for (index, server) in servers.enumerated() {
150+
let translatedServer = translateServer(index: index, server: server)
151+
serverDecls.append(contentsOf: [
152+
translatedServer.decl,
153+
translateServerAsDeprecated(
154+
index: index,
155+
server: server,
156+
renamedTo: translatedServer.pathToStaticFunction
157+
),
158+
])
159+
}
85160
return .commentable(
86161
.doc("Server URLs defined in the OpenAPI document."),
87162
.enum(accessModifier: config.access, name: Constants.ServerURL.namespace, members: serverDecls)

0 commit comments

Comments
 (0)