diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 76f71961..83d09104 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -7,9 +7,9 @@ on: jobs: soundness: name: Soundness - uses: apple/swift-nio/.github/workflows/soundness.yml@main + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main with: - api_breakage_check_enabled: true + api_breakage_check_enabled: false broken_symlink_check_enabled: true docs_check_enabled: true format_check_enabled: true @@ -17,6 +17,7 @@ jobs: license_header_check_project_name: "SwiftOpenAPIGenerator" shell_check_enabled: true unacceptable_language_check_enabled: true + yamllint_check_enabled: false unit-tests: name: Unit tests @@ -33,7 +34,7 @@ jobs: uses: apple/swift-nio/.github/workflows/swift_matrix.yml@main with: name: "Integration test" - matrix_linux_command: "apt-get update -yq && apt-get install -yq jq && ./scripts/run-integration-test.sh" + matrix_linux_command: "apt-get update -yq && apt-get install -yq jq && SWIFT_OPENAPI_GENERATOR_REPO_URL=file://${GITHUB_WORKSPACE} ./scripts/run-integration-test.sh" matrix_linux_5_8_enabled: false matrix_linux_nightly_main_enabled: false diff --git a/.github/workflows/scheduled.yml b/.github/workflows/scheduled.yml index cb5f46df..6c367b83 100644 --- a/.github/workflows/scheduled.yml +++ b/.github/workflows/scheduled.yml @@ -20,7 +20,7 @@ jobs: uses: apple/swift-nio/.github/workflows/swift_matrix.yml@main with: name: "Integration test" - matrix_linux_command: "apt-get update -yq && apt-get install -yq jq && ./scripts/run-integration-test.sh" + matrix_linux_command: "apt-get update -yq && apt-get install -yq jq && SWIFT_OPENAPI_GENERATOR_REPO_URL=file://${GITHUB_WORKSPACE} ./scripts/run-integration-test.sh" matrix_linux_5_8_enabled: false example-packages: 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/ClientTranslator/ClientTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift index 29381dfb..4df9a035 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift @@ -37,8 +37,7 @@ struct ClientFileTranslator: FileTranslator { let imports = Constants.File.clientServerImports + config.additionalImports.map { ImportDescription(moduleName: $0) } - let clientMethodDecls = - try OperationDescription.all(from: doc.paths, in: components, asSwiftSafeName: swiftSafeName) + let clientMethodDecls = try OperationDescription.all(from: doc.paths, in: components, context: context) .map(translateClientMethod(_:)) let clientStructPropertyDecl: Declaration = .commentable( diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/SwiftSafeNames.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/SwiftSafeNames.swift index 35d04086..9a52bc04 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/SwiftSafeNames.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/SwiftSafeNames.swift @@ -13,16 +13,7 @@ //===----------------------------------------------------------------------===// import Foundation -extension FileTranslator { - - /// Returns a copy of the string modified to be a valid Swift identifier. - /// - /// - Parameter string: The string to convert to be safe for Swift. - /// - Returns: A Swift-safe version of the input string. - func swiftSafeName(for string: String) -> String { string.safeForSwiftCode } -} - -fileprivate extension String { +extension String { /// Returns a string sanitized to be usable as a Swift identifier. /// diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift index ad7a1b63..37ac8830 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift @@ -64,7 +64,7 @@ extension TypesFileTranslator { parent: typeName ) let associatedDeclarations: [Declaration] - if TypeMatcher.isInlinable(schema) { + if typeMatcher.isInlinable(schema) { associatedDeclarations = try translateSchema( typeName: propertyType.typeName, schema: schema, @@ -78,10 +78,10 @@ extension TypesFileTranslator { originalName: key, typeUsage: propertyType, associatedDeclarations: associatedDeclarations, - asSwiftSafeName: swiftSafeName + context: context ) var referenceStack = ReferenceStack.empty - let isKeyValuePairSchema = try TypeMatcher.isKeyValuePair( + let isKeyValuePairSchema = try typeMatcher.isKeyValuePair( schema, referenceStack: &referenceStack, components: components @@ -173,7 +173,7 @@ extension TypesFileTranslator { parent: typeName ) let associatedDeclarations: [Declaration] - if TypeMatcher.isInlinable(schema) { + if typeMatcher.isInlinable(schema) { associatedDeclarations = try translateSchema( typeName: childType.typeName, schema: schema, @@ -183,7 +183,7 @@ extension TypesFileTranslator { associatedDeclarations = [] } var referenceStack = ReferenceStack.empty - let isKeyValuePair = try TypeMatcher.isKeyValuePair( + let isKeyValuePair = try typeMatcher.isKeyValuePair( schema, referenceStack: &referenceStack, components: components @@ -209,7 +209,7 @@ extension TypesFileTranslator { let decoder: Declaration if let discriminator { let originalName = discriminator.propertyName - let swiftName = swiftSafeName(for: originalName) + let swiftName = context.asSwiftSafeName(originalName) codingKeysDecls = [ .enum( accessModifier: config.access, diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateObjectStruct.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateObjectStruct.swift index 4b48e2e1..e2680fe6 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateObjectStruct.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateObjectStruct.swift @@ -85,7 +85,7 @@ extension TypesFileTranslator { parent: typeName ) let associatedDeclarations: [Declaration] - if TypeMatcher.isInlinable(value) { + if typeMatcher.isInlinable(value) { associatedDeclarations = try translateSchema( typeName: propertyType.typeName, schema: value, @@ -100,7 +100,7 @@ extension TypesFileTranslator { originalName: key, typeUsage: propertyType, associatedDeclarations: associatedDeclarations, - asSwiftSafeName: swiftSafeName + context: context ) } @@ -153,7 +153,7 @@ extension TypesFileTranslator { components: components, inParent: parent ) - if TypeMatcher.isInlinable(schema) { + if typeMatcher.isInlinable(schema) { associatedDeclarations = try translateSchema( typeName: valueTypeUsage.typeName, schema: schema, @@ -175,7 +175,7 @@ extension TypesFileTranslator { default: .emptyInit, isSerializedInTopLevelDictionary: false, associatedDeclarations: associatedDeclarations, - asSwiftSafeName: swiftSafeName + context: context ) return (.allowingAdditionalProperties, extraProperty) } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift index 3bc47bb8..9add9482 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift @@ -49,11 +49,11 @@ extension FileTranslator { // In nullable enum schemas, empty strings are parsed as Void. // This is unlikely to be fixed, so handling that case here. // https://github.com/apple/swift-openapi-generator/issues/118 - if isNullable && anyValue is Void { return (swiftSafeName(for: ""), .string("")) } + if isNullable && anyValue is Void { return (context.asSwiftSafeName(""), .string("")) } guard let rawValue = anyValue as? String else { throw GenericError(message: "Disallowed value for a string enum '\(typeName)': \(anyValue)") } - let caseName = swiftSafeName(for: rawValue) + let caseName = context.asSwiftSafeName(rawValue) return (caseName, .string(rawValue)) case .integer: let rawValue: Int 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/CommonTypes/DiscriminatorExtensions.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/DiscriminatorExtensions.swift index b27a61c1..1bc1cd7f 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/DiscriminatorExtensions.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/DiscriminatorExtensions.swift @@ -79,7 +79,9 @@ extension FileTranslator { /// component. /// - Parameter type: The `OneOfMappedType` for which to determine the case name. /// - Returns: A string representing the safe Swift name for the specified `OneOfMappedType`. - func safeSwiftNameForOneOfMappedType(_ type: OneOfMappedType) -> String { swiftSafeName(for: type.rawNames[0]) } + func safeSwiftNameForOneOfMappedType(_ type: OneOfMappedType) -> String { + context.asSwiftSafeName(type.rawNames[0]) + } } extension OpenAPI.Discriminator { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/StructBlueprint.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/StructBlueprint.swift index 2b3d7123..9eef5d6d 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/StructBlueprint.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/StructBlueprint.swift @@ -146,15 +146,14 @@ struct PropertyBlueprint { /// referring to them in the property. var associatedDeclarations: [Declaration] = [] - /// A converted function from user-provided strings to strings - /// safe to be used as a Swift identifier. - var asSwiftSafeName: (String) -> String + /// A set of configuration values that inform translation. + var context: TranslatorContext } extension PropertyBlueprint { /// A name that is verified to be a valid Swift identifier. - var swiftSafeName: String { asSwiftSafeName(originalName) } + var swiftSafeName: String { context.asSwiftSafeName(originalName) } /// The JSON path to the property. /// diff --git a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift index ecbf9771..4f246521 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift @@ -43,3 +43,19 @@ protocol FileTranslator { /// - Throws: An error if translation encounters issues or errors during the process. func translateFile(parsedOpenAPI: ParsedOpenAPIRepresentation) throws -> StructuredSwiftRepresentation } + +extension FileTranslator { + + /// A new context from the file translator. + var context: TranslatorContext { TranslatorContext(asSwiftSafeName: { $0.safeForSwiftCode }) } +} + +/// A set of configuration values for concrete file translators. +struct TranslatorContext { + + /// A closure that returns a copy of the string modified to be a valid Swift identifier. + /// + /// - Parameter string: The string to convert to be safe for Swift. + /// - Returns: A Swift-safe version of the input string. + var asSwiftSafeName: (String) -> String +} diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContent.swift b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContent.swift index 881e967b..290b3942 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContent.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContent.swift @@ -90,11 +90,11 @@ extension MultipartSchemaTypedContent { } } -extension SchemaContent { +extension TypeMatcher { /// Returns a Boolean value whether the schema is a multipart content type and is referenceable. - var isReferenceableMultipart: Bool { - guard contentType.isMultipart else { return false } - let ref = TypeMatcher.multipartElementTypeReferenceIfReferenceable(schema: schema, encoding: encoding) + func isReferenceableMultipart(_ content: SchemaContent) -> Bool { + guard content.contentType.isMultipart else { return false } + let ref = multipartElementTypeReferenceIfReferenceable(schema: content.schema, encoding: content.encoding) return ref == nil } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift index 1c27dbd9..f5a83882 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift @@ -120,7 +120,7 @@ extension FileTranslator { } var parts: [MultipartSchemaTypedContent] = try topLevelObject.properties.compactMap { (key, value) -> MultipartSchemaTypedContent? in - let swiftSafeName = swiftSafeName(for: key) + let swiftSafeName = context.asSwiftSafeName(key) let typeName = typeName.appending( swiftComponent: swiftSafeName + Constants.Global.inlineTypeSuffix, jsonComponent: key diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/translateMultipart.swift b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/translateMultipart.swift index 186cfda8..b288ff1a 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/translateMultipart.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/translateMultipart.swift @@ -64,7 +64,7 @@ extension TypesFileTranslator { typeUsage: headersTypeName.asUsage, default: headersStructBlueprint.hasEmptyInit ? .emptyInit : nil, associatedDeclarations: [headersStructDecl], - asSwiftSafeName: swiftSafeName + context: context ) } else { headersProperty = nil @@ -76,7 +76,7 @@ extension TypesFileTranslator { inParent: typeName.appending(swiftComponent: nil, jsonComponent: "content") ) let associatedDeclarations: [Declaration] - if TypeMatcher.isInlinable(schema) { + if typeMatcher.isInlinable(schema) { associatedDeclarations = try translateSchema( typeName: bodyTypeUsage.typeName, schema: schema, @@ -90,7 +90,7 @@ extension TypesFileTranslator { originalName: Constants.Operation.Body.variableName, typeUsage: bodyTypeUsage, associatedDeclarations: associatedDeclarations, - asSwiftSafeName: swiftSafeName + context: context ) let structDecl = translateStructBlueprint( .init( @@ -117,7 +117,7 @@ extension TypesFileTranslator { schema: JSONSchema ) throws -> [Declaration] { let associatedDeclarations: [Declaration] - if TypeMatcher.isInlinable(schema) { + if typeMatcher.isInlinable(schema) { associatedDeclarations = try translateSchema(typeName: typeName, schema: schema, overrides: .none) } else { associatedDeclarations = [] @@ -137,7 +137,7 @@ extension TypesFileTranslator { switch part { case .documentedTyped(let documentedPart): let caseDecl: Declaration = .enumCase( - name: swiftSafeName(for: documentedPart.originalName), + name: context.asSwiftSafeName(documentedPart.originalName), kind: .nameWithAssociatedValues([.init(type: .init(part.wrapperTypeUsage))]) ) let decl = try translateMultipartPartContent( @@ -404,7 +404,7 @@ extension FileTranslator { switch part { case .documentedTyped(let part): let originalName = part.originalName - let identifier = swiftSafeName(for: originalName) + let identifier = context.asSwiftSafeName(originalName) let contentType = part.partInfo.contentType let partTypeName = part.typeName let schema = part.schema @@ -613,7 +613,7 @@ extension FileTranslator { switch part { case .documentedTyped(let part): let originalName = part.originalName - let identifier = swiftSafeName(for: originalName) + let identifier = context.asSwiftSafeName(originalName) let contentType = part.partInfo.contentType let headersTypeName = part.typeName.appending( swiftComponent: Constants.Operation.Output.Payload.Headers.typeName, diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift b/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift index aea727dc..2a72252b 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift @@ -31,9 +31,8 @@ struct OperationDescription { /// The OpenAPI components, used to resolve JSON references. var components: OpenAPI.Components - /// A converted function from user-provided strings to strings - /// safe to be used as a Swift identifier. - var asSwiftSafeName: (String) -> String + /// A set of configuration values that inform translation. + var context: TranslatorContext /// The OpenAPI operation object. var operation: OpenAPI.Operation { endpoint.operation } @@ -52,7 +51,7 @@ extension OperationDescription { /// - Parameters: /// - map: The paths from the OpenAPI document. /// - components: The components from the OpenAPI document. - /// - asSwiftSafeName: A converted function from user-provided strings + /// - context: A set of configuration values that inform translation. /// to strings safe to be used as a Swift identifier. /// - Returns: An array of `OperationDescription` instances, each representing /// an operation discovered in the provided paths. @@ -62,11 +61,9 @@ extension OperationDescription { /// 1. OpenAPI 3.0.3 only supports external path references (cf. 3.1, which supports internal references too) /// 2. Swift OpenAPI Generator currently only supports OpenAPI 3.0.x. /// 3. Swift OpenAPI Generator currently doesn't support external references. - static func all( - from map: OpenAPI.PathItem.Map, - in components: OpenAPI.Components, - asSwiftSafeName: @escaping (String) -> String - ) throws -> [OperationDescription] { + static func all(from map: OpenAPI.PathItem.Map, in components: OpenAPI.Components, context: TranslatorContext) + throws -> [OperationDescription] + { try map.flatMap { path, value in let value = try value.resolve(in: components) return value.endpoints.map { endpoint in @@ -75,7 +72,7 @@ extension OperationDescription { endpoint: endpoint, pathParameters: value.parameters, components: components, - asSwiftSafeName: asSwiftSafeName + context: context ) } } @@ -86,7 +83,7 @@ extension OperationDescription { /// Uses the `operationID` value in the OpenAPI operation, if one was /// specified. Otherwise, computes a unique name from the operation's /// path and HTTP method. - var methodName: String { asSwiftSafeName(operationID) } + var methodName: String { context.asSwiftSafeName(operationID) } /// Returns the identifier for the operation. /// @@ -295,7 +292,7 @@ extension OperationDescription { } let newPath = OpenAPI.Path(newComponents, trailingSlash: path.trailingSlash) let names: [Expression] = orderedPathParameters.map { param in - .identifierPattern("input").dot("path").dot(asSwiftSafeName(param)) + .identifierPattern("input").dot("path").dot(context.asSwiftSafeName(param)) } let arrayExpr: Expression = .literal(.array(names)) return (newPath.rawValue, arrayExpr) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift index 66a0ce26..e8eb0700 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift @@ -34,9 +34,8 @@ struct TypedParameter { /// The coding strategy appropriate for this parameter. var codingStrategy: CodingStrategy - /// A converted function from user-provided strings to strings - /// safe to be used as a Swift identifier. - var asSwiftSafeName: (String) -> String + /// A set of configuration values that inform translation. + var context: TranslatorContext } extension TypedParameter: CustomStringConvertible { @@ -49,7 +48,7 @@ extension TypedParameter { var name: String { parameter.name } /// The name of the parameter sanitized to be a valid Swift identifier. - var variableName: String { asSwiftSafeName(name) } + var variableName: String { context.asSwiftSafeName(name) } /// A Boolean value that indicates whether the parameter must be specified /// when performing the OpenAPI operation. @@ -58,22 +57,14 @@ extension TypedParameter { /// The location of the parameter in the HTTP request. var location: OpenAPI.Parameter.Context.Location { parameter.location } - /// A schema to be inlined. - /// - /// - Returns: Nil when schema is referenceable. - var inlineableSchema: JSONSchema? { schema.inlineableSchema } -} - -extension UnresolvedSchema { - /// A schema to be inlined. /// /// - Returns: Nil when schema is referenceable. var inlineableSchema: JSONSchema? { - switch self { + switch schema { case .a: return nil case let .b(schema): - if TypeMatcher.isInlinable(schema) { return schema } + if TypeMatcher(context: context).isInlinable(schema) { return schema } return nil } } @@ -208,7 +199,7 @@ extension FileTranslator { explode: explode, typeUsage: usage, codingStrategy: codingStrategy, - asSwiftSafeName: swiftSafeName + context: context ) } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift index 0176a8cd..3dcdbe75 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift @@ -42,7 +42,7 @@ extension TypesFileTranslator { originalName: parameter.name, typeUsage: parameter.typeUsage, associatedDeclarations: associatedDeclarations, - asSwiftSafeName: swiftSafeName + context: context ) } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift index 76c0c6f7..1a412704 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/RequestBody/translateRequestBody.swift @@ -46,7 +46,8 @@ extension TypesFileTranslator { let contentTypeName = typeName.appending(jsonComponent: "content") let contents = requestBody.contents for content in contents { - if TypeMatcher.isInlinable(content.content.schema) || content.content.isReferenceableMultipart { + if typeMatcher.isInlinable(content.content.schema) || typeMatcher.isReferenceableMultipart(content.content) + { let inlineTypeDecls = try translateRequestBodyContentInTypes(content) bodyMembers.append(contentsOf: inlineTypeDecls) } @@ -92,7 +93,7 @@ extension TypesFileTranslator { typeUsage: bodyEnumTypeUsage, default: nil, associatedDeclarations: extraDecls, - asSwiftSafeName: swiftSafeName + context: context ) return bodyProperty } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift index 4cf5a85a..ad11fcbc 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift @@ -32,15 +32,14 @@ struct TypedResponseHeader { /// The coding strategy appropriate for this parameter. var codingStrategy: CodingStrategy - /// A converted function from user-provided strings to strings - /// safe to be used as a Swift identifier. - var asSwiftSafeName: (String) -> String + /// A set of configuration values that inform translation. + var context: TranslatorContext } extension TypedResponseHeader { /// The name of the header sanitized to be a valid Swift identifier. - var variableName: String { asSwiftSafeName(name) } + var variableName: String { context.asSwiftSafeName(name) } /// A Boolean value that indicates whether the response header can /// be omitted in the HTTP response. @@ -152,7 +151,7 @@ extension FileTranslator { schema: schema, typeUsage: usage, codingStrategy: codingStrategy, - asSwiftSafeName: swiftSafeName + context: context ) } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponse.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponse.swift index 13a44064..a6754853 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponse.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponse.swift @@ -50,7 +50,7 @@ extension TypesFileTranslator { typeUsage: headersTypeName.asUsage, default: headersStructBlueprint.hasEmptyInit ? .emptyInit : nil, associatedDeclarations: [headersStructDecl], - asSwiftSafeName: swiftSafeName + context: context ) } else { headersProperty = nil @@ -92,7 +92,7 @@ extension TypesFileTranslator { typeUsage: contentTypeUsage, default: hasNoContent ? .nil : nil, associatedDeclarations: [contentEnumDecl], - asSwiftSafeName: swiftSafeName + context: context ) } else { bodyProperty = nil @@ -145,7 +145,7 @@ extension TypesFileTranslator { let associatedType = typedContent.resolvedTypeUsage let content = typedContent.content let schema = content.schema - if TypeMatcher.isInlinable(schema) || content.isReferenceableMultipart { + if typeMatcher.isInlinable(schema) || typeMatcher.isReferenceableMultipart(content) { let decls: [Declaration] if contentType.isMultipart { decls = try translateMultipartBody(typedContent) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift index 229d575a..1096ae08 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/translateResponseHeader.swift @@ -66,7 +66,7 @@ extension TypesFileTranslator { let schema = header.schema let typeUsage = header.typeUsage let associatedDeclarations: [Declaration] - if TypeMatcher.isInlinable(schema) { + if typeMatcher.isInlinable(schema) { associatedDeclarations = try translateSchema(typeName: typeUsage.typeName, schema: schema, overrides: .none) } else { associatedDeclarations = [] @@ -78,7 +78,7 @@ extension TypesFileTranslator { typeUsage: typeUsage, default: header.header.required ? nil : .nil, associatedDeclarations: associatedDeclarations, - asSwiftSafeName: swiftSafeName + context: context ) } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift index e62df4f6..3f54d4ad 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift @@ -35,11 +35,7 @@ struct ServerFileTranslator: FileTranslator { let imports = Constants.File.clientServerImports + config.additionalImports.map { ImportDescription(moduleName: $0) } - let allOperations = try OperationDescription.all( - from: doc.paths, - in: components, - asSwiftSafeName: swiftSafeName - ) + let allOperations = try OperationDescription.all(from: doc.paths, in: components, context: context) let (registerHandlersDecl, serverMethodDecls) = try translateRegisterHandlers(allOperations) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift index 72e18f8a..7f7a46ff 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift @@ -41,9 +41,8 @@ import Foundation /// cases when it's a simple string schema. struct TypeAssigner { - /// A converted function from user-provided strings to strings - /// safe to be used as a Swift identifier. - var asSwiftSafeName: (String) -> String + /// A set of configuration values that inform translation. + var context: TranslatorContext /// Returns a type name for an OpenAPI-named component type. /// @@ -60,7 +59,7 @@ struct TypeAssigner { /// - Returns: A Swift type name for the specified component type. func typeName(forComponentOriginallyNamed originalName: String, in location: TypeLocation) -> TypeName { typeName(forLocation: location) - .appending(swiftComponent: asSwiftSafeName(originalName), jsonComponent: originalName) + .appending(swiftComponent: context.asSwiftSafeName(originalName), jsonComponent: originalName) } /// Returns the type name for an OpenAPI-named component namespace. @@ -123,10 +122,12 @@ struct TypeAssigner { inParent parent: TypeName ) throws -> TypeUsage { let multipartBodyElementTypeName: TypeName - if let ref = TypeMatcher.multipartElementTypeReferenceIfReferenceable(schema: schema, encoding: encoding) { + if let ref = TypeMatcher(context: context) + .multipartElementTypeReferenceIfReferenceable(schema: schema, encoding: encoding) + { multipartBodyElementTypeName = try typeName(for: ref) } else { - let swiftSafeName = asSwiftSafeName(hint) + let swiftSafeName = context.asSwiftSafeName(hint) multipartBodyElementTypeName = parent.appending( swiftComponent: swiftSafeName + Constants.Global.inlineTypeSuffix, jsonComponent: hint @@ -328,7 +329,7 @@ struct TypeAssigner { inParent parent: TypeName, subtype: SubtypeNamingMethod ) throws -> TypeUsage { - let typeMatcher = TypeMatcher(asSwiftSafeName: asSwiftSafeName) + let typeMatcher = TypeMatcher(context: context) // Check if this type can be simply referenced without // creating a new inline type. if let referenceableType = try typeMatcher.tryMatchReferenceableType(for: schema, components: components) { @@ -342,7 +343,7 @@ struct TypeAssigner { } return baseType.appending( - swiftComponent: asSwiftSafeName(originalName) + suffix, + swiftComponent: context.asSwiftSafeName(originalName) + suffix, jsonComponent: jsonReferenceComponentOverride ?? originalName ) .asUsage.withOptional(try typeMatcher.isOptional(schema, components: components)) @@ -405,7 +406,7 @@ struct TypeAssigner { of componentType: Component.Type ) -> TypeName { typeName(for: Component.self) - .appending(swiftComponent: asSwiftSafeName(key.rawValue), jsonComponent: key.rawValue) + .appending(swiftComponent: context.asSwiftSafeName(key.rawValue), jsonComponent: key.rawValue) } /// Returns a type name for a JSON reference. @@ -469,7 +470,8 @@ struct TypeAssigner { guard case let .component(name) = reference else { throw JSONReferenceParsingError.nonComponentPathsUnsupported(reference.name) } - return typeName(for: componentType).appending(swiftComponent: asSwiftSafeName(name), jsonComponent: name) + return typeName(for: componentType) + .appending(swiftComponent: context.asSwiftSafeName(name), jsonComponent: name) } /// Returns a type name for the namespace for the specified component type. @@ -493,7 +495,7 @@ struct TypeAssigner { { typeNameForComponents() .appending( - swiftComponent: asSwiftSafeName(componentType.openAPIComponentsKey).uppercasingFirstLetter, + swiftComponent: context.asSwiftSafeName(componentType.openAPIComponentsKey).uppercasingFirstLetter, jsonComponent: componentType.openAPIComponentsKey ) } @@ -526,14 +528,14 @@ struct TypeAssigner { case "application/pdf": return "pdf" case "image/jpeg": return "jpeg" default: - let safedType = asSwiftSafeName(contentType.originallyCasedType) - let safedSubtype = asSwiftSafeName(contentType.originallyCasedSubtype) + let safedType = context.asSwiftSafeName(contentType.originallyCasedType) + let safedSubtype = context.asSwiftSafeName(contentType.originallyCasedSubtype) let prefix = "\(safedType)_\(safedSubtype)" let params = contentType.lowercasedParameterPairs guard !params.isEmpty else { return prefix } let safedParams = params.map { pair in - pair.split(separator: "=").map { asSwiftSafeName(String($0)) }.joined(separator: "_") + pair.split(separator: "=").map { context.asSwiftSafeName(String($0)) }.joined(separator: "_") } .joined(separator: "_") return prefix + "_" + safedParams @@ -545,10 +547,10 @@ struct TypeAssigner { extension FileTranslator { /// A configured type assigner. - var typeAssigner: TypeAssigner { TypeAssigner(asSwiftSafeName: swiftSafeName) } + var typeAssigner: TypeAssigner { TypeAssigner(context: context) } /// A configured type matcher. - var typeMatcher: TypeMatcher { TypeMatcher(asSwiftSafeName: swiftSafeName) } + var typeMatcher: TypeMatcher { TypeMatcher(context: context) } } /// An error used during the parsing of JSON references specified in an diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift index 08ac4a1e..1c503ae7 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift @@ -16,9 +16,8 @@ import OpenAPIKit /// A set of functions that match Swift types onto OpenAPI types. struct TypeMatcher { - /// A converted function from user-provided strings to strings - /// safe to be used as a Swift identifier. - var asSwiftSafeName: (String) -> String + /// A set of configuration values that inform translation. + var context: TranslatorContext /// Returns the type name of a built-in type that matches the specified /// schema. @@ -43,7 +42,7 @@ struct TypeMatcher { func tryMatchBuiltinType(for schema: JSONSchema.Schema) -> TypeUsage? { Self._tryMatchRecursive( for: schema, - test: { schema in Self._tryMatchBuiltinNonRecursive(for: schema) }, + test: { schema in _tryMatchBuiltinNonRecursive(for: schema) }, matchedArrayHandler: { elementType, nullableItems in nullableItems ? elementType.asOptional.asArray : elementType.asArray }, @@ -69,9 +68,9 @@ struct TypeMatcher { try Self._tryMatchRecursive( for: schema.value, test: { (schema) -> TypeUsage? in - if let builtinType = Self._tryMatchBuiltinNonRecursive(for: schema) { return builtinType } + if let builtinType = _tryMatchBuiltinNonRecursive(for: schema) { return builtinType } guard case let .reference(ref, _) = schema else { return nil } - return try TypeAssigner(asSwiftSafeName: asSwiftSafeName).typeName(for: ref).asUsage + return try TypeAssigner(context: context).typeName(for: ref).asUsage }, matchedArrayHandler: { elementType, nullableItems in nullableItems ? elementType.asOptional.asArray : elementType.asArray @@ -89,9 +88,9 @@ struct TypeMatcher { /// - A reference /// - Parameter schema: The schema to match a referenceable type for. /// - Returns: `true` if the schema is referenceable; `false` otherwise. - static func isReferenceable(_ schema: JSONSchema) -> Bool { + func isReferenceable(_ schema: JSONSchema) -> Bool { // This logic should be kept in sync with `tryMatchReferenceableType`. - _tryMatchRecursive( + Self._tryMatchRecursive( for: schema.value, test: { schema in if _tryMatchBuiltinNonRecursive(for: schema) != nil { return true } @@ -111,7 +110,7 @@ struct TypeMatcher { /// - A reference /// - Parameter schema: The schema to match a referenceable type for. /// - Returns: `true` if the schema is referenceable; `false` otherwise. - static func isReferenceable(_ schema: UnresolvedSchema?) -> Bool { + func isReferenceable(_ schema: UnresolvedSchema?) -> Bool { guard let schema else { // fragment type is referenceable return true @@ -133,7 +132,7 @@ struct TypeMatcher { /// referenceable. /// - Parameter schema: The schema to match a referenceable type for. /// - Returns: `true` if the schema is inlinable; `false` otherwise. - static func isInlinable(_ schema: JSONSchema) -> Bool { !isReferenceable(schema) } + func isInlinable(_ schema: JSONSchema) -> Bool { !isReferenceable(schema) } /// Returns a Boolean value that indicates whether the schema /// needs to be defined inline. @@ -144,14 +143,14 @@ struct TypeMatcher { /// referenceable. /// - Parameter schema: The schema to match a referenceable type for. /// - Returns: `true` if the schema is inlinable; `false` otherwise. - static func isInlinable(_ schema: UnresolvedSchema?) -> Bool { !isReferenceable(schema) } + func isInlinable(_ schema: UnresolvedSchema?) -> Bool { !isReferenceable(schema) } /// Return a reference to a multipart element type if the provided schema is referenceable. /// - Parameters: /// - schema: The schema to try to reference. /// - encoding: The associated encoding. /// - Returns: A reference if the schema is referenceable, nil otherwise. - static func multipartElementTypeReferenceIfReferenceable( + func multipartElementTypeReferenceIfReferenceable( schema: UnresolvedSchema?, encoding: OrderedDictionary? ) -> OpenAPI.Reference? { @@ -175,11 +174,9 @@ struct TypeMatcher { /// - components: The reusable components from the OpenAPI document. /// - Throws: An error if there's an issue while checking the schema. /// - Returns: `true` if the schema is a key-value pair; `false` otherwise. - static func isKeyValuePair( - _ schema: JSONSchema, - referenceStack: inout ReferenceStack, - components: OpenAPI.Components - ) throws -> Bool { + func isKeyValuePair(_ schema: JSONSchema, referenceStack: inout ReferenceStack, components: OpenAPI.Components) + throws -> Bool + { switch schema.value { case .object, .fragment: return true case .null, .boolean, .number, .integer, .string, .array, .not: return false @@ -223,7 +220,7 @@ struct TypeMatcher { /// - components: The reusable components from the OpenAPI document. /// - Throws: An error if there's an issue while checking the schema. /// - Returns: `true` if the schema is a key-value pair; `false` otherwise. - static func isKeyValuePair( + func isKeyValuePair( _ schema: UnresolvedSchema?, referenceStack: inout ReferenceStack, components: OpenAPI.Components @@ -286,7 +283,7 @@ struct TypeMatcher { /// - Parameter schema: The schema to match a referenceable type for. /// - Returns: A type usage for the schema if the schema is built-in. /// Otherwise, returns nil. - private static func _tryMatchBuiltinNonRecursive(for schema: JSONSchema.Schema) -> TypeUsage? { + private func _tryMatchBuiltinNonRecursive(for schema: JSONSchema.Schema) -> TypeUsage? { let typeName: TypeName switch schema { case .boolean(_): typeName = .swift("Bool") diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift index fb0cc056..3ad52eb0 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift @@ -45,11 +45,7 @@ struct TypesFileTranslator: FileTranslator { let multipartSchemaNames = try parseSchemaNamesUsedInMultipart(paths: doc.paths, components: doc.components) let components = try translateComponents(doc.components, multipartSchemaNames: multipartSchemaNames) - let operationDescriptions = try OperationDescription.all( - from: doc.paths, - in: doc.components, - asSwiftSafeName: swiftSafeName - ) + let operationDescriptions = try OperationDescription.all(from: doc.paths, in: doc.components, context: context) let operations = try translateOperations(operationDescriptions) let typesFile = FileDescription( diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateAPIProtocol.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateAPIProtocol.swift index 3b061854..2f9134ed 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateAPIProtocol.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateAPIProtocol.swift @@ -22,7 +22,7 @@ extension TypesFileTranslator { /// - Throws: If `paths` contains any references. func translateAPIProtocol(_ paths: OpenAPI.PathItem.Map) throws -> Declaration { - let operations = try OperationDescription.all(from: paths, in: components, asSwiftSafeName: swiftSafeName) + let operations = try OperationDescription.all(from: paths, in: components, context: context) let functionDecls = operations.map(translateAPIProtocolDeclaration(operation:)) let protocolDescription = ProtocolDescription( @@ -38,7 +38,7 @@ extension TypesFileTranslator { /// Returns an extension to the `APIProtocol` protocol, with some syntactic sugar APIs. func translateAPIProtocolExtension(_ paths: OpenAPI.PathItem.Map) throws -> Declaration { - let operations = try OperationDescription.all(from: paths, in: components, asSwiftSafeName: swiftSafeName) + let operations = try OperationDescription.all(from: paths, in: components, context: context) // This looks for all initializers in the operation input struct and creates a flattened function. let flattenedOperations = try operations.flatMap { operation in diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateOperations.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateOperations.swift index df3576ac..55d01890 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateOperations.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateOperations.swift @@ -71,7 +71,7 @@ extension TypesFileTranslator { typeUsage: structTypeName.asUsage, default: defaultValue, associatedDeclarations: [structDecl], - asSwiftSafeName: swiftSafeName + context: context ) } let bodyProperty = try parseRequestBodyAsProperty( @@ -89,7 +89,7 @@ extension TypesFileTranslator { originalName: Constants.Operation.AcceptableContentType.variableName, typeUsage: description.acceptableArrayName, default: .expression(.dot("defaultValues").call([])), - asSwiftSafeName: swiftSafeName + context: context ) extraHeaderProperties = [acceptPropertyBlueprint] } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServers.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServers.swift index bb7a6552..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: swiftSafeName(for: 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/Articles/Useful-OpenAPI-patterns.md b/Sources/swift-openapi-generator/Documentation.docc/Articles/Useful-OpenAPI-patterns.md index 7c6140ab..258848f3 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Articles/Useful-OpenAPI-patterns.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Articles/Useful-OpenAPI-patterns.md @@ -113,8 +113,9 @@ The returned binary body contains the raw events, and the stream can be split up - encode: `AsyncSequence.asEncodedJSONSequence(encoder:)` - Server-sent Events - decode (if data is JSON): `AsyncSequence>.asDecodedServerSentEventsWithJSONData(of:decoder:)` + - decode (if data is JSON with a non-JSON terminating byte sequence): `AsyncSequence>.asDecodedServerSentEventsWithJSONData(of:decoder:while:)` - encode (if data is JSON): `AsyncSequence.asEncodedServerSentEventsWithJSONData(encoder:)` - - decode (for other data): `AsyncSequence>.asDecodedServerSentEvents()` + - decode (for other data): `AsyncSequence>.asDecodedServerSentEvents(while:)` - encode (for other data): `AsyncSequence.asEncodedServerSentEvents()` See the `event-streams-*` client and server examples in to learn how to produce and consume these sequences. 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/OpenAPIGeneratorCoreTests/Extensions/Test_String.swift b/Tests/OpenAPIGeneratorCoreTests/Extensions/Test_String.swift index c9c58c94..9d10b45c 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Extensions/Test_String.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Extensions/Test_String.swift @@ -64,7 +64,7 @@ final class Test_String: Test_Core { ("application", "application"), ("vendor1+json", "vendor1_plus_json"), ] let translator = makeTranslator() - let asSwiftSafeName: (String) -> String = translator.swiftSafeName + let asSwiftSafeName: (String) -> String = translator.context.asSwiftSafeName for (input, sanitized) in cases { XCTAssertEqual(asSwiftSafeName(input), sanitized) } } } diff --git a/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift b/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift index 67d2e94a..a99d4d30 100644 --- a/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift +++ b/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift @@ -59,10 +59,12 @@ class Test_Core: XCTestCase { var typeMatcher: TypeMatcher { makeTranslator().typeMatcher } - var asSwiftSafeName: (String) -> String { makeTranslator().swiftSafeName } + var context: TranslatorContext { makeTranslator().context } + + var asSwiftSafeName: (String) -> String { context.asSwiftSafeName } func makeProperty(originalName: String, typeUsage: TypeUsage) -> PropertyBlueprint { - .init(originalName: originalName, typeUsage: typeUsage, asSwiftSafeName: asSwiftSafeName) + .init(originalName: originalName, typeUsage: typeUsage, context: context) } } diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/Operations/Test_OperationDescription.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/Operations/Test_OperationDescription.swift index a5da80a4..6b37703c 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/Operations/Test_OperationDescription.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/Operations/Test_OperationDescription.swift @@ -144,7 +144,7 @@ final class Test_OperationDescription: Test_Core { endpoint: endpoint, pathParameters: pathItem.parameters, components: .init(), - asSwiftSafeName: { $0 } + context: .init(asSwiftSafeName: { $0 }) ) } } diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift index a7e0b8e3..6ca197d2 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift @@ -127,8 +127,8 @@ final class Test_TypeMatcher: Test_Core { .fullyQualifiedSwiftName, name ) - XCTAssertTrue(TypeMatcher.isReferenceable(schema)) - XCTAssertFalse(TypeMatcher.isInlinable(schema)) + XCTAssertTrue(typeMatcher.isReferenceable(schema)) + XCTAssertFalse(typeMatcher.isInlinable(schema)) } } @@ -145,8 +145,8 @@ final class Test_TypeMatcher: Test_Core { typeMatcher.tryMatchBuiltinType(for: schema.value), "Type is expected to not match a builtin type: \(schema)" ) - XCTAssertFalse(TypeMatcher.isReferenceable(schema), "Expected schema not to be referenceable: \(schema)") - XCTAssertTrue(TypeMatcher.isInlinable(schema), "Expected schema to be inlinable: \(schema)") + XCTAssertFalse(typeMatcher.isReferenceable(schema), "Expected schema not to be referenceable: \(schema)") + XCTAssertTrue(typeMatcher.isInlinable(schema), "Expected schema to be inlinable: \(schema)") } } @@ -175,7 +175,7 @@ final class Test_TypeMatcher: Test_Core { for schema in Self.keyValuePairTypes { var referenceStack = ReferenceStack.empty XCTAssertTrue( - try TypeMatcher.isKeyValuePair(schema, referenceStack: &referenceStack, components: components), + try typeMatcher.isKeyValuePair(schema, referenceStack: &referenceStack, components: components), "Type is expected to be a key-value pair schema: \(schema)" ) } @@ -249,7 +249,7 @@ final class Test_TypeMatcher: Test_Core { ] func testMultipartElementTypeReferenceIfReferenceableTypes() throws { for (schema, encoding, name) in Self.multipartElementTypeReferenceIfReferenceableTypes { - let actualName = TypeMatcher.multipartElementTypeReferenceIfReferenceable( + let actualName = typeMatcher.multipartElementTypeReferenceIfReferenceable( schema: schema, encoding: encoding )? @@ -257,5 +257,4 @@ final class Test_TypeMatcher: Test_Core { XCTAssertEqual(actualName, name) } } - } 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 0196c5ee..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, @@ -5284,7 +5587,7 @@ extension SnippetBasedReferenceTests { let operationDescriptions = try OperationDescription.all( from: document.paths, in: document.components, - asSwiftSafeName: types.swiftSafeName + context: types.context ) let operation = try XCTUnwrap(operationDescriptions.first) let generatedTypesStructuredSwift = try types.translateOperationInput(operation) @@ -5343,7 +5646,7 @@ extension SnippetBasedReferenceTests { let operationDescriptions = try OperationDescription.all( from: document.paths, in: document.components, - asSwiftSafeName: types.swiftSafeName + context: types.context ) let operation = try XCTUnwrap(operationDescriptions.first) let generatedTypesStructuredSwift = try types.translateOperationOutput(operation) @@ -5465,14 +5768,25 @@ extension SnippetBasedReferenceTests { ) throws { let (_, _, translator) = try makeTranslators() let paths = try YAMLDecoder().decode(OpenAPI.PathItem.Map.self, from: pathsYAML) - let operations = try OperationDescription.all( - from: paths, - in: .noComponents, - asSwiftSafeName: translator.swiftSafeName - ) + let operations = try OperationDescription.all(from: paths, in: .noComponents, context: translator.context) 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"]) - } } }