Skip to content

Commit

Permalink
Update proposal with latest from discussion on forums
Browse files Browse the repository at this point in the history
  • Loading branch information
theoriginalbit committed Oct 1, 2024
1 parent bc99d26 commit 777db9c
Showing 1 changed file with 162 additions and 107 deletions.
269 changes: 162 additions & 107 deletions Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0012.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ Introduce generator logic to generate Swift enums for server variables that defi
- 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
- 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

Expand Down Expand Up @@ -55,6 +60,7 @@ The currently generated code:
```swift
/// Server URLs defined in the OpenAPI document.
internal enum Servers {
/// Server environment.
///
/// - Parameters:
/// - environment:
Expand Down Expand Up @@ -95,43 +101,64 @@ let serverURL = try Servers.server1(environment: "stg") // might be a valid envi

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:
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 {
/// 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.
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: Variables.Server1.Environment = Variables.Server1.Environment.default,
environment: Swift.String = "prod",
version: Swift.String = "v1"
) throws -> Foundation.URL {
try Foundation.URL(
validatingOpenAPIServerURL: "https://example.com/api",
validatingOpenAPIServerURL: "https://{environment}.example.com/api/{version}",
variables: [
.init(
name: "environment",
value: environment.rawValue
value: environment,
allowedValues: [
"prod",
"staging",
"dev"
]
),
.init(
name: "version",
Expand All @@ -143,54 +170,37 @@ internal enum Servers {
}
```

This would allow the compiler to validate the provided value.
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() // ✅ compiles
let url = try Servers.Server1.url() // ✅ compiles

let url = try Servers.server1(environment: .default) // ✅ compiles
let url = try Servers.Server1.url(environment: .default) // ✅ compiles

let url = try Servers.server1(environment: .staging) // ✅ compiles
let url = try Servers.Server1.url(environment: .staging) // ✅ compiles

let url = try Servers.server1(environment: .stg) // ❌ compiler error, 'stg' not defined on the enum
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(environment: . staging) // ❌ compiler error, 'staging' not defined on the enum
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.
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) and enums for variables, should they be required.
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.

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.
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.

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.
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
Expand All @@ -200,70 +210,52 @@ servers:
enum:
- prod
- staging
- url: https://{env}.example2.com
variables:
environment:
default: prod
enum:
- prod
- dev
- 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 Server1 {
enum Environment: String {
// ...
}
enum Server2 {
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(/* omitted for brevity */) throws -> Foundation.URL { /* omitted for brevity */ }
static func server2(/* omitted for brevity */) 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 Variables {
enum Server1 {
enum _Protocol: String {
case https
case https
}
enum Port: String {
case _443 = "443"
case _8443 = "8443"
}
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 */ }
}
}
```

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:
Expand Down Expand Up @@ -292,28 +284,35 @@ The parameter will reference a fully-qualified path to the generated enum declar

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.

#### New Feature Flag

A feature flag, `serverVariablesAsEnums`, has been introduced to allow opt-in to the changes of this proposal.

When the feature flag is disabled the `RawStringTranslatedServerVariable` generator will be used for **all** variables, resulting in an identical output to the previous generator. However, when the feature flag is enabled the `GeneratedEnumTranslatedServerVariable` generator will be used for any variable that declares an enum field, and the `RawStringTranslatedServerVariable` generator will be used otherwise.

#### Compatibility

Given the previous generation, any adopter that relied on default values provided by the static server functions, e.g. `let url = try Servers.server1()`, will not experience any breaking changes by adopting the implementation from this proposal. The new generators will still provide valid default parameters, even for the generated Swift enums. Adopters that do not rely on the default values, however, will experience compile errors by adopting the changes in this proposal; though migration should be a straight-forward change as adopters were previously unable to provide _any_ value due to runtime validation, so the generated enum cases should have a similar spelling/shape to the previous string counterpart.
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

Nothing comes to mind at this point in time.
#### 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

Expand Down Expand Up @@ -371,3 +370,59 @@ This approach was reconsidered due to the wording in the OpenAPI specification o
> — 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.

0 comments on commit 777db9c

Please sign in to comment.