From 68ef99c0b7ebd59cb974e4c08810094dae81fbfb Mon Sep 17 00:00:00 2001 From: simonbility Date: Thu, 24 Apr 2025 20:45:01 +0200 Subject: [PATCH 01/13] Allow substituting types --- .../CommonTranslations/translateSchema.swift | 15 +++++++++++ .../Test_translateSchemas.swift | 26 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift index e92b5605..dc2cac4b 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift @@ -87,6 +87,21 @@ extension TypesFileTranslator { ) ) } + if let substituteTypeName = schema.vendorExtensions["x-swift-open-api-substitute-type"]?.value + as? String + { + try diagnostics.emit(.note(message: "Substituting type \(typeName) with \(substituteTypeName)")) + let substitutedType = TypeName(swiftKeyPath: substituteTypeName.components(separatedBy: ".")).asUsage + + let typealiasDecl = try translateTypealias( + named: typeName, + userDescription: overrides.userDescription ?? schema.description, + to: substitutedType.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/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_translateSchemas.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_translateSchemas.swift index 1d1e89f5..6ae27414 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_translateSchemas.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_translateSchemas.swift @@ -47,4 +47,30 @@ class Test_translateSchemas: Test_Core { XCTAssertEqual(collector.diagnostics.map(\.description), diagnosticDescriptions) } } + + func testSchemaTypeSubstitution() throws { + let typeName = TypeName(swiftKeyPath: ["Foo"]) + + let schema = try loadSchemaFromYAML( + #""" + type: string + x-swift-open-api-substitute-type: MyLibrary.MyCustomType + """# + ) + let collector = AccumulatingDiagnosticCollector() + let translator = makeTranslator(diagnostics: collector) + let translated = try translator.translateSchema(typeName: typeName, schema: schema, overrides: .none) + + XCTAssertEqual( + collector.diagnostics.map(\.description), + ["note: Substituting type Foo with MyLibrary.MyCustomType"] + ) + XCTAssertTrue(translated.count == 1, "Should have one translated schema") + guard case let .typealias(typeAliasDescription) = translated.first?.strippingTopComment else { + XCTFail("Expected typealias description got") + return + } + XCTAssertEqual(typeAliasDescription.name, "Foo") + XCTAssertEqual(typeAliasDescription.existingType, .member(["MyLibrary", "MyCustomType"])) + } } From 762fbdd6697216784f49d1f8b9dcdb658d78baf8 Mon Sep 17 00:00:00 2001 From: simonbility Date: Wed, 14 May 2025 13:42:55 +0200 Subject: [PATCH 02/13] Support replacement also in inline defined schemas --- .../CommonTranslations/translateSchema.swift | 11 ++--- .../translateSubstitutedType.swift | 36 ++++++++++++++ .../Translator/CommonTypes/Constants.swift | 2 + .../TypeAssignment/TypeMatcher.swift | 49 +++++++++++++++++-- 4 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSubstitutedType.swift diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift index dc2cac4b..839db3f2 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift @@ -87,16 +87,11 @@ extension TypesFileTranslator { ) ) } - if let substituteTypeName = schema.vendorExtensions["x-swift-open-api-substitute-type"]?.value - as? String - { - try diagnostics.emit(.note(message: "Substituting type \(typeName) with \(substituteTypeName)")) - let substitutedType = TypeName(swiftKeyPath: substituteTypeName.components(separatedBy: ".")).asUsage - - let typealiasDecl = try translateTypealias( + if let substituteType = schema.value.substituteType() { + let typealiasDecl = try translateSubstitutedType( named: typeName, userDescription: overrides.userDescription ?? schema.description, - to: substitutedType.withOptional( + to: substituteType.asUsage.withOptional( overrides.isOptional ?? typeMatcher.isOptional(schema, components: components) ) ) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSubstitutedType.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSubstitutedType.swift new file mode 100644 index 00000000..6973d4fa --- /dev/null +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSubstitutedType.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import OpenAPIKit + +extension FileTranslator { + + /// Returns a declaration of a typealias. + /// - Parameters: + /// - typeName: The name of the type to give to the declared typealias. + /// - userDescription: A user-specified description from the OpenAPI document. + /// - existingTypeUsage: The existing type the alias points to. + /// - Throws: An error if there is an issue during translation. + /// - Returns: A declaration representing the translated typealias. + func translateSubstitutedType(named typeName: TypeName, userDescription: String?, to existingTypeUsage: TypeUsage) throws + -> Declaration + { + let typealiasDescription = TypealiasDescription( + accessModifier: config.access, + name: typeName.shortSwiftName, + existingType: .init(existingTypeUsage.withOptional(false)) + ) + let typealiasComment: Comment? = typeName.docCommentWithUserDescription(userDescription) + return .commentable(typealiasComment, .typealias(typealiasDescription)) + } +} diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index d1fbedcf..66bc10d7 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -372,6 +372,8 @@ enum Constants { /// The substring used in method names for the multipart coding strategy. static let multipart: String = "Multipart" } + /// Constants related to the vendor extensions.. + enum VendorExtension { static let replaceType: String = "x-swift-open-api-replace-type" } /// Constants related to types used in many components. enum Global { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift index 1c503ae7..716cef85 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// import OpenAPIKit +import Foundation /// A set of functions that match Swift types onto OpenAPI types. struct TypeMatcher { @@ -46,7 +47,11 @@ struct TypeMatcher { matchedArrayHandler: { elementType, nullableItems in nullableItems ? elementType.asOptional.asArray : elementType.asArray }, - genericArrayHandler: { TypeName.arrayContainer.asUsage } + genericArrayHandler: { TypeName.arrayContainer.asUsage }, + substitutedTypeHandler: { substitute in + // never built-in + nil + } ) } @@ -75,7 +80,10 @@ struct TypeMatcher { matchedArrayHandler: { elementType, nullableItems in nullableItems ? elementType.asOptional.asArray : elementType.asArray }, - genericArrayHandler: { TypeName.arrayContainer.asUsage } + genericArrayHandler: { TypeName.arrayContainer.asUsage }, + substitutedTypeHandler: { substitute in + substitute.asUsage + } )? .withOptional(isOptional(schema, components: components)) } @@ -98,7 +106,8 @@ struct TypeMatcher { return true }, matchedArrayHandler: { elementIsReferenceable, _ in elementIsReferenceable }, - genericArrayHandler: { true } + genericArrayHandler: { true }, + substitutedTypeHandler: { _ in true } ) ?? false } @@ -353,8 +362,10 @@ struct TypeMatcher { for schema: JSONSchema.Schema, test: (JSONSchema.Schema) throws -> R?, matchedArrayHandler: (R, _ nullableItems: Bool) -> R, - genericArrayHandler: () -> R + genericArrayHandler: () -> R, + substitutedTypeHandler: (TypeName) -> R? ) rethrows -> R? { + if let substitute = schema.substituteType() { return substitutedTypeHandler(substitute) } switch schema { case let .array(_, arrayContext): guard let items = arrayContext.items else { return genericArrayHandler() } @@ -363,7 +374,8 @@ struct TypeMatcher { for: items.value, test: test, matchedArrayHandler: matchedArrayHandler, - genericArrayHandler: genericArrayHandler + genericArrayHandler: genericArrayHandler, + substitutedTypeHandler: substitutedTypeHandler ) else { return nil } return matchedArrayHandler(itemsResult, items.nullable) @@ -371,3 +383,30 @@ struct TypeMatcher { } } } + +extension JSONSchema.Schema { + func substituteType() -> TypeName? { + let extensions: [String: AnyCodable] = + switch self { + case .null(let context): context.vendorExtensions + case .boolean(let context): context.vendorExtensions + case .number(let context, _): context.vendorExtensions + case .integer(let context, _): context.vendorExtensions + case .string(let context, _): context.vendorExtensions + case .object(let context, _): context.vendorExtensions + case .array(let context, _): context.vendorExtensions + case .all(of: _, core: let context): context.vendorExtensions + case .one(of: _, core: let context): context.vendorExtensions + case .any(of: _, core: let context): context.vendorExtensions + case .not: [:] + case .reference(_, let context): context.vendorExtensions + case .fragment(let context): context.vendorExtensions + } + guard let substituteTypeName = extensions[Constants.VendorExtension.replaceType]?.value as? String else { + return nil + } + assert(!substituteTypeName.isEmpty) + + return TypeName(swiftKeyPath: substituteTypeName.components(separatedBy: ".")) + } +} From cff57605279fb25909a9d5ee1d626fc4f76d6687 Mon Sep 17 00:00:00 2001 From: simonbility Date: Wed, 14 May 2025 13:43:04 +0200 Subject: [PATCH 03/13] More tests --- .../Test_translateSchemas.swift | 26 --- .../Test_typeSubstitutions.swift | 216 ++++++++++++++++++ 2 files changed, 216 insertions(+), 26 deletions(-) create mode 100644 Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeSubstitutions.swift diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_translateSchemas.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_translateSchemas.swift index 6ae27414..1d1e89f5 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_translateSchemas.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_translateSchemas.swift @@ -47,30 +47,4 @@ class Test_translateSchemas: Test_Core { XCTAssertEqual(collector.diagnostics.map(\.description), diagnosticDescriptions) } } - - func testSchemaTypeSubstitution() throws { - let typeName = TypeName(swiftKeyPath: ["Foo"]) - - let schema = try loadSchemaFromYAML( - #""" - type: string - x-swift-open-api-substitute-type: MyLibrary.MyCustomType - """# - ) - let collector = AccumulatingDiagnosticCollector() - let translator = makeTranslator(diagnostics: collector) - let translated = try translator.translateSchema(typeName: typeName, schema: schema, overrides: .none) - - XCTAssertEqual( - collector.diagnostics.map(\.description), - ["note: Substituting type Foo with MyLibrary.MyCustomType"] - ) - XCTAssertTrue(translated.count == 1, "Should have one translated schema") - guard case let .typealias(typeAliasDescription) = translated.first?.strippingTopComment else { - XCTFail("Expected typealias description got") - return - } - XCTAssertEqual(typeAliasDescription.name, "Foo") - XCTAssertEqual(typeAliasDescription.existingType, .member(["MyLibrary", "MyCustomType"])) - } } diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeSubstitutions.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeSubstitutions.swift new file mode 100644 index 00000000..2e0f2f63 --- /dev/null +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeSubstitutions.swift @@ -0,0 +1,216 @@ +//===----------------------------------------------------------------------===// +// +// 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_typeSubstitutions: Test_Core { + + func testSchemaString() throws { + func _test( + schema schemaString: String, + expectedType: ExistingTypeDescription, + file: StaticString = #file, + line: UInt = #line + ) throws { + let typeName = TypeName(swiftKeyPath: ["Foo"]) + + let schema = try loadSchemaFromYAML(schemaString) + let collector = AccumulatingDiagnosticCollector() + let translator = makeTranslator(diagnostics: collector) + let translated = try translator.translateSchema(typeName: typeName, schema: schema, overrides: .none) + if translated.count != 1 { + XCTFail("Expected only a single translated schema, got: \(translated.count)", file: file, line: line) + return + } + XCTAssertTrue(translated.count == 1, "Should have one translated schema") + guard case let .typealias(typeAliasDescription) = translated.first?.strippingTopComment else { + XCTFail("Expected typealias description got", file: file, line: line) + return + } + XCTAssertEqual(typeAliasDescription.name, "Foo", file: file, line: line) + XCTAssertEqual(typeAliasDescription.existingType, expectedType, file: file, line: line) + } + try _test( + schema: #""" + type: string + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """#, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + try _test( + schema: """ + type: array + items: + type: integer + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + try _test( + schema: """ + type: string + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + try _test( + schema: """ + type: array + items: + type: integer + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .array(.member(["MyLibrary", "MyCustomType"])) + ) + // TODO: Investigate if vendor-extensions are allowed in anyOf, allOf, oneOf + try _test( + schema: """ + anyOf: + - type: string + - type: integer + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + try _test( + schema: """ + allOf: + - type: object + properties: + foo: + type: string + - type: object + properties: + bar: + type: string + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + try _test( + schema: """ + oneOf: + - type: object + properties: + foo: + type: string + - type: object + properties: + bar: + type: string + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + } + func testSimpleInlinePropertiesReplacements() throws { + func _testInlineProperty( + schema schemaString: String, + expectedType: ExistingTypeDescription, + file: StaticString = #file, + line: UInt = #line + ) throws { + let typeName = TypeName(swiftKeyPath: ["Foo"]) + + let propertySchema = try YAMLDecoder().decode(JSONSchema.self, from: schemaString).requiredSchemaObject() + let schema = JSONSchema.object(properties: ["property": propertySchema]) + let collector = AccumulatingDiagnosticCollector() + let translator = makeTranslator(diagnostics: collector) + let translated = try translator.translateSchema(typeName: typeName, schema: schema, overrides: .none) + if translated.count != 1 { + XCTFail("Expected only a single translated schema, got: \(translated.count)", file: file, line: line) + return + } + guard case let .struct(structDescription) = translated.first?.strippingTopComment else { + throw GenericError(message: "Expected struct") + } + let variables: [VariableDescription] = structDescription.members.compactMap { member in + guard case let .variable(variableDescription) = member.strippingTopComment else { return nil } + return variableDescription + } + if variables.count != 1 { + XCTFail("Expected only a single variable, got: \(variables.count)", file: file, line: line) + return + } + XCTAssertEqual(variables[0].type, expectedType, file: file, line: line) + } + try _testInlineProperty( + schema: """ + type: array + items: + type: integer + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + try _testInlineProperty( + schema: """ + type: string + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + try _testInlineProperty( + schema: """ + type: array + items: + type: integer + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .array(.member(["MyLibrary", "MyCustomType"])) + ) + // TODO: Investigate if vendor-extensions are allowed in anyOf, allOf, oneOf + try _testInlineProperty( + schema: """ + anyOf: + - type: string + - type: integer + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + try _testInlineProperty( + schema: """ + allOf: + - type: object + properties: + foo: + type: string + - type: object + properties: + bar: + type: string + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + try _testInlineProperty( + schema: """ + oneOf: + - type: object + properties: + foo: + type: string + - type: object + properties: + bar: + type: string + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + } +} From 0227843a58e053f837e110d45db73470f24b8d56 Mon Sep 17 00:00:00 2001 From: simonbility Date: Wed, 14 May 2025 14:23:17 +0200 Subject: [PATCH 04/13] WIP Proposal --- .../Documentation.docc/Proposals/Proposals.md | 1 + .../Documentation.docc/Proposals/SOAR-0014.md | 72 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md 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..59b69d72 --- /dev/null +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md @@ -0,0 +1,72 @@ +# SOAR-0014: Support Type Substitutions + +Allow using user-defined types instead of generated ones, utilizing vendor-extensions + +## 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 code using vendor extensions. 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` +- 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 the `x-swift-open-api-replace-type` vendor-extension to prevent the generation of types as usual, and instead use the specified type. +This should be supported anywhere within a OpenAPI document where a schema can be defined. +This includes: +* "TopLevel" schemas in `components.schemas` +* "Inline" schema definitions in object + +```diff + + +``` + + +Describe your solution to the problem. Provide examples and describe how they work. Show how your solution is better than current workarounds. + +This section should focus on what will change for the _adopters_ of Swift OpenAPI Generator. + +### Detailed design + +Describe the implementation of the feature, a link to a prototype implementation is encouraged here. + +This section should focus on what will change for the _contributors_ to Swift OpenAPI Generator. + +### API stability + +Discuss the API implications, making sure to considering all of: +- runtime public API +- runtime "Generated" SPI +- existing transport and middleware implementations +- generator implementation affected by runtime API changes +- generator API (config file, CLI, plugin) +- existing and new generated adopter code + +### Future directions + +Discuss any potential future improvements to the feature. + +### Alternatives considered + +Discuss the alternative solutions considered, even during the review process itself. From 8fe84b72219d89e25df800bd1e1df652e1ea96c4 Mon Sep 17 00:00:00 2001 From: simonbility Date: Wed, 14 May 2025 14:02:47 +0200 Subject: [PATCH 05/13] Add Example Project --- Examples/replace-types-example/.gitignore | 11 + Examples/replace-types-example/Package.swift | 37 +++ Examples/replace-types-example/README.md | 40 +++ .../ExternalLibrary/ExternalObject.swift | 4 + .../Sources/ExternalLibrary/PrimeNumber.swift | 35 +++ .../Sources/Types/Generated/Types.swift | 257 ++++++++++++++++++ .../Types/openapi-generator-config.yaml | 4 + .../Sources/Types/openapi.yaml | 1 + .../Sources/openapi.yaml | 46 ++++ 9 files changed, 435 insertions(+) create mode 100644 Examples/replace-types-example/.gitignore create mode 100644 Examples/replace-types-example/Package.swift create mode 100644 Examples/replace-types-example/README.md create mode 100644 Examples/replace-types-example/Sources/ExternalLibrary/ExternalObject.swift create mode 100644 Examples/replace-types-example/Sources/ExternalLibrary/PrimeNumber.swift create mode 100644 Examples/replace-types-example/Sources/Types/Generated/Types.swift create mode 100644 Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml create mode 120000 Examples/replace-types-example/Sources/Types/openapi.yaml create mode 100644 Examples/replace-types-example/Sources/openapi.yaml 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..c5dc2180 --- /dev/null +++ b/Examples/replace-types-example/Sources/Types/Generated/Types.swift @@ -0,0 +1,257 @@ +// 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 +/// 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 = Swift.String + /// A value with the greeting contents. + /// + /// - 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/favorite_prime_number`. + package var favoritePrimeNumber: Swift.Int? + /// - Remark: Generated from `#/components/schemas/User/foo`. + package struct FooPayload: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/User/foo/foo`. + package var foo: Swift.String? + /// - Remark: Generated from `#/components/schemas/User/foo/bar`. + package var bar: Swift.String? + /// Creates a new `FooPayload`. + /// + /// - Parameters: + /// - foo: + /// - bar: + package init( + foo: Swift.String? = nil, + bar: Swift.String? = nil + ) { + self.foo = foo + self.bar = bar + } + package enum CodingKeys: String, CodingKey { + case foo + case bar + } + } + /// - Remark: Generated from `#/components/schemas/User/foo`. + package var foo: Components.Schemas.User.FooPayload? + /// Creates a new `User`. + /// + /// - Parameters: + /// - id: + /// - favoritePrimeNumber: + /// - foo: + package init( + id: Components.Schemas.Uuid? = nil, + favoritePrimeNumber: Swift.Int? = nil, + foo: Components.Schemas.User.FooPayload? = nil + ) { + self.id = id + self.favoritePrimeNumber = favoritePrimeNumber + self.foo = foo + } + package enum CodingKeys: String, CodingKey { + case id + case favoritePrimeNumber = "favorite_prime_number" + case foo + } + } + } + /// 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..a12e67bf --- /dev/null +++ b/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml @@ -0,0 +1,4 @@ +generate: + - types +accessModifier: package +namingStrategy: idiomatic 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..bcdfb7a1 --- /dev/null +++ b/Examples/replace-types-example/Sources/openapi.yaml @@ -0,0 +1,46 @@ +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: + type: string + format: uuid + + User: + type: object + description: A value with the greeting contents. + properties: + id: + $ref: '#/components/schemas/UUID' + favorite_prime_number: + type: integer + foo: + type: object + properties: + foo: + type: string + bar: + type: string From 8646df857879128e3a1ad2cbaf576ce50d770f66 Mon Sep 17 00:00:00 2001 From: simonbility Date: Wed, 14 May 2025 14:38:34 +0200 Subject: [PATCH 06/13] Apply x-swift-open-api-replace-type extension --- .../Sources/Types/Generated/Types.swift | 35 ++++--------------- .../Types/openapi-generator-config.yaml | 3 ++ .../Sources/openapi.yaml | 4 +++ 3 files changed, 14 insertions(+), 28 deletions(-) diff --git a/Examples/replace-types-example/Sources/Types/Generated/Types.swift b/Examples/replace-types-example/Sources/Types/Generated/Types.swift index c5dc2180..d617ebe7 100644 --- a/Examples/replace-types-example/Sources/Types/Generated/Types.swift +++ b/Examples/replace-types-example/Sources/Types/Generated/Types.swift @@ -9,6 +9,8 @@ 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`. @@ -58,7 +60,7 @@ 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 = Swift.String + package typealias Uuid = Foundation.UUID /// A value with the greeting contents. /// /// - Remark: Generated from `#/components/schemas/User`. @@ -66,32 +68,9 @@ package enum Components { /// - Remark: Generated from `#/components/schemas/User/id`. package var id: Components.Schemas.Uuid? /// - Remark: Generated from `#/components/schemas/User/favorite_prime_number`. - package var favoritePrimeNumber: Swift.Int? + package var favoritePrimeNumber: ExternalLibrary.PrimeNumber? /// - Remark: Generated from `#/components/schemas/User/foo`. - package struct FooPayload: Codable, Hashable, Sendable { - /// - Remark: Generated from `#/components/schemas/User/foo/foo`. - package var foo: Swift.String? - /// - Remark: Generated from `#/components/schemas/User/foo/bar`. - package var bar: Swift.String? - /// Creates a new `FooPayload`. - /// - /// - Parameters: - /// - foo: - /// - bar: - package init( - foo: Swift.String? = nil, - bar: Swift.String? = nil - ) { - self.foo = foo - self.bar = bar - } - package enum CodingKeys: String, CodingKey { - case foo - case bar - } - } - /// - Remark: Generated from `#/components/schemas/User/foo`. - package var foo: Components.Schemas.User.FooPayload? + package var foo: ExternalLibrary.ExternalObject? /// Creates a new `User`. /// /// - Parameters: @@ -100,8 +79,8 @@ package enum Components { /// - foo: package init( id: Components.Schemas.Uuid? = nil, - favoritePrimeNumber: Swift.Int? = nil, - foo: Components.Schemas.User.FooPayload? = nil + favoritePrimeNumber: ExternalLibrary.PrimeNumber? = nil, + foo: ExternalLibrary.ExternalObject? = nil ) { self.id = id self.favoritePrimeNumber = favoritePrimeNumber diff --git a/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml b/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml index a12e67bf..585b5e7d 100644 --- a/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml +++ b/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml @@ -2,3 +2,6 @@ generate: - types accessModifier: package namingStrategy: idiomatic +additionalImports: + - Foundation + - ExternalLibrary diff --git a/Examples/replace-types-example/Sources/openapi.yaml b/Examples/replace-types-example/Sources/openapi.yaml index bcdfb7a1..dc75821a 100644 --- a/Examples/replace-types-example/Sources/openapi.yaml +++ b/Examples/replace-types-example/Sources/openapi.yaml @@ -28,6 +28,7 @@ components: UUID: type: string format: uuid + x-swift-open-api-replace-type: Foundation.UUID User: type: object @@ -37,6 +38,7 @@ components: $ref: '#/components/schemas/UUID' favorite_prime_number: type: integer + x-swift-open-api-replace-type: ExternalLibrary.PrimeNumber foo: type: object properties: @@ -44,3 +46,5 @@ components: type: string bar: type: string + + x-swift-open-api-replace-type: ExternalLibrary.ExternalObject From 37b854432a3e0c97e259a2c2e32347ba46d377f0 Mon Sep 17 00:00:00 2001 From: simonbility Date: Fri, 16 May 2025 15:20:03 +0200 Subject: [PATCH 07/13] Add additionalProperties to examples --- .../Sources/Types/Generated/Types.swift | 43 ++++++++++++++++++- .../Sources/openapi.yaml | 6 ++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/Examples/replace-types-example/Sources/Types/Generated/Types.swift b/Examples/replace-types-example/Sources/Types/Generated/Types.swift index d617ebe7..6fa854ce 100644 --- a/Examples/replace-types-example/Sources/Types/Generated/Types.swift +++ b/Examples/replace-types-example/Sources/Types/Generated/Types.swift @@ -71,26 +71,67 @@ package enum Components { package var favoritePrimeNumber: ExternalLibrary.PrimeNumber? /// - Remark: Generated from `#/components/schemas/User/foo`. package var foo: ExternalLibrary.ExternalObject? + /// A container of undocumented properties. + package var additionalProperties: [String: OpenAPIRuntime.OpenAPIObjectContainer] /// Creates a new `User`. /// /// - Parameters: /// - id: /// - favoritePrimeNumber: /// - foo: + /// - additionalProperties: A container of undocumented properties. package init( id: Components.Schemas.Uuid? = nil, favoritePrimeNumber: ExternalLibrary.PrimeNumber? = nil, - foo: ExternalLibrary.ExternalObject? = nil + foo: ExternalLibrary.ExternalObject? = nil, + additionalProperties: [String: OpenAPIRuntime.OpenAPIObjectContainer] = .init() ) { self.id = id self.favoritePrimeNumber = favoritePrimeNumber self.foo = foo + self.additionalProperties = additionalProperties } package enum CodingKeys: String, CodingKey { case id case favoritePrimeNumber = "favorite_prime_number" case foo } + package init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decodeIfPresent( + Components.Schemas.Uuid.self, + forKey: .id + ) + self.favoritePrimeNumber = try container.decodeIfPresent( + ExternalLibrary.PrimeNumber.self, + forKey: .favoritePrimeNumber + ) + self.foo = try container.decodeIfPresent( + ExternalLibrary.ExternalObject.self, + forKey: .foo + ) + additionalProperties = try decoder.decodeAdditionalProperties(knownKeys: [ + "id", + "favorite_prime_number", + "foo" + ]) + } + package func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent( + self.id, + forKey: .id + ) + try container.encodeIfPresent( + self.favoritePrimeNumber, + forKey: .favoritePrimeNumber + ) + try container.encodeIfPresent( + self.foo, + forKey: .foo + ) + try encoder.encodeAdditionalProperties(additionalProperties) + } } } /// Types generated from the `#/components/parameters` section of the OpenAPI document. diff --git a/Examples/replace-types-example/Sources/openapi.yaml b/Examples/replace-types-example/Sources/openapi.yaml index dc75821a..69a30002 100644 --- a/Examples/replace-types-example/Sources/openapi.yaml +++ b/Examples/replace-types-example/Sources/openapi.yaml @@ -46,5 +46,9 @@ components: type: string bar: type: string - x-swift-open-api-replace-type: ExternalLibrary.ExternalObject + default: + foo: "foo" + bar: "bar" + additionalProperties: + type: object From dbf467d059caa5835a4858c20fb2c4be0c678fbf Mon Sep 17 00:00:00 2001 From: simonbility Date: Fri, 16 May 2025 15:23:58 +0200 Subject: [PATCH 08/13] Replace in additionalProperties --- .../Sources/Types/Generated/Types.swift | 4 +-- .../Sources/openapi.yaml | 27 ++++++++++--------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/Examples/replace-types-example/Sources/Types/Generated/Types.swift b/Examples/replace-types-example/Sources/Types/Generated/Types.swift index 6fa854ce..56d45551 100644 --- a/Examples/replace-types-example/Sources/Types/Generated/Types.swift +++ b/Examples/replace-types-example/Sources/Types/Generated/Types.swift @@ -72,7 +72,7 @@ package enum Components { /// - Remark: Generated from `#/components/schemas/User/foo`. package var foo: ExternalLibrary.ExternalObject? /// A container of undocumented properties. - package var additionalProperties: [String: OpenAPIRuntime.OpenAPIObjectContainer] + package var additionalProperties: [String: ExternalLibrary.PrimeNumber] /// Creates a new `User`. /// /// - Parameters: @@ -84,7 +84,7 @@ package enum Components { id: Components.Schemas.Uuid? = nil, favoritePrimeNumber: ExternalLibrary.PrimeNumber? = nil, foo: ExternalLibrary.ExternalObject? = nil, - additionalProperties: [String: OpenAPIRuntime.OpenAPIObjectContainer] = .init() + additionalProperties: [String: ExternalLibrary.PrimeNumber] = .init() ) { self.id = id self.favoritePrimeNumber = favoritePrimeNumber diff --git a/Examples/replace-types-example/Sources/openapi.yaml b/Examples/replace-types-example/Sources/openapi.yaml index 69a30002..8d1e4d33 100644 --- a/Examples/replace-types-example/Sources/openapi.yaml +++ b/Examples/replace-types-example/Sources/openapi.yaml @@ -37,18 +37,19 @@ components: id: $ref: '#/components/schemas/UUID' favorite_prime_number: - type: integer - x-swift-open-api-replace-type: ExternalLibrary.PrimeNumber + type: integer + x-swift-open-api-replace-type: ExternalLibrary.PrimeNumber foo: - type: object - properties: - foo: - type: string - bar: - type: string - x-swift-open-api-replace-type: ExternalLibrary.ExternalObject - default: - foo: "foo" - bar: "bar" + type: object + properties: + foo: + type: string + bar: + type: string + x-swift-open-api-replace-type: ExternalLibrary.ExternalObject + default: + foo: "foo" + bar: "bar" additionalProperties: - type: object + type: integer + x-swift-open-api-replace-type: ExternalLibrary.PrimeNumber From 108386aa79dcc00dfaec78505da335d00613bbee Mon Sep 17 00:00:00 2001 From: simonbility Date: Fri, 16 May 2025 15:49:46 +0200 Subject: [PATCH 09/13] Update ProposalText --- .../Documentation.docc/Proposals/SOAR-0014.md | 100 ++++++++++++++---- 1 file changed, 80 insertions(+), 20 deletions(-) diff --git a/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md index 59b69d72..528a9aee 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md @@ -22,7 +22,7 @@ The goal of this proposal is to allow users to specify custom types for generate 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` +- 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. @@ -32,41 +32,101 @@ Using this comes with the risk of user-provided types not being compliant with t ### Proposed solution The proposed solution is to allow the `x-swift-open-api-replace-type` vendor-extension to prevent the generation of types as usual, and instead use the specified type. -This should be supported anywhere within a OpenAPI document where a schema can be defined. -This includes: -* "TopLevel" schemas in `components.schemas` -* "Inline" schema definitions in object +This should be supported anywhere within a OpenAPI document where a schema can be defined. (e.g. `components.schemas`, `properites`, `additionalProperties`, etc.) -```diff +It can be used in "top-level" schemas, defined in `components.schemas` +```diff + components: + schemas: + UUID: + type: string + format: uuid ++ x-swift-open-api-replace-type: Foundation.UUID +``` +Will affect the generated code in the following way: +```diff + /// Types generated from the `#/components/schemas` section of the OpenAPI document. + enum Schemas { + /// - Remark: Generated from `#/components/schemas/UUID`. +- package typealias Uuid = Swift.String ++ package typealias Uuid = Foundation.UUID ``` -Describe your solution to the problem. Provide examples and describe how they work. Show how your solution is better than current workarounds. +This will also work for properties defined inline -This section should focus on what will change for the _adopters_ of Swift OpenAPI Generator. +```diff + components: + schemas: + UUID: + type: string + format: uuid + + User: + type: object + properties: + id: + type: string + name: + type: string ++ x-swift-open-api-replace-type: ExternalLibrary.ExternallyDefinedUser +``` -### Detailed design +Will affect the generated code in the following way: -Describe the implementation of the feature, a link to a prototype implementation is encouraged here. +```diff +enum Schemas { + /// - 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 +- } +- } ++ package typealias User = ExternalLibrary.ExternallyDefinedUser +} +``` + +### Detailed design -This section should focus on what will change for the _contributors_ to Swift OpenAPI Generator. +The implementation modifies the Translator and the TypeAssignement logic to account for the presence of the vendor extension. ### API stability -Discuss the API implications, making sure to considering all of: -- runtime public API -- runtime "Generated" SPI -- existing transport and middleware implementations -- generator implementation affected by runtime API changes -- generator API (config file, CLI, plugin) -- existing and new generated adopter code +While this proposal does affect the generated code, it requires the addition of a very specific vendor-extension. + +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 -Discuss any potential future improvements to the feature. +None so far. ### Alternatives considered +An alternative to relying on vendor-extension, was to allow specifying the types to be replaced via paths in the config file like this + +```yaml +... +replaceTypes: + #/components/schemas/User: Foundation.UUID +``` -Discuss the alternative solutions considered, even during the review process itself. +The advantage of this approach is that it could also be used without modifying the OpenAPI document. (which is not always possible/straightforward when using third party API-specs) From 715b35d29e5c8a23dae05bbdfd3ce50254e77937 Mon Sep 17 00:00:00 2001 From: simonbility Date: Mon, 19 May 2025 07:21:11 +0200 Subject: [PATCH 10/13] Remove vendor-extensions implementation --- .../Sources/Types/Generated/Types.swift | 63 +---- .../Sources/openapi.yaml | 21 +- Sources/_OpenAPIGeneratorCore/Config.swift | 5 + .../CommonTranslations/translateSchema.swift | 7 +- .../translateSubstitutedType.swift | 36 --- .../Translator/CommonTypes/Constants.swift | 4 +- .../Translator/FileTranslator.swift | 3 +- .../TypeAssignment/TypeMatcher.swift | 49 +--- .../GenerateOptions+runGenerator.swift | 5 + .../GenerateOptions.swift | 5 + .../swift-openapi-generator/UserConfig.swift | 4 + .../DeclarationHelpers.swift | 66 ++++++ .../TestUtilities.swift | 9 + .../Test_OperationDescription.swift | 2 +- .../TypesTranslator/Test_typeOverrides.swift | 53 +++++ .../Test_typeSubstitutions.swift | 216 ------------------ 16 files changed, 169 insertions(+), 379 deletions(-) delete mode 100644 Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSubstitutedType.swift create mode 100644 Tests/OpenAPIGeneratorCoreTests/DeclarationHelpers.swift create mode 100644 Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift delete mode 100644 Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeSubstitutions.swift diff --git a/Examples/replace-types-example/Sources/Types/Generated/Types.swift b/Examples/replace-types-example/Sources/Types/Generated/Types.swift index 56d45551..1c851c81 100644 --- a/Examples/replace-types-example/Sources/Types/Generated/Types.swift +++ b/Examples/replace-types-example/Sources/Types/Generated/Types.swift @@ -60,77 +60,28 @@ 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 - /// A value with the greeting contents. - /// + package typealias Uuid = Swift.String /// - 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/favorite_prime_number`. - package var favoritePrimeNumber: ExternalLibrary.PrimeNumber? - /// - Remark: Generated from `#/components/schemas/User/foo`. - package var foo: ExternalLibrary.ExternalObject? - /// A container of undocumented properties. - package var additionalProperties: [String: ExternalLibrary.PrimeNumber] + /// - Remark: Generated from `#/components/schemas/User/name`. + package var name: Swift.String? /// Creates a new `User`. /// /// - Parameters: /// - id: - /// - favoritePrimeNumber: - /// - foo: - /// - additionalProperties: A container of undocumented properties. + /// - name: package init( id: Components.Schemas.Uuid? = nil, - favoritePrimeNumber: ExternalLibrary.PrimeNumber? = nil, - foo: ExternalLibrary.ExternalObject? = nil, - additionalProperties: [String: ExternalLibrary.PrimeNumber] = .init() + name: Swift.String? = nil ) { self.id = id - self.favoritePrimeNumber = favoritePrimeNumber - self.foo = foo - self.additionalProperties = additionalProperties + self.name = name } package enum CodingKeys: String, CodingKey { case id - case favoritePrimeNumber = "favorite_prime_number" - case foo - } - package init(from decoder: any Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.id = try container.decodeIfPresent( - Components.Schemas.Uuid.self, - forKey: .id - ) - self.favoritePrimeNumber = try container.decodeIfPresent( - ExternalLibrary.PrimeNumber.self, - forKey: .favoritePrimeNumber - ) - self.foo = try container.decodeIfPresent( - ExternalLibrary.ExternalObject.self, - forKey: .foo - ) - additionalProperties = try decoder.decodeAdditionalProperties(knownKeys: [ - "id", - "favorite_prime_number", - "foo" - ]) - } - package func encode(to encoder: any Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent( - self.id, - forKey: .id - ) - try container.encodeIfPresent( - self.favoritePrimeNumber, - forKey: .favoritePrimeNumber - ) - try container.encodeIfPresent( - self.foo, - forKey: .foo - ) - try encoder.encodeAdditionalProperties(additionalProperties) + case name } } } diff --git a/Examples/replace-types-example/Sources/openapi.yaml b/Examples/replace-types-example/Sources/openapi.yaml index 8d1e4d33..97a1974b 100644 --- a/Examples/replace-types-example/Sources/openapi.yaml +++ b/Examples/replace-types-example/Sources/openapi.yaml @@ -28,28 +28,11 @@ components: UUID: type: string format: uuid - x-swift-open-api-replace-type: Foundation.UUID User: type: object - description: A value with the greeting contents. properties: id: $ref: '#/components/schemas/UUID' - favorite_prime_number: - type: integer - x-swift-open-api-replace-type: ExternalLibrary.PrimeNumber - foo: - type: object - properties: - foo: - type: string - bar: - type: string - x-swift-open-api-replace-type: ExternalLibrary.ExternalObject - default: - foo: "foo" - bar: "bar" - additionalProperties: - type: integer - x-swift-open-api-replace-type: ExternalLibrary.PrimeNumber + 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 839db3f2..a7c807c2 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift @@ -87,11 +87,12 @@ extension TypesFileTranslator { ) ) } - if let substituteType = schema.value.substituteType() { - let typealiasDecl = try translateSubstitutedType( + if let jsonPath = typeName.fullyQualifiedJSONPath, let typeOverride = config.typeOverrides[jsonPath] { + let typeOverride = TypeName(swiftKeyPath: typeOverride.components(separatedBy: ".")) + let typealiasDecl = try translateTypealias( named: typeName, userDescription: overrides.userDescription ?? schema.description, - to: substituteType.asUsage.withOptional( + to: typeOverride.asUsage.withOptional( overrides.isOptional ?? typeMatcher.isOptional(schema, components: components) ) ) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSubstitutedType.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSubstitutedType.swift deleted file mode 100644 index 6973d4fa..00000000 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSubstitutedType.swift +++ /dev/null @@ -1,36 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftOpenAPIGenerator open source project -// -// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// -import OpenAPIKit - -extension FileTranslator { - - /// Returns a declaration of a typealias. - /// - Parameters: - /// - typeName: The name of the type to give to the declared typealias. - /// - userDescription: A user-specified description from the OpenAPI document. - /// - existingTypeUsage: The existing type the alias points to. - /// - Throws: An error if there is an issue during translation. - /// - Returns: A declaration representing the translated typealias. - func translateSubstitutedType(named typeName: TypeName, userDescription: String?, to existingTypeUsage: TypeUsage) throws - -> Declaration - { - let typealiasDescription = TypealiasDescription( - accessModifier: config.access, - name: typeName.shortSwiftName, - existingType: .init(existingTypeUsage.withOptional(false)) - ) - let typealiasComment: Comment? = typeName.docCommentWithUserDescription(userDescription) - return .commentable(typealiasComment, .typealias(typealiasDescription)) - } -} diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index 66bc10d7..0f036db6 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -372,9 +372,7 @@ enum Constants { /// The substring used in method names for the multipart coding strategy. static let multipart: String = "Multipart" } - /// Constants related to the vendor extensions.. - enum VendorExtension { static let replaceType: String = "x-swift-open-api-replace-type" } - + /// Constants related to types used in many components. enum Global { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift index 00dde5d8..66686f33 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift @@ -81,7 +81,7 @@ extension FileTranslator { upstream: safeNameGenerator, overrides: config.nameOverrides ) - return TranslatorContext(safeNameGenerator: overridingGenerator) + return TranslatorContext(safeNameGenerator: overridingGenerator, typeOverrides: config.typeOverrides) } } @@ -90,4 +90,5 @@ struct TranslatorContext { /// A type that generates safe names for use as Swift identifiers. var safeNameGenerator: any SafeNameGenerator + var typeOverrides: [String: String] } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift index 716cef85..1c503ae7 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift @@ -12,7 +12,6 @@ // //===----------------------------------------------------------------------===// import OpenAPIKit -import Foundation /// A set of functions that match Swift types onto OpenAPI types. struct TypeMatcher { @@ -47,11 +46,7 @@ struct TypeMatcher { matchedArrayHandler: { elementType, nullableItems in nullableItems ? elementType.asOptional.asArray : elementType.asArray }, - genericArrayHandler: { TypeName.arrayContainer.asUsage }, - substitutedTypeHandler: { substitute in - // never built-in - nil - } + genericArrayHandler: { TypeName.arrayContainer.asUsage } ) } @@ -80,10 +75,7 @@ struct TypeMatcher { matchedArrayHandler: { elementType, nullableItems in nullableItems ? elementType.asOptional.asArray : elementType.asArray }, - genericArrayHandler: { TypeName.arrayContainer.asUsage }, - substitutedTypeHandler: { substitute in - substitute.asUsage - } + genericArrayHandler: { TypeName.arrayContainer.asUsage } )? .withOptional(isOptional(schema, components: components)) } @@ -106,8 +98,7 @@ struct TypeMatcher { return true }, matchedArrayHandler: { elementIsReferenceable, _ in elementIsReferenceable }, - genericArrayHandler: { true }, - substitutedTypeHandler: { _ in true } + genericArrayHandler: { true } ) ?? false } @@ -362,10 +353,8 @@ struct TypeMatcher { for schema: JSONSchema.Schema, test: (JSONSchema.Schema) throws -> R?, matchedArrayHandler: (R, _ nullableItems: Bool) -> R, - genericArrayHandler: () -> R, - substitutedTypeHandler: (TypeName) -> R? + genericArrayHandler: () -> R ) rethrows -> R? { - if let substitute = schema.substituteType() { return substitutedTypeHandler(substitute) } switch schema { case let .array(_, arrayContext): guard let items = arrayContext.items else { return genericArrayHandler() } @@ -374,8 +363,7 @@ struct TypeMatcher { for: items.value, test: test, matchedArrayHandler: matchedArrayHandler, - genericArrayHandler: genericArrayHandler, - substitutedTypeHandler: substitutedTypeHandler + genericArrayHandler: genericArrayHandler ) else { return nil } return matchedArrayHandler(itemsResult, items.nullable) @@ -383,30 +371,3 @@ struct TypeMatcher { } } } - -extension JSONSchema.Schema { - func substituteType() -> TypeName? { - let extensions: [String: AnyCodable] = - switch self { - case .null(let context): context.vendorExtensions - case .boolean(let context): context.vendorExtensions - case .number(let context, _): context.vendorExtensions - case .integer(let context, _): context.vendorExtensions - case .string(let context, _): context.vendorExtensions - case .object(let context, _): context.vendorExtensions - case .array(let context, _): context.vendorExtensions - case .all(of: _, core: let context): context.vendorExtensions - case .one(of: _, core: let context): context.vendorExtensions - case .any(of: _, core: let context): context.vendorExtensions - case .not: [:] - case .reference(_, let context): context.vendorExtensions - case .fragment(let context): context.vendorExtensions - } - guard let substituteTypeName = extensions[Constants.VendorExtension.replaceType]?.value as? String else { - return nil - } - assert(!substituteTypeName.isEmpty) - - return TypeName(swiftKeyPath: substituteTypeName.components(separatedBy: ".")) - } -} 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/Operations/Test_OperationDescription.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/Operations/Test_OperationDescription.swift index abd341a7..b082ecdc 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(), - context: .init(safeNameGenerator: .defensive) + context: .init(safeNameGenerator: .defensive, typeOverrides: [:]) ) } } diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift new file mode 100644 index 00000000..5d1916fb --- /dev/null +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift @@ -0,0 +1,53 @@ +//===----------------------------------------------------------------------===// +// +// 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(\.typealias) + XCTAssertEqual( + typeAliases, + [ + TypealiasDescription( + accessModifier: .internal, + name: "UUID", + existingType: .member(["Foundation", "UUID"]) + ) + ] + ) + } +} diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeSubstitutions.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeSubstitutions.swift deleted file mode 100644 index 2e0f2f63..00000000 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeSubstitutions.swift +++ /dev/null @@ -1,216 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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_typeSubstitutions: Test_Core { - - func testSchemaString() throws { - func _test( - schema schemaString: String, - expectedType: ExistingTypeDescription, - file: StaticString = #file, - line: UInt = #line - ) throws { - let typeName = TypeName(swiftKeyPath: ["Foo"]) - - let schema = try loadSchemaFromYAML(schemaString) - let collector = AccumulatingDiagnosticCollector() - let translator = makeTranslator(diagnostics: collector) - let translated = try translator.translateSchema(typeName: typeName, schema: schema, overrides: .none) - if translated.count != 1 { - XCTFail("Expected only a single translated schema, got: \(translated.count)", file: file, line: line) - return - } - XCTAssertTrue(translated.count == 1, "Should have one translated schema") - guard case let .typealias(typeAliasDescription) = translated.first?.strippingTopComment else { - XCTFail("Expected typealias description got", file: file, line: line) - return - } - XCTAssertEqual(typeAliasDescription.name, "Foo", file: file, line: line) - XCTAssertEqual(typeAliasDescription.existingType, expectedType, file: file, line: line) - } - try _test( - schema: #""" - type: string - x-swift-open-api-replace-type: MyLibrary.MyCustomType - """#, - expectedType: .member(["MyLibrary", "MyCustomType"]) - ) - try _test( - schema: """ - type: array - items: - type: integer - x-swift-open-api-replace-type: MyLibrary.MyCustomType - """, - expectedType: .member(["MyLibrary", "MyCustomType"]) - ) - try _test( - schema: """ - type: string - x-swift-open-api-replace-type: MyLibrary.MyCustomType - """, - expectedType: .member(["MyLibrary", "MyCustomType"]) - ) - try _test( - schema: """ - type: array - items: - type: integer - x-swift-open-api-replace-type: MyLibrary.MyCustomType - """, - expectedType: .array(.member(["MyLibrary", "MyCustomType"])) - ) - // TODO: Investigate if vendor-extensions are allowed in anyOf, allOf, oneOf - try _test( - schema: """ - anyOf: - - type: string - - type: integer - x-swift-open-api-replace-type: MyLibrary.MyCustomType - """, - expectedType: .member(["MyLibrary", "MyCustomType"]) - ) - try _test( - schema: """ - allOf: - - type: object - properties: - foo: - type: string - - type: object - properties: - bar: - type: string - x-swift-open-api-replace-type: MyLibrary.MyCustomType - """, - expectedType: .member(["MyLibrary", "MyCustomType"]) - ) - try _test( - schema: """ - oneOf: - - type: object - properties: - foo: - type: string - - type: object - properties: - bar: - type: string - x-swift-open-api-replace-type: MyLibrary.MyCustomType - """, - expectedType: .member(["MyLibrary", "MyCustomType"]) - ) - } - func testSimpleInlinePropertiesReplacements() throws { - func _testInlineProperty( - schema schemaString: String, - expectedType: ExistingTypeDescription, - file: StaticString = #file, - line: UInt = #line - ) throws { - let typeName = TypeName(swiftKeyPath: ["Foo"]) - - let propertySchema = try YAMLDecoder().decode(JSONSchema.self, from: schemaString).requiredSchemaObject() - let schema = JSONSchema.object(properties: ["property": propertySchema]) - let collector = AccumulatingDiagnosticCollector() - let translator = makeTranslator(diagnostics: collector) - let translated = try translator.translateSchema(typeName: typeName, schema: schema, overrides: .none) - if translated.count != 1 { - XCTFail("Expected only a single translated schema, got: \(translated.count)", file: file, line: line) - return - } - guard case let .struct(structDescription) = translated.first?.strippingTopComment else { - throw GenericError(message: "Expected struct") - } - let variables: [VariableDescription] = structDescription.members.compactMap { member in - guard case let .variable(variableDescription) = member.strippingTopComment else { return nil } - return variableDescription - } - if variables.count != 1 { - XCTFail("Expected only a single variable, got: \(variables.count)", file: file, line: line) - return - } - XCTAssertEqual(variables[0].type, expectedType, file: file, line: line) - } - try _testInlineProperty( - schema: """ - type: array - items: - type: integer - x-swift-open-api-replace-type: MyLibrary.MyCustomType - """, - expectedType: .member(["MyLibrary", "MyCustomType"]) - ) - try _testInlineProperty( - schema: """ - type: string - x-swift-open-api-replace-type: MyLibrary.MyCustomType - """, - expectedType: .member(["MyLibrary", "MyCustomType"]) - ) - try _testInlineProperty( - schema: """ - type: array - items: - type: integer - x-swift-open-api-replace-type: MyLibrary.MyCustomType - """, - expectedType: .array(.member(["MyLibrary", "MyCustomType"])) - ) - // TODO: Investigate if vendor-extensions are allowed in anyOf, allOf, oneOf - try _testInlineProperty( - schema: """ - anyOf: - - type: string - - type: integer - x-swift-open-api-replace-type: MyLibrary.MyCustomType - """, - expectedType: .member(["MyLibrary", "MyCustomType"]) - ) - try _testInlineProperty( - schema: """ - allOf: - - type: object - properties: - foo: - type: string - - type: object - properties: - bar: - type: string - x-swift-open-api-replace-type: MyLibrary.MyCustomType - """, - expectedType: .member(["MyLibrary", "MyCustomType"]) - ) - try _testInlineProperty( - schema: """ - oneOf: - - type: object - properties: - foo: - type: string - - type: object - properties: - bar: - type: string - x-swift-open-api-replace-type: MyLibrary.MyCustomType - """, - expectedType: .member(["MyLibrary", "MyCustomType"]) - ) - } -} From 4922fa7c22708d2cb038e35f388cc7876345eb91 Mon Sep 17 00:00:00 2001 From: simonbility Date: Mon, 19 May 2025 07:35:22 +0200 Subject: [PATCH 11/13] Add example --- .../replace-types-example/Sources/Types/Generated/Types.swift | 2 +- .../Sources/Types/openapi-generator-config.yaml | 2 ++ Examples/replace-types-example/Sources/openapi.yaml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Examples/replace-types-example/Sources/Types/Generated/Types.swift b/Examples/replace-types-example/Sources/Types/Generated/Types.swift index 1c851c81..80084c74 100644 --- a/Examples/replace-types-example/Sources/Types/Generated/Types.swift +++ b/Examples/replace-types-example/Sources/Types/Generated/Types.swift @@ -60,7 +60,7 @@ 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 = Swift.String + package typealias Uuid = Foundation.UUID /// - Remark: Generated from `#/components/schemas/User`. package struct User: Codable, Hashable, Sendable { /// - Remark: Generated from `#/components/schemas/User/id`. diff --git a/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml b/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml index 585b5e7d..c9ef0876 100644 --- a/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml +++ b/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml @@ -5,3 +5,5 @@ namingStrategy: idiomatic additionalImports: - Foundation - ExternalLibrary +typeOverrides: + UUID: Foundation.UUID diff --git a/Examples/replace-types-example/Sources/openapi.yaml b/Examples/replace-types-example/Sources/openapi.yaml index 97a1974b..eabfb5cf 100644 --- a/Examples/replace-types-example/Sources/openapi.yaml +++ b/Examples/replace-types-example/Sources/openapi.yaml @@ -25,7 +25,7 @@ paths: $ref: '#/components/schemas/User' components: schemas: - UUID: + UUID: # this will be replaced by with Foundation.UUID specified by typeOverrides in open-api-generator-config type: string format: uuid From af6c2421aa8fe3b8d100c324b122ee62d8837030 Mon Sep 17 00:00:00 2001 From: simonbility Date: Mon, 19 May 2025 08:05:40 +0200 Subject: [PATCH 12/13] more tests --- .../CommonTranslations/translateSchema.swift | 2 +- .../Translator/FileTranslator.swift | 3 +- .../Test_OperationDescription.swift | 2 +- .../TypesTranslator/Test_typeOverrides.swift | 37 ++++++++++++++++++- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift index a7c807c2..59942560 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift @@ -87,7 +87,7 @@ extension TypesFileTranslator { ) ) } - if let jsonPath = typeName.fullyQualifiedJSONPath, let typeOverride = config.typeOverrides[jsonPath] { + if let jsonPath = typeName.shortJSONName, let typeOverride = config.typeOverrides[jsonPath] { let typeOverride = TypeName(swiftKeyPath: typeOverride.components(separatedBy: ".")) let typealiasDecl = try translateTypealias( named: typeName, diff --git a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift index 66686f33..00dde5d8 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift @@ -81,7 +81,7 @@ extension FileTranslator { upstream: safeNameGenerator, overrides: config.nameOverrides ) - return TranslatorContext(safeNameGenerator: overridingGenerator, typeOverrides: config.typeOverrides) + return TranslatorContext(safeNameGenerator: overridingGenerator) } } @@ -90,5 +90,4 @@ struct TranslatorContext { /// A type that generates safe names for use as Swift identifiers. var safeNameGenerator: any SafeNameGenerator - var typeOverrides: [String: String] } diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/Operations/Test_OperationDescription.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/Operations/Test_OperationDescription.swift index b082ecdc..abd341a7 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(), - context: .init(safeNameGenerator: .defensive, typeOverrides: [:]) + context: .init(safeNameGenerator: .defensive) ) } } diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift index 5d1916fb..ab526db9 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift @@ -38,7 +38,7 @@ class Test_typeOverrides: Test_Core { 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(\.typealias) + let typeAliases = enumDecl.members.compactMap(\.strippingTopComment.typealias) XCTAssertEqual( typeAliases, [ @@ -50,4 +50,39 @@ class Test_typeOverrides: Test_Core { ] ) } + + 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"]) + ) + ] + ) + } } From 2130376fb452a278ceec897963f3da2fa6de1be0 Mon Sep 17 00:00:00 2001 From: simonbility Date: Mon, 19 May 2025 08:49:59 +0200 Subject: [PATCH 13/13] Update Proposal Document --- .../Documentation.docc/Proposals/SOAR-0014.md | 119 +++++++++--------- 1 file changed, 58 insertions(+), 61 deletions(-) diff --git a/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md index 528a9aee..520e0c88 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md @@ -1,6 +1,6 @@ -# SOAR-0014: Support Type Substitutions +# SOAR-0014: Support Type Overrides -Allow using user-defined types instead of generated ones, utilizing vendor-extensions +Allow using user-defined types instead of generated ones ## Overview @@ -15,7 +15,7 @@ Allow using user-defined types instead of generated ones, utilizing vendor-exten ### Introduction -The goal of this proposal is to allow users to specify custom types for generated code using vendor extensions. This will enable users to use their own types instead of the default generated ones, allowing for greater flexibility. +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 @@ -31,102 +31,99 @@ Using this comes with the risk of user-provided types not being compliant with t ### Proposed solution -The proposed solution is to allow the `x-swift-open-api-replace-type` vendor-extension to prevent the generation of types as usual, and instead use the specified type. -This should be supported anywhere within a OpenAPI document where a schema can be defined. (e.g. `components.schemas`, `properites`, `additionalProperties`, etc.) +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. -It can be used in "top-level" schemas, defined in `components.schemas` +### Example +A current limitiation is string formats are not directly supported by the generator. (for example, `uuid` is not supported) -```diff +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 -+ x-swift-open-api-replace-type: Foundation.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. - enum Schemas { + package enum Schemas { /// - Remark: Generated from `#/components/schemas/UUID`. - package typealias Uuid = Swift.String + package typealias Uuid = Foundation.UUID + } ``` +### Detailed design -This will also work for properties defined inline +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. -```diff - components: - schemas: - UUID: - type: string - format: uuid - - User: - type: object - properties: - id: - type: string - name: - type: string -+ x-swift-open-api-replace-type: ExternalLibrary.ExternallyDefinedUser -``` +The mapping is evaluated relative to `#/components/schemas` -Will affect the generated code in the following way: +So defining overrides like this: ```diff -enum Schemas { - /// - 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 -- } -- } -+ package typealias User = ExternalLibrary.ExternallyDefinedUser -} +typeOverrides: + OriginalName: NewName ``` -### Detailed design +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` -The implementation modifies the Translator and the TypeAssignement logic to account for the presence of the vendor extension. ### API stability -While this proposal does affect the generated code, it requires the addition of a very specific vendor-extension. +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 -None so far. +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 relying on vendor-extension, was to allow specifying the types to be replaced via paths in the config file like this +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 ... -replaceTypes: - #/components/schemas/User: Foundation.UUID +schemas: + UUID: + type: string + x-swift-open-api-override-type: Foundation.UUID ``` -The advantage of this approach is that it could also be used without modifying the OpenAPI document. (which is not always possible/straightforward when using third party API-specs) +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.