diff --git a/Examples/replace-types-example/.gitignore b/Examples/replace-types-example/.gitignore new file mode 100644 index 00000000..f6f5465e --- /dev/null +++ b/Examples/replace-types-example/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.vscode +/Package.resolved +.ci/ +.docc-build/ diff --git a/Examples/replace-types-example/Package.swift b/Examples/replace-types-example/Package.swift new file mode 100644 index 00000000..4f0f4d41 --- /dev/null +++ b/Examples/replace-types-example/Package.swift @@ -0,0 +1,37 @@ +// swift-tools-version:5.9 +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2024 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 PackageDescription + +let package = Package( + name: "replace-types-example", + platforms: [.macOS(.v14)], + products: [ + .library(name: "Types", targets: ["Types"]), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.7.0"), + ], + targets: [ + .target( + name: "Types", + dependencies: [ + "ExternalLibrary", + .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime")] + ), + .target( + name: "ExternalLibrary" + ), + ] +) diff --git a/Examples/replace-types-example/README.md b/Examples/replace-types-example/README.md new file mode 100644 index 00000000..974e6d52 --- /dev/null +++ b/Examples/replace-types-example/README.md @@ -0,0 +1,40 @@ +# Replacing types + +An example project using [Swift OpenAPI Generator](https://github.com/apple/swift-openapi-generator). + +> **Disclaimer:** This example is deliberately simplified and is intended for illustrative purposes only. + +## Overview + +This example shows how you can structure a Swift package to share the types +from an OpenAPI document between a client and server module by having a common +target that runs the generator in `types` mode only. + +This allows you to write extensions or other helper functions that use these +types and use them in both the client and server code. + +## Usage + +Build and run the server using: + +```console +% swift run hello-world-server +Build complete! +... +info HummingBird : [HummingbirdCore] Server started and listening on 127.0.0.1:8080 +``` + +Then, in another terminal window, run the client: + +```console +% swift run hello-world-client +Build complete! ++––––––––––––––––––+ +|+––––––––––––––––+| +||Hello, Stranger!|| +|+––––––––––––––––+| ++––––––––––––––––––+ +``` + +Note how the message is boxed twice: once by the server and once by the client, +both using an extension on a shared type, defined in the `Types` module. diff --git a/Examples/replace-types-example/Sources/ExternalLibrary/ExternalObject.swift b/Examples/replace-types-example/Sources/ExternalLibrary/ExternalObject.swift new file mode 100644 index 00000000..5082fd55 --- /dev/null +++ b/Examples/replace-types-example/Sources/ExternalLibrary/ExternalObject.swift @@ -0,0 +1,4 @@ +public struct ExternalObject: Codable, Hashable, Sendable { + public let foo: String + public let bar: String +} diff --git a/Examples/replace-types-example/Sources/ExternalLibrary/PrimeNumber.swift b/Examples/replace-types-example/Sources/ExternalLibrary/PrimeNumber.swift new file mode 100644 index 00000000..3d7dddd4 --- /dev/null +++ b/Examples/replace-types-example/Sources/ExternalLibrary/PrimeNumber.swift @@ -0,0 +1,35 @@ +public struct PrimeNumber: Codable, Hashable, RawRepresentable, Sendable { + public let rawValue: Int + public init?(rawValue: Int) { + if !rawValue.isPrime { return nil } + self.rawValue = rawValue + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let number = try container.decode(Int.self) + guard let value = PrimeNumber(rawValue: number) else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "The number is not prime.") + } + self = value + } + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.rawValue) + } + +} + +extension Int { + fileprivate var isPrime: Bool { + if self <= 1 { return false } + if self <= 3 { return true } + + var i = 2 + while i * i <= self { + if self % i == 0 { return false } + i += 1 + } + return true + } +} diff --git a/Examples/replace-types-example/Sources/Types/Generated/Types.swift b/Examples/replace-types-example/Sources/Types/Generated/Types.swift new file mode 100644 index 00000000..80084c74 --- /dev/null +++ b/Examples/replace-types-example/Sources/Types/Generated/Types.swift @@ -0,0 +1,228 @@ +// Generated by swift-openapi-generator, do not modify. +@_spi(Generated) import OpenAPIRuntime +#if os(Linux) +@preconcurrency import struct Foundation.URL +@preconcurrency import struct Foundation.Data +@preconcurrency import struct Foundation.Date +#else +import struct Foundation.URL +import struct Foundation.Data +import struct Foundation.Date +#endif +import Foundation +import ExternalLibrary +/// A type that performs HTTP operations defined by the OpenAPI document. +package protocol APIProtocol: Sendable { + /// - Remark: HTTP `GET /user`. + /// - Remark: Generated from `#/paths//user/get(getUser)`. + func getUser(_ input: Operations.GetUser.Input) async throws -> Operations.GetUser.Output +} + +/// Convenience overloads for operation inputs. +extension APIProtocol { + /// - Remark: HTTP `GET /user`. + /// - Remark: Generated from `#/paths//user/get(getUser)`. + package func getUser( + query: Operations.GetUser.Input.Query = .init(), + headers: Operations.GetUser.Input.Headers = .init() + ) async throws -> Operations.GetUser.Output { + try await getUser(Operations.GetUser.Input( + query: query, + headers: headers + )) + } +} + +/// Server URLs defined in the OpenAPI document. +package enum Servers { + /// Example service deployment. + package enum Server1 { + /// Example service deployment. + package static func url() throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://example.com/api", + variables: [] + ) + } + } + /// Example service deployment. + @available(*, deprecated, renamed: "Servers.Server1.url") + package static func server1() throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://example.com/api", + variables: [] + ) + } +} + +/// Types generated from the components section of the OpenAPI document. +package enum Components { + /// Types generated from the `#/components/schemas` section of the OpenAPI document. + package enum Schemas { + /// - Remark: Generated from `#/components/schemas/UUID`. + package typealias Uuid = Foundation.UUID + /// - Remark: Generated from `#/components/schemas/User`. + package struct User: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/User/id`. + package var id: Components.Schemas.Uuid? + /// - Remark: Generated from `#/components/schemas/User/name`. + package var name: Swift.String? + /// Creates a new `User`. + /// + /// - Parameters: + /// - id: + /// - name: + package init( + id: Components.Schemas.Uuid? = nil, + name: Swift.String? = nil + ) { + self.id = id + self.name = name + } + package enum CodingKeys: String, CodingKey { + case id + case name + } + } + } + /// Types generated from the `#/components/parameters` section of the OpenAPI document. + package enum Parameters {} + /// Types generated from the `#/components/requestBodies` section of the OpenAPI document. + package enum RequestBodies {} + /// Types generated from the `#/components/responses` section of the OpenAPI document. + package enum Responses {} + /// Types generated from the `#/components/headers` section of the OpenAPI document. + package enum Headers {} +} + +/// API operations, with input and output types, generated from `#/paths` in the OpenAPI document. +package enum Operations { + /// - Remark: HTTP `GET /user`. + /// - Remark: Generated from `#/paths//user/get(getUser)`. + package enum GetUser { + package static let id: Swift.String = "getUser" + package struct Input: Sendable, Hashable { + /// - Remark: Generated from `#/paths/user/GET/query`. + package struct Query: Sendable, Hashable { + /// The name of the user + /// + /// - Remark: Generated from `#/paths/user/GET/query/name`. + package var name: Swift.String? + /// Creates a new `Query`. + /// + /// - Parameters: + /// - name: The name of the user + package init(name: Swift.String? = nil) { + self.name = name + } + } + package var query: Operations.GetUser.Input.Query + /// - Remark: Generated from `#/paths/user/GET/header`. + package struct Headers: Sendable, Hashable { + package var accept: [OpenAPIRuntime.AcceptHeaderContentType] + /// Creates a new `Headers`. + /// + /// - Parameters: + /// - accept: + package init(accept: [OpenAPIRuntime.AcceptHeaderContentType] = .defaultValues()) { + self.accept = accept + } + } + package var headers: Operations.GetUser.Input.Headers + /// Creates a new `Input`. + /// + /// - Parameters: + /// - query: + /// - headers: + package init( + query: Operations.GetUser.Input.Query = .init(), + headers: Operations.GetUser.Input.Headers = .init() + ) { + self.query = query + self.headers = headers + } + } + @frozen package enum Output: Sendable, Hashable { + package struct Ok: Sendable, Hashable { + /// - Remark: Generated from `#/paths/user/GET/responses/200/content`. + @frozen package enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/user/GET/responses/200/content/application\/json`. + case json(Components.Schemas.User) + /// The associated value of the enum case if `self` is `.json`. + /// + /// - Throws: An error if `self` is not `.json`. + /// - SeeAlso: `.json`. + package var json: Components.Schemas.User { + get throws { + switch self { + case let .json(body): + return body + } + } + } + } + /// Received HTTP response body + package var body: Operations.GetUser.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - body: Received HTTP response body + package init(body: Operations.GetUser.Output.Ok.Body) { + self.body = body + } + } + /// A success response with the user. + /// + /// - Remark: Generated from `#/paths//user/get(getUser)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.GetUser.Output.Ok) + /// The associated value of the enum case if `self` is `.ok`. + /// + /// - Throws: An error if `self` is not `.ok`. + /// - SeeAlso: `.ok`. + package var ok: Operations.GetUser.Output.Ok { + get throws { + switch self { + case let .ok(response): + return response + default: + try throwUnexpectedResponseStatus( + expectedStatus: "ok", + response: self + ) + } + } + } + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Swift.Int, OpenAPIRuntime.UndocumentedPayload) + } + @frozen package enum AcceptableContentType: AcceptableProtocol { + case json + case other(Swift.String) + package init?(rawValue: Swift.String) { + switch rawValue.lowercased() { + case "application/json": + self = .json + default: + self = .other(rawValue) + } + } + package var rawValue: Swift.String { + switch self { + case let .other(string): + return string + case .json: + return "application/json" + } + } + package static var allCases: [Self] { + [ + .json + ] + } + } + } +} diff --git a/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml b/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml new file mode 100644 index 00000000..c9ef0876 --- /dev/null +++ b/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml @@ -0,0 +1,9 @@ +generate: + - types +accessModifier: package +namingStrategy: idiomatic +additionalImports: + - Foundation + - ExternalLibrary +typeOverrides: + UUID: Foundation.UUID diff --git a/Examples/replace-types-example/Sources/Types/openapi.yaml b/Examples/replace-types-example/Sources/Types/openapi.yaml new file mode 120000 index 00000000..1c2a243e --- /dev/null +++ b/Examples/replace-types-example/Sources/Types/openapi.yaml @@ -0,0 +1 @@ +../openapi.yaml \ No newline at end of file diff --git a/Examples/replace-types-example/Sources/openapi.yaml b/Examples/replace-types-example/Sources/openapi.yaml new file mode 100644 index 00000000..eabfb5cf --- /dev/null +++ b/Examples/replace-types-example/Sources/openapi.yaml @@ -0,0 +1,38 @@ +openapi: '3.1.0' +info: + title: GreetingService + version: 1.0.0 +servers: + - url: https://example.com/api + description: Example service deployment. +paths: + /user: + get: + operationId: getUser + parameters: + - name: name + required: false + in: query + description: The name of the user + schema: + type: string + responses: + '200': + description: A success response with the user. + content: + application/json: + schema: + $ref: '#/components/schemas/User' +components: + schemas: + UUID: # this will be replaced by with Foundation.UUID specified by typeOverrides in open-api-generator-config + type: string + format: uuid + + User: + type: object + properties: + id: + $ref: '#/components/schemas/UUID' + name: + type: string diff --git a/Sources/_OpenAPIGeneratorCore/Config.swift b/Sources/_OpenAPIGeneratorCore/Config.swift index b4953742..ff40405b 100644 --- a/Sources/_OpenAPIGeneratorCore/Config.swift +++ b/Sources/_OpenAPIGeneratorCore/Config.swift @@ -59,6 +59,8 @@ public struct Config: Sendable { /// A map of OpenAPI identifiers to desired Swift identifiers, used instead of the naming strategy. public var nameOverrides: [String: String] + /// A map of OpenAPI paths to desired Types + public var typeOverrides: [String: String] /// Additional pre-release features to enable. public var featureFlags: FeatureFlags @@ -73,6 +75,7 @@ public struct Config: Sendable { /// Defaults to `defensive`. /// - nameOverrides: A map of OpenAPI identifiers to desired Swift identifiers, used instead /// of the naming strategy. + /// - typeOverrides: A map of OpenAPI paths to desired Types /// - featureFlags: Additional pre-release features to enable. public init( mode: GeneratorMode, @@ -81,6 +84,7 @@ public struct Config: Sendable { filter: DocumentFilter? = nil, namingStrategy: NamingStrategy, nameOverrides: [String: String] = [:], + typeOverrides: [String: String] = [:], featureFlags: FeatureFlags = [] ) { self.mode = mode @@ -89,6 +93,7 @@ public struct Config: Sendable { self.filter = filter self.namingStrategy = namingStrategy self.nameOverrides = nameOverrides + self.typeOverrides = typeOverrides self.featureFlags = featureFlags } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift index e92b5605..59942560 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift @@ -87,6 +87,17 @@ extension TypesFileTranslator { ) ) } + if let jsonPath = typeName.shortJSONName, let typeOverride = config.typeOverrides[jsonPath] { + let typeOverride = TypeName(swiftKeyPath: typeOverride.components(separatedBy: ".")) + let typealiasDecl = try translateTypealias( + named: typeName, + userDescription: overrides.userDescription ?? schema.description, + to: typeOverride.asUsage.withOptional( + overrides.isOptional ?? typeMatcher.isOptional(schema, components: components) + ) + ) + return [typealiasDecl] + } // If this type maps to a referenceable schema, define a typealias if let builtinType = try typeMatcher.tryMatchReferenceableType(for: schema, components: components) { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index d1fbedcf..0f036db6 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -372,7 +372,7 @@ enum Constants { /// The substring used in method names for the multipart coding strategy. static let multipart: String = "Multipart" } - + /// Constants related to types used in many components. enum Global { diff --git a/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md b/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md index bdaff6a5..c8a91e34 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md @@ -55,3 +55,4 @@ If you have any questions, tag [Honza Dvorsky](https://github.com/czechboy0) or - - - +- diff --git a/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md new file mode 100644 index 00000000..520e0c88 --- /dev/null +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md @@ -0,0 +1,129 @@ +# SOAR-0014: Support Type Overrides + +Allow using user-defined types instead of generated ones + +## Overview + +- Proposal: SOAR-0014 +- Author(s): [simonbility](https://github.com/simonbility) +- Status: **Awaiting Review** +- Issue: [apple/swift-openapi-generator#375](https://github.com/apple/swift-openapi-generator/issues/375) +- Implementation: + - [apple/swift-openapi-generator#764](https://github.com/apple/swift-openapi-generator/pull/764) +- Affected components: + - generator + +### Introduction + +The goal of this proposal is to allow users to specify custom types for generated. This will enable users to use their own types instead of the default generated ones, allowing for greater flexibility. + +### Motivation + +This proposal would enable more flexibility in the generated code. +Some usecases include: +- Using custom types that are already defined in the user's codebase or even coming from a third party library, instead of generating new ones. +- workaround missing support for `format` for strings +- Implement custom validation/encoding/decoding logic that cannot be expressed using the OpenAPI spec + +This is intended as a "escape hatch" for use-cases that (currently) cannot be expressed. +Using this comes with the risk of user-provided types not being compliant with the original OpenAPI spec. + + +### Proposed solution + +The proposed solution is to allow specifying typeOverrides using a new configuration option named `typeOverrides`. +This is only supported for schemas defined in the `components.schemas` section of a OpenAPI document. + +### Example +A current limitiation is string formats are not directly supported by the generator. (for example, `uuid` is not supported) + +With this proposal this can be worked around with with the following approach (This proposal does not preclude extending support for formats in the future): + +Given schemas defined in the OpenAPI document like this: +```yaml + components: + schemas: + UUID: + type: string + format: uuid +``` + +Adding typeOverrides like this in the configuration + +```diff ++ typeOverrides: ++ UUID: Foundation.UUID +``` + +Will affect the generated code in the following way: +```diff + /// Types generated from the `#/components/schemas` section of the OpenAPI document. + package enum Schemas { + /// - Remark: Generated from `#/components/schemas/UUID`. +- package typealias Uuid = Swift.String ++ package typealias Uuid = Foundation.UUID + } +``` + +### Detailed design + +In the configuration file a new `typeOverrides` option i supported. +It contains mapping from the original name (as defined in the OpenAPI document) to a override type name to use instead of the generated name. + +The mapping is evaluated relative to `#/components/schemas` + +So defining overrides like this: + +```diff +typeOverrides: + OriginalName: NewName +``` + +will replace the generated type for `#/components/schemas/OriginalName` with `NewName`. + +Its in the users responsibility to ensure that the type is valid and available. +It must conform to `Codable`, `Hashable` and `Sendable` + + +### API stability + +While this proposal does affect the generated code, it requires the user to explicitly opt-in to using the `typeOverrides` configuration option. + +This is interpreted as a "strong enough" signal of the user to opt into this behaviour, to justify NOT introducing a feauture-flag or considering this a breaking change. + + +### Future directions + +The implementation could potentially be extended to support inline defined properties as well. +This could be done by supporting "Paths" insteand of names in the mapping. + +For example with the following schema. +```yaml + components: + schemas: + User: + properties: + id: + type: string + format: uuid +``` + +This configuration could be used to override the type of `id`: +```yaml +typeOverrides: + 'User/id': Foundation.UUID +``` + + +### Alternatives considered +An alternative to the mapping defined in the configuration file is to use a vendor extension (for instance `x-swift-open-api-override-type`) in the OpenAPI document itself. + +```yaml +... +schemas: + UUID: + type: string + x-swift-open-api-override-type: Foundation.UUID +``` + +The current proposal using the configuration file was preferred because it does not rely on modifying the OpenAPI document itself, which is not always possible/straightforward when its provided by a third-party. diff --git a/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift b/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift index d91f7ad8..1f116abb 100644 --- a/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift +++ b/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift @@ -34,6 +34,7 @@ extension _GenerateOptions { let resolvedAdditionalImports = resolvedAdditionalImports(config) let resolvedNamingStragy = resolvedNamingStrategy(config) let resolvedNameOverrides = resolvedNameOverrides(config) + let resolvedTypeOverrides = resolvedTypeOverrides(config) let resolvedFeatureFlags = resolvedFeatureFlags(config) let configs: [Config] = sortedModes.map { .init( @@ -43,6 +44,7 @@ extension _GenerateOptions { filter: config?.filter, namingStrategy: resolvedNamingStragy, nameOverrides: resolvedNameOverrides, + typeOverrides: resolvedTypeOverrides, featureFlags: resolvedFeatureFlags ) } @@ -59,6 +61,9 @@ extension _GenerateOptions { - Name overrides: \(resolvedNameOverrides.isEmpty ? "" : resolvedNameOverrides .sorted(by: { $0.key < $1.key }) .map { "\"\($0.key)\"->\"\($0.value)\"" }.joined(separator: ", ")) + - Type overrides: \(resolvedTypeOverrides.isEmpty ? "" : resolvedTypeOverrides + .sorted(by: { $0.key < $1.key }) + .map { "\"\($0.key)\"->\"\($0.value)\"" }.joined(separator: ", ")) - Feature flags: \(resolvedFeatureFlags.isEmpty ? "" : resolvedFeatureFlags.map(\.rawValue).joined(separator: ", ")) - Output file names: \(sortedModes.map(\.outputFileName).joined(separator: ", ")) - Output directory: \(outputDirectory.path) diff --git a/Sources/swift-openapi-generator/GenerateOptions.swift b/Sources/swift-openapi-generator/GenerateOptions.swift index 6935a134..d1167b98 100644 --- a/Sources/swift-openapi-generator/GenerateOptions.swift +++ b/Sources/swift-openapi-generator/GenerateOptions.swift @@ -93,6 +93,11 @@ extension _GenerateOptions { /// - Parameter config: The configuration specified by the user. /// - Returns: The name overrides requested by the user func resolvedNameOverrides(_ config: _UserConfig?) -> [String: String] { config?.nameOverrides ?? [:] } + + /// Returns the type overrides requested by the user. + /// - Parameter config: The configuration specified by the user. + /// - Returns: The type overrides requested by the user + func resolvedTypeOverrides(_ config: _UserConfig?) -> [String: String] { config?.typeOverrides ?? [:] } /// Returns a list of the feature flags requested by the user. /// - Parameter config: The configuration specified by the user. diff --git a/Sources/swift-openapi-generator/UserConfig.swift b/Sources/swift-openapi-generator/UserConfig.swift index 239f67b0..8c616839 100644 --- a/Sources/swift-openapi-generator/UserConfig.swift +++ b/Sources/swift-openapi-generator/UserConfig.swift @@ -41,6 +41,9 @@ struct _UserConfig: Codable { /// Any names not included use the `namingStrategy` to compute a Swift name. var nameOverrides: [String: String]? + /// A dictionary of overrides for replacing the types of generated with manually provided types + var typeOverrides: [String: String]? + /// A set of features to explicitly enable. var featureFlags: FeatureFlags? @@ -54,6 +57,7 @@ struct _UserConfig: Codable { case filter case namingStrategy case nameOverrides + case typeOverrides case featureFlags } } diff --git a/Tests/OpenAPIGeneratorCoreTests/DeclarationHelpers.swift b/Tests/OpenAPIGeneratorCoreTests/DeclarationHelpers.swift new file mode 100644 index 00000000..e33e6a1f --- /dev/null +++ b/Tests/OpenAPIGeneratorCoreTests/DeclarationHelpers.swift @@ -0,0 +1,66 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// +@testable import _OpenAPIGeneratorCore + +extension Declaration { + var commentable: (Comment?, Declaration)? { + guard case .commentable(let comment, let decl) = self else { return nil } + return (comment, decl) + } + + var deprecated: (DeprecationDescription, Declaration)? { + guard case .deprecated(let description, let decl) = self else { return nil } + return (description, decl) + } + + var variable: VariableDescription? { + guard case .variable(let description) = self else { return nil } + return description + } + + var `extension`: ExtensionDescription? { + guard case .extension(let description) = self else { return nil } + return description + } + + var `struct`: StructDescription? { + guard case .struct(let description) = self else { return nil } + return description + } + + var `enum`: EnumDescription? { + guard case .enum(let description) = self else { return nil } + return description + } + + var `typealias`: TypealiasDescription? { + guard case .typealias(let description) = self else { return nil } + return description + } + + var `protocol`: ProtocolDescription? { + guard case .protocol(let description) = self else { return nil } + return description + } + + var function: FunctionDescription? { + guard case .function(let description) = self else { return nil } + return description + } + + var enumCase: EnumCaseDescription? { + guard case .enumCase(let description) = self else { return nil } + return description + } +} diff --git a/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift b/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift index 48651d1c..bfe8a248 100644 --- a/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift +++ b/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift @@ -30,6 +30,7 @@ class Test_Core: XCTestCase { diagnostics: any DiagnosticCollector = PrintingDiagnosticCollector(), namingStrategy: NamingStrategy = .defensive, nameOverrides: [String: String] = [:], + typeOverrides: [String: String] = [:], featureFlags: FeatureFlags = [] ) -> TypesFileTranslator { makeTypesTranslator( @@ -37,6 +38,7 @@ class Test_Core: XCTestCase { diagnostics: diagnostics, namingStrategy: namingStrategy, nameOverrides: nameOverrides, + typeOverrides: typeOverrides, featureFlags: featureFlags ) } @@ -46,12 +48,14 @@ class Test_Core: XCTestCase { diagnostics: any DiagnosticCollector = PrintingDiagnosticCollector(), namingStrategy: NamingStrategy = .defensive, nameOverrides: [String: String] = [:], + typeOverrides: [String: String] = [:], featureFlags: FeatureFlags = [] ) -> TypesFileTranslator { TypesFileTranslator( config: makeConfig( namingStrategy: namingStrategy, nameOverrides: nameOverrides, + typeOverrides: typeOverrides, featureFlags: featureFlags ), diagnostics: diagnostics, @@ -62,6 +66,7 @@ class Test_Core: XCTestCase { func makeConfig( namingStrategy: NamingStrategy = .defensive, nameOverrides: [String: String] = [:], + typeOverrides: [String: String] = [:], featureFlags: FeatureFlags = [] ) -> Config { .init( @@ -69,6 +74,7 @@ class Test_Core: XCTestCase { access: Config.defaultAccessModifier, namingStrategy: namingStrategy, nameOverrides: nameOverrides, + typeOverrides: typeOverrides, featureFlags: featureFlags ) } @@ -76,6 +82,9 @@ class Test_Core: XCTestCase { func loadSchemaFromYAML(_ yamlString: String) throws -> JSONSchema { try YAMLDecoder().decode(JSONSchema.self, from: yamlString) } + func loadComponentsFromYAML(_ yamlString: String) throws -> OpenAPI.Components { + try YAMLDecoder().decode(OpenAPI.Components.self, from: yamlString) + } static var testTypeName: TypeName { .init(swiftKeyPath: ["Foo"]) } diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift new file mode 100644 index 00000000..ab526db9 --- /dev/null +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift @@ -0,0 +1,88 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest +import OpenAPIKit +import Yams +@testable import _OpenAPIGeneratorCore + +class Test_typeOverrides: Test_Core { + func testSchemas() throws { + let components = try loadComponentsFromYAML( + #""" + schemas: + User: + type: object + properties: + id: + $ref: '#/components/schemas/UUID' + UUID: + type: string + format: uuid + """# + ) + let translator = makeTranslator( + components: components, + typeOverrides: ["#/components/schemas/UUID": "Foundation.UUID"] + ) + let translated = try translator.translateSchemas(components.schemas, multipartSchemaNames: []) + .strippingTopComment + guard let enumDecl = translated.enum else { return XCTFail("Expected enum declaration") } + let typeAliases = enumDecl.members.compactMap(\.strippingTopComment.typealias) + XCTAssertEqual( + typeAliases, + [ + TypealiasDescription( + accessModifier: .internal, + name: "UUID", + existingType: .member(["Foundation", "UUID"]) + ) + ] + ) + } + + func testTypeOverrideWithNameOverride() throws { + let components = try loadComponentsFromYAML( + #""" + schemas: + User: + type: object + properties: + id: + $ref: '#/components/schemas/UUID' + UUID: + type: string + format: uuid + """# + ) + let translator = makeTranslator( + components: components, + nameOverrides: ["UUID": "MyUUID"], + typeOverrides: ["UUID": "Foundation.UUID"] + ) + let translated = try translator.translateSchemas(components.schemas, multipartSchemaNames: []) + .strippingTopComment + guard let enumDecl = translated.enum else { return XCTFail("Expected enum declaration") } + let typeAliases = enumDecl.members.compactMap(\.strippingTopComment.typealias) + XCTAssertEqual( + typeAliases, + [ + TypealiasDescription( + accessModifier: .internal, + name: "MyUUID", + existingType: .member(["Foundation", "UUID"]) + ) + ] + ) + } +}