Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1629,8 +1629,14 @@ extension KeywordKind {

extension Declaration {
/// 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 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, description: @autoclosure () -> DeprecationDescription = .init()) -> Self {
if shouldDeprecate { return .deprecated(description(), self) }
return self
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ 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 configuration type, which is used by both
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,47 +14,19 @@
import OpenAPIKit

extension TypesFileTranslator {

/// Returns a declaration of a server URL static method defined in
/// the OpenAPI document.
/// - 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 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(
deprecated: DeprecationDescription?,
abstract: String?,
name: String,
url: String,
variableGenerators variables: [any ServerVariableGenerator]
) -> Declaration {
return .commentable(
.functionComment(abstract: abstract, 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: [
Expand All @@ -64,15 +36,73 @@ extension TypesFileTranslator {
.call([
.init(
label: "validatingOpenAPIServerURL",
expression: .literal(.string(server.urlTemplate.absoluteString))
), .init(label: "variables", expression: .literal(.array(variableInitializers))),
expression: .literal(.string(url))
),
.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.
/// - 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(
deprecated: DeprecationDescription(renamed: pathToReplacementSymbol),
abstract: server.description,
name: "\(Constants.ServerURL.propertyPrefix)\(index + 1)",
url: server.urlTemplate.absoluteString,
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(deprecated: nil,
abstract: server.description,
name: Constants.ServerURL.urlStaticFunc,
url: server.urlTemplate.absoluteString,
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
Expand All @@ -81,7 +111,16 @@ 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
//===----------------------------------------------------------------------===//
//
// 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

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.
/// - 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] {
return 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 server variable and the function of generation that should be applied.
protocol ServerVariableGenerator {
/// Returns the declaration (enum) that should be added to the `Variables.Server#`
/// 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 }
}

/// Represents a variable that is required to be represented as a `Swift.String`.
private struct RawStringTranslatedServerVariable: ServerVariableGenerator {
let key: String
let swiftSafeKey: String
let variable: OpenAPI.Server.Variable

init(key: String, variable: OpenAPI.Server.Variable, context: TranslatorContext) {
self.key = key
swiftSafeKey = context.asSwiftSafeName(key)
self.variable = variable
}

/// A variable being represented by a `Swift.String` does not have a declaration that needs to
/// be added to the `Variables.Server#` namespace.
var declaration: Declaration? { 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 {
return .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 a variable that will be generated as an enum and added to the `Variables.Server#`
/// namespace. The enum will contain a `default` static case which returns the default defined in
/// the OpenAPI Document.
private struct GeneratedEnumTranslatedServerVariable: ServerVariableGenerator {
let key: String
let swiftSafeKey: String
let enumName: String
let variable: OpenAPI.Server.Variable
let enumValues: [String]

let accessModifier: AccessModifier
let context: TranslatorContext

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 `Variables.Server#`
/// namespace.
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: [
TypeName.string.fullyQualifiedSwiftName,
],
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 {
return .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)
if caseName == name {
return .enumCase(name: caseName, kind: .nameOnly)
} else {
return .enumCase(name: caseName, kind: .nameWithRawValue(.string(name)))
}
}
}
}
Loading