From 9abfdfdcf65ec3dc7380b1aa589ef5a2aca32d41 Mon Sep 17 00:00:00 2001 From: theoriginalbit <1377564+theoriginalbit@users.noreply.github.com> Date: Wed, 18 Sep 2024 22:47:16 +1000 Subject: [PATCH] [Proposal] SOAR-0011 Generate enums for server variables --- .../Documentation.docc/Proposals/Proposals.md | 3 +- .../Documentation.docc/Proposals/SOAR-0011.md | 366 ++++++++++++++++++ 2 files changed, 368 insertions(+), 1 deletion(-) 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 752cc0a6..0eebf58b 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-0011.md b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0011.md new file mode 100644 index 00000000..6a0699e0 --- /dev/null +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0011.md @@ -0,0 +1,366 @@ +# SOAR-0011: Generate enums for server variables + +Introduce generator logic to generate Swift enums for server variables that define the 'enum' field. + +## Overview + +- Proposal: SOAR-NNNN +- Author(s): [Joshua Asbury](https://github.com/theoriginalbit) +- Status: **Awaiting Review** +- 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) +- Feature flag: `serverVariablesAsEnums` +- Affected components: + - generator + - runtime (optional) +- Related links: + - [Server variable object](https://spec.openapis.org/oas/latest.html#server-variable-object) + +### 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 { + /// + /// - 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 generated code would look like so: +```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 + ) + ] + ) + } +} +``` + +This would allow the compiler to validate the provided value. + +```swift +let url = try Servers.server1() // ✅ compiles + +let url = try Servers.server1(environment: .default) // ✅ compiles + +let url = try Servers.server1(environment: .staging) // ✅ compiles + +let url = try Servers.server1(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(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. + +### Detailed design + +Implementation: https://github.com/apple/swift-openapi-generator/pull/618 + +The implementation of `translateServers(_:)` is modified to generate the relevant namespaces (enums) and enums for variables, should they be required. + +If no variables are defined in the OpenAPI document, or the defined variables do not make use of the 'enum' field, then nothing needs to be generated. + +An additional namespace, `Variables` would be generated, as required, within the (existing) `Servers` namespace. This new `Variables` namespace would contain further namespaces (enums) for each server, named and numbered to match the corresponding static function. These server specific namespaces would then contain enums that represent each of the variables defined in the OpenAPI document for that server. e.g. +```swift +enum Servers { // enum generated prior to this PR + enum Variables { + enum Server1 { + enum VariableName1 { + // ... + } + enum VariableName2 { + // ... + } + } + } + static func server1(/* omitted for brevity */) throws -> Foundation.URL { /* omitted for brevity */ } +} +``` + +This approach was used 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 Variables { + enum Server1 { + enum Environment: String { + // ... + } + } + enum Server2 { + enum Environment: String { + // ... + } + } + } + static func server1(/* omitted for brevity */) throws -> Foundation.URL { /* omitted for brevity */ } + static func server2(/* omitted for brevity */) throws -> Foundation.URL { /* 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 Variables { + enum Server1 { + enum _Protocol: String { + case https + case https + } + enum Port: String { + case _443 = "443" + case _8443 = "8443" + } + } + } +} +``` + +Each server variable enum is also generated with 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 allows the server's static function to use `default` as the default parameter. + +#### 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 for creating/accessing server definitions. + +The change could be backwards compatible to any adopter that relies on default values provided by the static server functions. + +Adopters that do not rely on the default values will have compile errors, though migration should be a straight-forward change as adopters were previously unable to provide _any_ value due to runtime validation; the generated enum cases should have a similar spelling to the previous string counterpart. + +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 + +Nothing comes to mind at this point in time. + +### 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.