From 68ef99c0b7ebd59cb974e4c08810094dae81fbfb Mon Sep 17 00:00:00 2001 From: simonbility Date: Thu, 24 Apr 2025 20:45:01 +0200 Subject: [PATCH 01/35] 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 e92b5605a..dc2cac4b3 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 1d1e89f52..6ae27414c 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/35] 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 dc2cac4b3..839db3f2b 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 000000000..6973d4fab --- /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 d1fbedcf3..66bc10d7a 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 1c503ae74..716cef854 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/35] 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 6ae27414c..1d1e89f52 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 000000000..2e0f2f635 --- /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/35] 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 bdaff6a59..c8a91e34d 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 000000000..59b69d72b --- /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/35] 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 000000000..f6f5465e0 --- /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 000000000..4f0f4d411 --- /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 000000000..974e6d52f --- /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 000000000..5082fd55f --- /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 000000000..3d7dddd41 --- /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 000000000..c5dc2180a --- /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 000000000..a12e67bf7 --- /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 000000000..1c2a243ef --- /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 000000000..bcdfb7a12 --- /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/35] 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 c5dc2180a..d617ebe76 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 a12e67bf7..585b5e7dd 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 bcdfb7a12..dc75821ab 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/35] 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 d617ebe76..6fa854ce5 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 dc75821ab..69a30002f 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/35] 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 6fa854ce5..56d45551d 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 69a30002f..8d1e4d334 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/35] 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 59b69d72b..528a9aee0 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/35] 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 56d45551d..1c851c81d 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 8d1e4d334..97a1974bf 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 b49537422..ff40405b5 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 839db3f2b..a7c807c2e 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 6973d4fab..000000000 --- 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 66bc10d7a..0f036db6d 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 00dde5d85..66686f331 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 716cef854..1c503ae74 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 d91f7ad8e..1f116abb6 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 6935a1346..d1167b98c 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 239f67b09..8c616839c 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 000000000..e33e6a1f4 --- /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 48651d1c1..bfe8a2485 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 abd341a71..b082ecdcc 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 000000000..5d1916fbf --- /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 2e0f2f635..000000000 --- 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/35] 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 1c851c81d..80084c740 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 585b5e7dd..c9ef08769 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 97a1974bf..eabfb5cfa 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/35] 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 a7c807c2e..599425604 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 66686f331..00dde5d85 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 b082ecdcc..abd341a71 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 5d1916fbf..ab526db95 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/35] 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 528a9aee0..520e0c887 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. From f14f7101c9beca72e148f84c798c373d05b4fd5b Mon Sep 17 00:00:00 2001 From: simonbility Date: Sun, 25 May 2025 14:35:23 +0200 Subject: [PATCH 14/35] add type-overrides in nested config --- .../Types/openapi-generator-config.yaml | 3 ++- Sources/_OpenAPIGeneratorCore/Config.swift | 5 ++-- .../CommonTranslations/translateSchema.swift | 2 +- .../_OpenAPIGeneratorCore/TypeOverrides.swift | 25 +++++++++++++++++++ .../Documentation.docc/Proposals/SOAR-0014.md | 18 +++++++------ .../GenerateOptions+runGenerator.swift | 11 +++++--- .../GenerateOptions.swift | 6 +++-- .../swift-openapi-generator/UserConfig.swift | 8 +++++- .../TestUtilities.swift | 12 ++++----- .../TypesTranslator/Test_typeOverrides.swift | 4 +-- 10 files changed, 69 insertions(+), 25 deletions(-) create mode 100644 Sources/_OpenAPIGeneratorCore/TypeOverrides.swift 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 c9ef08769..178a58f37 100644 --- a/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml +++ b/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml @@ -6,4 +6,5 @@ additionalImports: - Foundation - ExternalLibrary typeOverrides: - UUID: Foundation.UUID + schemas: + UUID: Foundation.UUID diff --git a/Sources/_OpenAPIGeneratorCore/Config.swift b/Sources/_OpenAPIGeneratorCore/Config.swift index ff40405b5..58a478826 100644 --- a/Sources/_OpenAPIGeneratorCore/Config.swift +++ b/Sources/_OpenAPIGeneratorCore/Config.swift @@ -59,8 +59,9 @@ 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] + public var typeOverrides: TypeOverrides /// Additional pre-release features to enable. public var featureFlags: FeatureFlags @@ -84,7 +85,7 @@ public struct Config: Sendable { filter: DocumentFilter? = nil, namingStrategy: NamingStrategy, nameOverrides: [String: String] = [:], - typeOverrides: [String: String] = [:], + typeOverrides: TypeOverrides = TypeOverrides(), featureFlags: FeatureFlags = [] ) { self.mode = mode diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift index 599425604..891bb930b 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.shortJSONName, let typeOverride = config.typeOverrides[jsonPath] { + if let jsonPath = typeName.shortJSONName, let typeOverride = config.typeOverrides.schemas[jsonPath] { let typeOverride = TypeName(swiftKeyPath: typeOverride.components(separatedBy: ".")) let typealiasDecl = try translateTypealias( named: typeName, diff --git a/Sources/_OpenAPIGeneratorCore/TypeOverrides.swift b/Sources/_OpenAPIGeneratorCore/TypeOverrides.swift new file mode 100644 index 000000000..89aace6ba --- /dev/null +++ b/Sources/_OpenAPIGeneratorCore/TypeOverrides.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +public struct TypeOverrides: Sendable { + /// A dictionary of overrides for replacing the types generated from schemas with manually provided types + public var schemas: [String: String] + + /// Creates a new instance of `TypeOverrides` + /// - Parameter schemas: A dictionary mapping schema names to their override types. + public init(schemas: [String: String] = [:]) { self.schemas = schemas } + + /// A Boolean value indicating whether there are no overrides. + public var isEmpty: Bool { schemas.isEmpty } +} 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 520e0c887..0c4d52064 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md @@ -52,7 +52,8 @@ Adding typeOverrides like this in the configuration ```diff + typeOverrides: -+ UUID: Foundation.UUID ++ schemas: ++ UUID: Foundation.UUID ``` Will affect the generated code in the following way: @@ -76,7 +77,8 @@ So defining overrides like this: ```diff typeOverrides: - OriginalName: NewName + schemas: + OriginalName: NewName ``` will replace the generated type for `#/components/schemas/OriginalName` with `NewName`. @@ -111,7 +113,8 @@ For example with the following schema. This configuration could be used to override the type of `id`: ```yaml typeOverrides: - 'User/id': Foundation.UUID + schemas: + 'User/id': Foundation.UUID ``` @@ -120,10 +123,11 @@ An alternative to the mapping defined in the configuration file is to use a vend ```yaml ... -schemas: - UUID: - type: string - x-swift-open-api-override-type: Foundation.UUID +components: + schemas: + UUID: + type: string + x-swift-open-api-override-type: Foundation.UUID ``` The current proposal using the configuration file was preferred because it does not rely on modifying the OpenAPI document itself, which is not always possible/straightforward when its provided by a third-party. diff --git a/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift b/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift index 1f116abb6..996663da8 100644 --- a/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift +++ b/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift @@ -50,6 +50,13 @@ extension _GenerateOptions { } let (diagnostics, finalizeDiagnostics) = preparedDiagnosticsCollector(outputPath: diagnosticsOutputPath) let doc = self.docPath + let typeOverridesDescription = """ + + - Schemas: \(resolvedTypeOverrides.schemas.isEmpty ? "" : resolvedTypeOverrides.schemas + .sorted(by: { $0.key < $1.key }) + .map { "\"\($0.key)\"->\"\($0.value)\"" } + .joined(separator: ", ")) + """ print( """ Swift OpenAPI Generator is running with the following configuration: @@ -61,9 +68,7 @@ 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: ", ")) + - Type overrides: \(resolvedTypeOverrides.isEmpty ? "" : typeOverridesDescription) - 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 d1167b98c..a419ea1a6 100644 --- a/Sources/swift-openapi-generator/GenerateOptions.swift +++ b/Sources/swift-openapi-generator/GenerateOptions.swift @@ -93,11 +93,13 @@ 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 ?? [:] } + func resolvedTypeOverrides(_ config: _UserConfig?) -> TypeOverrides { + if let typeOverrides = config?.typeOverrides { return TypeOverrides(schemas: typeOverrides.schemas ?? [:]) } + return 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 8c616839c..5ef0f6133 100644 --- a/Sources/swift-openapi-generator/UserConfig.swift +++ b/Sources/swift-openapi-generator/UserConfig.swift @@ -42,7 +42,7 @@ struct _UserConfig: Codable { var nameOverrides: [String: String]? /// A dictionary of overrides for replacing the types of generated with manually provided types - var typeOverrides: [String: String]? + var typeOverrides: TypeOverrides? /// A set of features to explicitly enable. var featureFlags: FeatureFlags? @@ -60,4 +60,10 @@ struct _UserConfig: Codable { case typeOverrides case featureFlags } + + struct TypeOverrides: Codable { + + /// A dictionary of overrides for replacing the types generated from schemas with manually provided types + var schemas: [String: String]? + } } diff --git a/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift b/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift index bfe8a2485..a156adbc8 100644 --- a/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift +++ b/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift @@ -30,7 +30,7 @@ class Test_Core: XCTestCase { diagnostics: any DiagnosticCollector = PrintingDiagnosticCollector(), namingStrategy: NamingStrategy = .defensive, nameOverrides: [String: String] = [:], - typeOverrides: [String: String] = [:], + schemaOverrides: [String: String] = [:], featureFlags: FeatureFlags = [] ) -> TypesFileTranslator { makeTypesTranslator( @@ -38,7 +38,7 @@ class Test_Core: XCTestCase { diagnostics: diagnostics, namingStrategy: namingStrategy, nameOverrides: nameOverrides, - typeOverrides: typeOverrides, + schemaOverrides: schemaOverrides, featureFlags: featureFlags ) } @@ -48,14 +48,14 @@ class Test_Core: XCTestCase { diagnostics: any DiagnosticCollector = PrintingDiagnosticCollector(), namingStrategy: NamingStrategy = .defensive, nameOverrides: [String: String] = [:], - typeOverrides: [String: String] = [:], + schemaOverrides: [String: String] = [:], featureFlags: FeatureFlags = [] ) -> TypesFileTranslator { TypesFileTranslator( config: makeConfig( namingStrategy: namingStrategy, nameOverrides: nameOverrides, - typeOverrides: typeOverrides, + schemaOverrides: schemaOverrides, featureFlags: featureFlags ), diagnostics: diagnostics, @@ -66,7 +66,7 @@ class Test_Core: XCTestCase { func makeConfig( namingStrategy: NamingStrategy = .defensive, nameOverrides: [String: String] = [:], - typeOverrides: [String: String] = [:], + schemaOverrides: [String: String] = [:], featureFlags: FeatureFlags = [] ) -> Config { .init( @@ -74,7 +74,7 @@ class Test_Core: XCTestCase { access: Config.defaultAccessModifier, namingStrategy: namingStrategy, nameOverrides: nameOverrides, - typeOverrides: typeOverrides, + typeOverrides: TypeOverrides(schemas: schemaOverrides), featureFlags: featureFlags ) } diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift index ab526db95..669d40a86 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift @@ -33,7 +33,7 @@ class Test_typeOverrides: Test_Core { ) let translator = makeTranslator( components: components, - typeOverrides: ["#/components/schemas/UUID": "Foundation.UUID"] + schemaOverrides: ["#/components/schemas/UUID": "Foundation.UUID"] ) let translated = try translator.translateSchemas(components.schemas, multipartSchemaNames: []) .strippingTopComment @@ -68,7 +68,7 @@ class Test_typeOverrides: Test_Core { let translator = makeTranslator( components: components, nameOverrides: ["UUID": "MyUUID"], - typeOverrides: ["UUID": "Foundation.UUID"] + schemaOverrides: ["UUID": "Foundation.UUID"] ) let translated = try translator.translateSchemas(components.schemas, multipartSchemaNames: []) .strippingTopComment From 5753bc1da65be1071417b4bb1c01508e8b2f8c80 Mon Sep 17 00:00:00 2001 From: simonbility <31699786+simonbility@users.noreply.github.com> Date: Mon, 26 May 2025 21:49:13 +0200 Subject: [PATCH 15/35] Apply suggestions from code review Co-authored-by: Honza Dvorsky --- Examples/replace-types-example/Package.swift | 2 +- .../Translator/CommonTranslations/translateSchema.swift | 4 +--- .../Translator/CommonTypes/Constants.swift | 1 - Sources/_OpenAPIGeneratorCore/TypeOverrides.swift | 2 +- .../Documentation.docc/Proposals/SOAR-0014.md | 6 +++--- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Examples/replace-types-example/Package.swift b/Examples/replace-types-example/Package.swift index 4f0f4d411..00104ff3f 100644 --- a/Examples/replace-types-example/Package.swift +++ b/Examples/replace-types-example/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:5.10 //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift index 891bb930b..b74ecc67b 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift @@ -92,9 +92,7 @@ extension TypesFileTranslator { let typealiasDecl = try translateTypealias( named: typeName, userDescription: overrides.userDescription ?? schema.description, - to: typeOverride.asUsage.withOptional( - overrides.isOptional ?? typeMatcher.isOptional(schema, components: components) - ) + to: typeOverride.asUsage ) return [typealiasDecl] } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index 0f036db6d..f2affe3a8 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -372,7 +372,6 @@ enum Constants { /// The substring used in method names for the multipart coding strategy. static let multipart: String = "Multipart" } - /// Constants related to types used in many components. enum Global { diff --git a/Sources/_OpenAPIGeneratorCore/TypeOverrides.swift b/Sources/_OpenAPIGeneratorCore/TypeOverrides.swift index 89aace6ba..2eff387fb 100644 --- a/Sources/_OpenAPIGeneratorCore/TypeOverrides.swift +++ b/Sources/_OpenAPIGeneratorCore/TypeOverrides.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftOpenAPIGenerator open source project // -// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Copyright (c) 2025 Apple Inc. and the SwiftOpenAPIGenerator project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information 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 0c4d52064..6a26123a2 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md @@ -68,7 +68,7 @@ Will affect the generated code in the following way: ### Detailed design -In the configuration file a new `typeOverrides` option i supported. +In the configuration file a new `typeOverrides` option is supported. It contains mapping from the original name (as defined in the OpenAPI document) to a override type name to use instead of the generated name. The mapping is evaluated relative to `#/components/schemas` @@ -91,13 +91,13 @@ It must conform to `Codable`, `Hashable` and `Sendable` 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. +This is interpreted as a "strong enough" signal of the user to opt into this behaviour, to justify NOT introducing a feature-flag or considering this a breaking change. ### Future directions The implementation could potentially be extended to support inline defined properties as well. -This could be done by supporting "Paths" insteand of names in the mapping. +This could be done by supporting "Paths" instead of names in the mapping. For example with the following schema. ```yaml From 33da4eeeaa3d497c1437b5207dcb650b64e9e562 Mon Sep 17 00:00:00 2001 From: simonbility Date: Mon, 26 May 2025 21:55:07 +0200 Subject: [PATCH 16/35] fix failing test --- .../Translator/TypesTranslator/Test_typeOverrides.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift index 669d40a86..35f470f12 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift @@ -33,7 +33,7 @@ class Test_typeOverrides: Test_Core { ) let translator = makeTranslator( components: components, - schemaOverrides: ["#/components/schemas/UUID": "Foundation.UUID"] + schemaOverrides: ["UUID": "Foundation.UUID"] ) let translated = try translator.translateSchemas(components.schemas, multipartSchemaNames: []) .strippingTopComment From b4acd4caab9cddc5faaf074e59df3ca75e6acc66 Mon Sep 17 00:00:00 2001 From: simonbility Date: Tue, 27 May 2025 05:50:04 +0200 Subject: [PATCH 17/35] add doc comment --- Sources/_OpenAPIGeneratorCore/TypeOverrides.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/_OpenAPIGeneratorCore/TypeOverrides.swift b/Sources/_OpenAPIGeneratorCore/TypeOverrides.swift index 2eff387fb..1f47c0f40 100644 --- a/Sources/_OpenAPIGeneratorCore/TypeOverrides.swift +++ b/Sources/_OpenAPIGeneratorCore/TypeOverrides.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +/// Type containing overrides for schema types. public struct TypeOverrides: Sendable { /// A dictionary of overrides for replacing the types generated from schemas with manually provided types public var schemas: [String: String] From 46b20fd80ddaa6321aa8c79c3550c842dc423405 Mon Sep 17 00:00:00 2001 From: simonbility Date: Tue, 27 May 2025 05:54:16 +0200 Subject: [PATCH 18/35] Update example code --- Examples/replace-types-example/README.md | 28 +++--------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/Examples/replace-types-example/README.md b/Examples/replace-types-example/README.md index 974e6d52f..cacccfe7e 100644 --- a/Examples/replace-types-example/README.md +++ b/Examples/replace-types-example/README.md @@ -6,35 +6,13 @@ An example project using [Swift OpenAPI Generator](https://github.com/apple/swif ## 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. +This example shows how to use the TypeOverrides feature of the Swift OpenAPI Generator. ## Usage -Build and run the server using: +Build: ```console -% swift run hello-world-server +% swift build 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. From 11fa2c2ccb554ebd8df689db8776bdaad0af1c0c Mon Sep 17 00:00:00 2001 From: simonbility Date: Tue, 27 May 2025 06:02:34 +0200 Subject: [PATCH 19/35] Remove Proposal added in separate PR --- .../Documentation.docc/Proposals/Proposals.md | 1 - .../Documentation.docc/Proposals/SOAR-0014.md | 133 ------------------ 2 files changed, 134 deletions(-) delete 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 c8a91e34d..bdaff6a59 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md @@ -55,4 +55,3 @@ 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 deleted file mode 100644 index 6a26123a2..000000000 --- a/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md +++ /dev/null @@ -1,133 +0,0 @@ -# SOAR-0014: Support Type Overrides - -Allow using user-defined types instead of generated ones - -## Overview - -- Proposal: SOAR-0014 -- Author(s): [simonbility](https://github.com/simonbility) -- Status: **Awaiting Review** -- Issue: [apple/swift-openapi-generator#375](https://github.com/apple/swift-openapi-generator/issues/375) -- Implementation: - - [apple/swift-openapi-generator#764](https://github.com/apple/swift-openapi-generator/pull/764) -- Affected components: - - generator - -### Introduction - -The goal of this proposal is to allow users to specify custom types for generated. This will enable users to use their own types instead of the default generated ones, allowing for greater flexibility. - -### Motivation - -This proposal would enable more flexibility in the generated code. -Some usecases include: -- Using custom types that are already defined in the user's codebase or even coming from a third party library, instead of generating new ones. -- workaround missing support for `format` for strings -- Implement custom validation/encoding/decoding logic that cannot be expressed using the OpenAPI spec - -This is intended as a "escape hatch" for use-cases that (currently) cannot be expressed. -Using this comes with the risk of user-provided types not being compliant with the original OpenAPI spec. - - -### Proposed solution - -The proposed solution is to allow specifying typeOverrides using a new configuration option named `typeOverrides`. -This is only supported for schemas defined in the `components.schemas` section of a OpenAPI document. - -### Example -A current limitiation is string formats are not directly supported by the generator. (for example, `uuid` is not supported) - -With this proposal this can be worked around with with the following approach (This proposal does not preclude extending support for formats in the future): - -Given schemas defined in the OpenAPI document like this: -```yaml - components: - schemas: - UUID: - type: string - format: uuid -``` - -Adding typeOverrides like this in the configuration - -```diff -+ typeOverrides: -+ schemas: -+ UUID: Foundation.UUID -``` - -Will affect the generated code in the following way: -```diff - /// Types generated from the `#/components/schemas` section of the OpenAPI document. - package enum Schemas { - /// - Remark: Generated from `#/components/schemas/UUID`. -- package typealias Uuid = Swift.String -+ package typealias Uuid = Foundation.UUID - } -``` - -### Detailed design - -In the configuration file a new `typeOverrides` option is supported. -It contains mapping from the original name (as defined in the OpenAPI document) to a override type name to use instead of the generated name. - -The mapping is evaluated relative to `#/components/schemas` - -So defining overrides like this: - -```diff -typeOverrides: - schemas: - OriginalName: NewName -``` - -will replace the generated type for `#/components/schemas/OriginalName` with `NewName`. - -Its in the users responsibility to ensure that the type is valid and available. -It must conform to `Codable`, `Hashable` and `Sendable` - - -### API stability - -While this proposal does affect the generated code, it requires the user to explicitly opt-in to using the `typeOverrides` configuration option. - -This is interpreted as a "strong enough" signal of the user to opt into this behaviour, to justify NOT introducing a feature-flag or considering this a breaking change. - - -### Future directions - -The implementation could potentially be extended to support inline defined properties as well. -This could be done by supporting "Paths" instead 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: - schemas: - 'User/id': Foundation.UUID -``` - - -### Alternatives considered -An alternative to the mapping defined in the configuration file is to use a vendor extension (for instance `x-swift-open-api-override-type`) in the OpenAPI document itself. - -```yaml -... -components: - schemas: - UUID: - type: string - x-swift-open-api-override-type: Foundation.UUID -``` - -The current proposal using the configuration file was preferred because it does not rely on modifying the OpenAPI document itself, which is not always possible/straightforward when its provided by a third-party. From 557e01a908664eb237f395b7b397c023d9c502fb Mon Sep 17 00:00:00 2001 From: simonbility Date: Tue, 10 Jun 2025 08:27:47 +0200 Subject: [PATCH 20/35] Cleanup example project --- .../Sources/ExternalLibrary/ExternalObject.swift | 4 ---- .../.gitignore | 0 .../Package.swift | 2 +- .../README.md | 2 +- .../Sources/ExternalLibrary/PrimeNumber.swift | 3 +++ .../Sources/Types/Generated/Types.swift | 10 +++++++++- .../Sources/Types/openapi-generator-config.yaml | 1 + .../Sources/Types/openapi.yaml | 0 .../Sources/openapi.yaml | 8 ++++++++ .../CommonTranslations/translateSchema.swift | 1 + 10 files changed, 24 insertions(+), 7 deletions(-) delete mode 100644 Examples/replace-types-example/Sources/ExternalLibrary/ExternalObject.swift rename Examples/{replace-types-example => type-overrides-example}/.gitignore (100%) rename Examples/{replace-types-example => type-overrides-example}/Package.swift (96%) rename Examples/{replace-types-example => type-overrides-example}/README.md (95%) rename Examples/{replace-types-example => type-overrides-example}/Sources/ExternalLibrary/PrimeNumber.swift (83%) rename Examples/{replace-types-example => type-overrides-example}/Sources/Types/Generated/Types.swift (93%) rename Examples/{replace-types-example => type-overrides-example}/Sources/Types/openapi-generator-config.yaml (79%) rename Examples/{replace-types-example => type-overrides-example}/Sources/Types/openapi.yaml (100%) rename Examples/{replace-types-example => type-overrides-example}/Sources/openapi.yaml (76%) diff --git a/Examples/replace-types-example/Sources/ExternalLibrary/ExternalObject.swift b/Examples/replace-types-example/Sources/ExternalLibrary/ExternalObject.swift deleted file mode 100644 index 5082fd55f..000000000 --- a/Examples/replace-types-example/Sources/ExternalLibrary/ExternalObject.swift +++ /dev/null @@ -1,4 +0,0 @@ -public struct ExternalObject: Codable, Hashable, Sendable { - public let foo: String - public let bar: String -} diff --git a/Examples/replace-types-example/.gitignore b/Examples/type-overrides-example/.gitignore similarity index 100% rename from Examples/replace-types-example/.gitignore rename to Examples/type-overrides-example/.gitignore diff --git a/Examples/replace-types-example/Package.swift b/Examples/type-overrides-example/Package.swift similarity index 96% rename from Examples/replace-types-example/Package.swift rename to Examples/type-overrides-example/Package.swift index 00104ff3f..201ed422a 100644 --- a/Examples/replace-types-example/Package.swift +++ b/Examples/type-overrides-example/Package.swift @@ -15,7 +15,7 @@ import PackageDescription let package = Package( - name: "replace-types-example", + name: "type-overrides-example", platforms: [.macOS(.v14)], products: [ .library(name: "Types", targets: ["Types"]), diff --git a/Examples/replace-types-example/README.md b/Examples/type-overrides-example/README.md similarity index 95% rename from Examples/replace-types-example/README.md rename to Examples/type-overrides-example/README.md index cacccfe7e..60d483d62 100644 --- a/Examples/replace-types-example/README.md +++ b/Examples/type-overrides-example/README.md @@ -1,4 +1,4 @@ -# Replacing types +# Overriding types An example project using [Swift OpenAPI Generator](https://github.com/apple/swift-openapi-generator). diff --git a/Examples/replace-types-example/Sources/ExternalLibrary/PrimeNumber.swift b/Examples/type-overrides-example/Sources/ExternalLibrary/PrimeNumber.swift similarity index 83% rename from Examples/replace-types-example/Sources/ExternalLibrary/PrimeNumber.swift rename to Examples/type-overrides-example/Sources/ExternalLibrary/PrimeNumber.swift index 3d7dddd41..e1d302e86 100644 --- a/Examples/replace-types-example/Sources/ExternalLibrary/PrimeNumber.swift +++ b/Examples/type-overrides-example/Sources/ExternalLibrary/PrimeNumber.swift @@ -1,3 +1,6 @@ + +/// Example struct to be used instead of the default generated type. +/// This illustrates how to introduce a type performing additional validation during Decoding that cannot be expressed with OpenAPI public struct PrimeNumber: Codable, Hashable, RawRepresentable, Sendable { public let rawValue: Int public init?(rawValue: Int) { diff --git a/Examples/replace-types-example/Sources/Types/Generated/Types.swift b/Examples/type-overrides-example/Sources/Types/Generated/Types.swift similarity index 93% rename from Examples/replace-types-example/Sources/Types/Generated/Types.swift rename to Examples/type-overrides-example/Sources/Types/Generated/Types.swift index 80084c740..d253a4136 100644 --- a/Examples/replace-types-example/Sources/Types/Generated/Types.swift +++ b/Examples/type-overrides-example/Sources/Types/Generated/Types.swift @@ -61,27 +61,35 @@ package enum Components { package enum Schemas { /// - Remark: Generated from `#/components/schemas/UUID`. package typealias Uuid = Foundation.UUID + /// - Remark: Generated from `#/components/schemas/PrimeNumber`. + package typealias PrimeNumber = 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/name`. package var name: Swift.String? + /// - Remark: Generated from `#/components/schemas/User/favorite_prime_number`. + package var favoritePrimeNumber: Components.Schemas.PrimeNumber? /// Creates a new `User`. /// /// - Parameters: /// - id: /// - name: + /// - favoritePrimeNumber: package init( id: Components.Schemas.Uuid? = nil, - name: Swift.String? = nil + name: Swift.String? = nil, + favoritePrimeNumber: Components.Schemas.PrimeNumber? = nil ) { self.id = id self.name = name + self.favoritePrimeNumber = favoritePrimeNumber } package enum CodingKeys: String, CodingKey { case id case name + case favoritePrimeNumber = "favorite_prime_number" } } } diff --git a/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml b/Examples/type-overrides-example/Sources/Types/openapi-generator-config.yaml similarity index 79% rename from Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml rename to Examples/type-overrides-example/Sources/Types/openapi-generator-config.yaml index 178a58f37..404cdf9e6 100644 --- a/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml +++ b/Examples/type-overrides-example/Sources/Types/openapi-generator-config.yaml @@ -8,3 +8,4 @@ additionalImports: typeOverrides: schemas: UUID: Foundation.UUID + PrimeNumber: ExternalLibrary.PrimeNumber diff --git a/Examples/replace-types-example/Sources/Types/openapi.yaml b/Examples/type-overrides-example/Sources/Types/openapi.yaml similarity index 100% rename from Examples/replace-types-example/Sources/Types/openapi.yaml rename to Examples/type-overrides-example/Sources/Types/openapi.yaml diff --git a/Examples/replace-types-example/Sources/openapi.yaml b/Examples/type-overrides-example/Sources/openapi.yaml similarity index 76% rename from Examples/replace-types-example/Sources/openapi.yaml rename to Examples/type-overrides-example/Sources/openapi.yaml index eabfb5cfa..e84097844 100644 --- a/Examples/replace-types-example/Sources/openapi.yaml +++ b/Examples/type-overrides-example/Sources/openapi.yaml @@ -28,6 +28,10 @@ components: UUID: # this will be replaced by with Foundation.UUID specified by typeOverrides in open-api-generator-config type: string format: uuid + + PrimeNumber: # this will be replaced by with ExternalLibrary.PrimeNumber specified by typeOverrides in open-api-generator-config + type: string + format: uuid User: type: object @@ -36,3 +40,7 @@ components: $ref: '#/components/schemas/UUID' name: type: string + favorite_prime_number: + $ref: '#/components/schemas/PrimeNumber' + + diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift index b74ecc67b..d8c0f1916 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// import OpenAPIKit +import Foundation extension TypesFileTranslator { From 7956564205e6bdcbfcc0915f6eb1e4b8d29cf301 Mon Sep 17 00:00:00 2001 From: simonbility Date: Tue, 10 Jun 2025 08:28:04 +0200 Subject: [PATCH 21/35] emit warning when overriding non-existing schema --- .../TypesTranslator/translateSchemas.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateSchemas.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateSchemas.swift index 5935ad805..f21083263 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateSchemas.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateSchemas.swift @@ -57,6 +57,7 @@ extension TypesFileTranslator { _ schemas: OpenAPI.ComponentDictionary, multipartSchemaNames: Set ) throws -> Declaration { + try diagnoseTypeOverrideForNonExistentSchema() let decls: [Declaration] = try schemas.flatMap { key, value in try translateSchema( @@ -76,4 +77,18 @@ extension TypesFileTranslator { ) return componentsSchemasEnum } + private func diagnoseTypeOverrideForNonExistentSchema() throws { + let nonExistentOverrides = config.typeOverrides.schemas.keys + .filter { key in + guard let componentKey = OpenAPI.ComponentKey(rawValue: key) else { return false } + return !self.components.schemas.contains(key: componentKey) + } + .sorted() + + for override in nonExistentOverrides { + try diagnostics.emit( + .warning(message: "TypeOverride defined for schema '\(override)' that is not defined in the OpenAPI document") + ) + } + } } From b2ea3fe2745295b15cd0f1bf7032db6b319ce1e3 Mon Sep 17 00:00:00 2001 From: simonbility Date: Tue, 10 Jun 2025 08:32:34 +0200 Subject: [PATCH 22/35] Switch to plugin in example --- Examples/type-overrides-example/Package.swift | 4 +- .../Sources/Types/Generated/Types.swift | 236 ------------------ 2 files changed, 3 insertions(+), 237 deletions(-) delete mode 100644 Examples/type-overrides-example/Sources/Types/Generated/Types.swift diff --git a/Examples/type-overrides-example/Package.swift b/Examples/type-overrides-example/Package.swift index 201ed422a..7240d5908 100644 --- a/Examples/type-overrides-example/Package.swift +++ b/Examples/type-overrides-example/Package.swift @@ -21,6 +21,7 @@ let package = Package( .library(name: "Types", targets: ["Types"]), ], dependencies: [ + .package(url: "https://github.com/apple/swift-openapi-generator", from: "1.6.0"), .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.7.0"), ], targets: [ @@ -28,7 +29,8 @@ let package = Package( name: "Types", dependencies: [ "ExternalLibrary", - .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime")] + .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime")], + plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")] ), .target( name: "ExternalLibrary" diff --git a/Examples/type-overrides-example/Sources/Types/Generated/Types.swift b/Examples/type-overrides-example/Sources/Types/Generated/Types.swift deleted file mode 100644 index d253a4136..000000000 --- a/Examples/type-overrides-example/Sources/Types/Generated/Types.swift +++ /dev/null @@ -1,236 +0,0 @@ -// Generated by swift-openapi-generator, do not modify. -@_spi(Generated) import OpenAPIRuntime -#if os(Linux) -@preconcurrency import struct Foundation.URL -@preconcurrency import struct Foundation.Data -@preconcurrency import struct Foundation.Date -#else -import struct Foundation.URL -import struct Foundation.Data -import struct Foundation.Date -#endif -import Foundation -import ExternalLibrary -/// A type that performs HTTP operations defined by the OpenAPI document. -package protocol APIProtocol: Sendable { - /// - Remark: HTTP `GET /user`. - /// - Remark: Generated from `#/paths//user/get(getUser)`. - func getUser(_ input: Operations.GetUser.Input) async throws -> Operations.GetUser.Output -} - -/// Convenience overloads for operation inputs. -extension APIProtocol { - /// - Remark: HTTP `GET /user`. - /// - Remark: Generated from `#/paths//user/get(getUser)`. - package func getUser( - query: Operations.GetUser.Input.Query = .init(), - headers: Operations.GetUser.Input.Headers = .init() - ) async throws -> Operations.GetUser.Output { - try await getUser(Operations.GetUser.Input( - query: query, - headers: headers - )) - } -} - -/// Server URLs defined in the OpenAPI document. -package enum Servers { - /// Example service deployment. - package enum Server1 { - /// Example service deployment. - package static func url() throws -> Foundation.URL { - try Foundation.URL( - validatingOpenAPIServerURL: "https://example.com/api", - variables: [] - ) - } - } - /// Example service deployment. - @available(*, deprecated, renamed: "Servers.Server1.url") - package static func server1() throws -> Foundation.URL { - try Foundation.URL( - validatingOpenAPIServerURL: "https://example.com/api", - variables: [] - ) - } -} - -/// Types generated from the components section of the OpenAPI document. -package enum Components { - /// Types generated from the `#/components/schemas` section of the OpenAPI document. - package enum Schemas { - /// - Remark: Generated from `#/components/schemas/UUID`. - package typealias Uuid = Foundation.UUID - /// - Remark: Generated from `#/components/schemas/PrimeNumber`. - package typealias PrimeNumber = 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/name`. - package var name: Swift.String? - /// - Remark: Generated from `#/components/schemas/User/favorite_prime_number`. - package var favoritePrimeNumber: Components.Schemas.PrimeNumber? - /// Creates a new `User`. - /// - /// - Parameters: - /// - id: - /// - name: - /// - favoritePrimeNumber: - package init( - id: Components.Schemas.Uuid? = nil, - name: Swift.String? = nil, - favoritePrimeNumber: Components.Schemas.PrimeNumber? = nil - ) { - self.id = id - self.name = name - self.favoritePrimeNumber = favoritePrimeNumber - } - package enum CodingKeys: String, CodingKey { - case id - case name - case favoritePrimeNumber = "favorite_prime_number" - } - } - } - /// 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 - ] - } - } - } -} From 16f4cf0776782dc579a17707fbf2c03f58b52d72 Mon Sep 17 00:00:00 2001 From: simonbility <31699786+simonbility@users.noreply.github.com> Date: Mon, 16 Jun 2025 07:08:21 +0200 Subject: [PATCH 23/35] Apply suggestions from code review Co-authored-by: Honza Dvorsky --- Examples/type-overrides-example/Package.swift | 4 ++-- Examples/type-overrides-example/README.md | 2 +- .../Sources/ExternalLibrary/PrimeNumber.swift | 2 +- .../Sources/openapi.yaml | 20 ------------------- Sources/_OpenAPIGeneratorCore/Config.swift | 6 +++--- .../CommonTranslations/translateSchema.swift | 2 ++ .../TypesTranslator/translateSchemas.swift | 2 +- .../_OpenAPIGeneratorCore/TypeOverrides.swift | 11 ++++------ .../GenerateOptions.swift | 9 ++++++--- .../swift-openapi-generator/UserConfig.swift | 5 +++-- 10 files changed, 23 insertions(+), 40 deletions(-) diff --git a/Examples/type-overrides-example/Package.swift b/Examples/type-overrides-example/Package.swift index 7240d5908..e4b456a8a 100644 --- a/Examples/type-overrides-example/Package.swift +++ b/Examples/type-overrides-example/Package.swift @@ -3,7 +3,7 @@ // // This source file is part of the SwiftOpenAPIGenerator open source project // -// Copyright (c) 2024 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Copyright (c) 2025 Apple Inc. and the SwiftOpenAPIGenerator project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -16,7 +16,7 @@ import PackageDescription let package = Package( name: "type-overrides-example", - platforms: [.macOS(.v14)], + platforms: [.macOS(.v10_15)], products: [ .library(name: "Types", targets: ["Types"]), ], diff --git a/Examples/type-overrides-example/README.md b/Examples/type-overrides-example/README.md index 60d483d62..104654992 100644 --- a/Examples/type-overrides-example/README.md +++ b/Examples/type-overrides-example/README.md @@ -6,7 +6,7 @@ An example project using [Swift OpenAPI Generator](https://github.com/apple/swif ## Overview -This example shows how to use the TypeOverrides feature of the Swift OpenAPI Generator. +This example shows how to use [type overrides](https://swiftpackageindex.com/apple/swift-openapi-generator/documentation/swift-openapi-generator/configuring-the-generator) with Swift OpenAPI Generator. ## Usage diff --git a/Examples/type-overrides-example/Sources/ExternalLibrary/PrimeNumber.swift b/Examples/type-overrides-example/Sources/ExternalLibrary/PrimeNumber.swift index e1d302e86..3cc11d132 100644 --- a/Examples/type-overrides-example/Sources/ExternalLibrary/PrimeNumber.swift +++ b/Examples/type-overrides-example/Sources/ExternalLibrary/PrimeNumber.swift @@ -16,11 +16,11 @@ public struct PrimeNumber: Codable, Hashable, RawRepresentable, Sendable { } self = value } + public func encode(to encoder: any Encoder) throws { var container = encoder.singleValueContainer() try container.encode(self.rawValue) } - } extension Int { diff --git a/Examples/type-overrides-example/Sources/openapi.yaml b/Examples/type-overrides-example/Sources/openapi.yaml index e84097844..d3413d1dd 100644 --- a/Examples/type-overrides-example/Sources/openapi.yaml +++ b/Examples/type-overrides-example/Sources/openapi.yaml @@ -5,24 +5,6 @@ info: servers: - url: https://example.com/api description: Example service deployment. -paths: - /user: - get: - operationId: getUser - parameters: - - name: name - required: false - in: query - description: The name of the user - schema: - type: string - responses: - '200': - description: A success response with the user. - content: - application/json: - schema: - $ref: '#/components/schemas/User' components: schemas: UUID: # this will be replaced by with Foundation.UUID specified by typeOverrides in open-api-generator-config @@ -42,5 +24,3 @@ components: type: string favorite_prime_number: $ref: '#/components/schemas/PrimeNumber' - - diff --git a/Sources/_OpenAPIGeneratorCore/Config.swift b/Sources/_OpenAPIGeneratorCore/Config.swift index 58a478826..aa0dda4d0 100644 --- a/Sources/_OpenAPIGeneratorCore/Config.swift +++ b/Sources/_OpenAPIGeneratorCore/Config.swift @@ -60,7 +60,7 @@ 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 + /// A map of OpenAPI schema names to desired custom type names. public var typeOverrides: TypeOverrides /// Additional pre-release features to enable. @@ -76,7 +76,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 + /// - typeOverrides: A map of OpenAPI schema names to desired custom type names. /// - featureFlags: Additional pre-release features to enable. public init( mode: GeneratorMode, @@ -85,7 +85,7 @@ public struct Config: Sendable { filter: DocumentFilter? = nil, namingStrategy: NamingStrategy, nameOverrides: [String: String] = [:], - typeOverrides: TypeOverrides = TypeOverrides(), + typeOverrides: TypeOverrides = .init(), featureFlags: FeatureFlags = [] ) { self.mode = mode diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift index d8c0f1916..fb8365693 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift @@ -88,6 +88,8 @@ extension TypesFileTranslator { ) ) } + + // Apply type overrides. if let jsonPath = typeName.shortJSONName, let typeOverride = config.typeOverrides.schemas[jsonPath] { let typeOverride = TypeName(swiftKeyPath: typeOverride.components(separatedBy: ".")) let typealiasDecl = try translateTypealias( diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateSchemas.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateSchemas.swift index f21083263..693a94fce 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateSchemas.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateSchemas.swift @@ -87,7 +87,7 @@ extension TypesFileTranslator { for override in nonExistentOverrides { try diagnostics.emit( - .warning(message: "TypeOverride defined for schema '\(override)' that is not defined in the OpenAPI document") + .warning(message: "A type override defined for schema '\(override)' is not defined in the OpenAPI document.") ) } } diff --git a/Sources/_OpenAPIGeneratorCore/TypeOverrides.swift b/Sources/_OpenAPIGeneratorCore/TypeOverrides.swift index 1f47c0f40..32f9b0a7a 100644 --- a/Sources/_OpenAPIGeneratorCore/TypeOverrides.swift +++ b/Sources/_OpenAPIGeneratorCore/TypeOverrides.swift @@ -12,15 +12,12 @@ // //===----------------------------------------------------------------------===// -/// Type containing overrides for schema types. +/// A container of schema type overrides. public struct TypeOverrides: Sendable { - /// A dictionary of overrides for replacing the types generated from schemas with manually provided types + /// A dictionary of overrides for replacing named schemas from the OpenAPI document with custom types. public var schemas: [String: String] - /// Creates a new instance of `TypeOverrides` - /// - Parameter schemas: A dictionary mapping schema names to their override types. + /// Creates a new instance. + /// - Parameter schemas: A dictionary of overrides for replacing named schemas from the OpenAPI document with custom types. public init(schemas: [String: String] = [:]) { self.schemas = schemas } - - /// A Boolean value indicating whether there are no overrides. - public var isEmpty: Bool { schemas.isEmpty } } diff --git a/Sources/swift-openapi-generator/GenerateOptions.swift b/Sources/swift-openapi-generator/GenerateOptions.swift index a419ea1a6..0999943aa 100644 --- a/Sources/swift-openapi-generator/GenerateOptions.swift +++ b/Sources/swift-openapi-generator/GenerateOptions.swift @@ -93,12 +93,15 @@ 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 + /// - Returns: The type overrides requested by the user. func resolvedTypeOverrides(_ config: _UserConfig?) -> TypeOverrides { - if let typeOverrides = config?.typeOverrides { return TypeOverrides(schemas: typeOverrides.schemas ?? [:]) } - return TypeOverrides() + guard let schemaOverrides = config?.typeOverrides?.schemas, !schemaOverrides.isEmpty else { + return .init() + } + return TypeOverrides(schemas: schemaOverrides) } /// Returns a list of the feature flags requested by the user. diff --git a/Sources/swift-openapi-generator/UserConfig.swift b/Sources/swift-openapi-generator/UserConfig.swift index 5ef0f6133..b158a3951 100644 --- a/Sources/swift-openapi-generator/UserConfig.swift +++ b/Sources/swift-openapi-generator/UserConfig.swift @@ -60,10 +60,11 @@ struct _UserConfig: Codable { case typeOverrides case featureFlags } - + + /// A container of type overrides. struct TypeOverrides: Codable { - /// A dictionary of overrides for replacing the types generated from schemas with manually provided types + /// A dictionary of overrides for replacing the types generated from schemas with manually provided types. var schemas: [String: String]? } } From 3df13a74b32a564a23f01045ee1c8c534f60be13 Mon Sep 17 00:00:00 2001 From: simonbility Date: Mon, 16 Jun 2025 07:21:24 +0200 Subject: [PATCH 24/35] move validation logic --- .../Parser/validateDoc.swift | 28 +++++++++++++++++-- .../TypesTranslator/translateSchemas.swift | 16 ----------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift b/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift index 416704ad0..9007706e9 100644 --- a/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift +++ b/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift @@ -97,8 +97,7 @@ func validateContentTypes(in doc: ParsedOpenAPIRepresentation, validate: (String /// Validates all references from an OpenAPI document represented by a ParsedOpenAPIRepresentation against its components. /// -/// This method traverses the OpenAPI document to ensure that all references -/// within the document are valid and point to existing components. +/// This method iterates through all Type overrides in a config and checks if a schem /// /// - Parameter doc: The OpenAPI document to validate. /// - Throws: `Diagnostic.error` if an external reference is found or a reference is not found in components. @@ -252,6 +251,28 @@ func validateReferences(in doc: ParsedOpenAPIRepresentation) throws { } } +/// Validates all type overrides from a Config are present in the components of a ParsedOpenAPIRepresentation. +/// +/// This method iterates through the type overrides defined in the config and checks that for each of them a named schema is defined in the OpenAPI document. +/// +/// - Parameters: +/// - doc: The OpenAPI document to validate. +/// - config: The generator config. +/// - Returns: An array of diagnostic messages representing type overrides for nonexistent schemas. +func validateTypeOverrides(_ doc: ParsedOpenAPIRepresentation, config: Config) -> [Diagnostic] { + let nonExistentOverrides = config.typeOverrides.schemas.keys + .filter { key in + guard let componentKey = OpenAPI.ComponentKey(rawValue: key) else { return false } + return !doc.components.schemas.contains(key: componentKey) + } + .sorted() + return nonExistentOverrides.map { override in + Diagnostic.warning( + message: "A type override defined for schema '\(override)' is not defined in the OpenAPI document." + ) + } +} + /// Runs validation steps on the incoming OpenAPI document. /// - Parameters: /// - doc: The OpenAPI document to validate. @@ -263,6 +284,7 @@ func validateDoc(_ doc: ParsedOpenAPIRepresentation, config: Config) throws -> [ try validateContentTypes(in: doc) { contentType in (try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil } + let typeOverrideDiagnostics = validateTypeOverrides(doc, config: config) // Run OpenAPIKit's built-in validation. // Pass `false` to `strict`, however, because we don't @@ -283,5 +305,5 @@ func validateDoc(_ doc: ParsedOpenAPIRepresentation, config: Config) throws -> [ ] ) } - return diagnostics + return typeOverrideDiagnostics + diagnostics } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateSchemas.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateSchemas.swift index 693a94fce..679629753 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateSchemas.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateSchemas.swift @@ -57,8 +57,6 @@ extension TypesFileTranslator { _ schemas: OpenAPI.ComponentDictionary, multipartSchemaNames: Set ) throws -> Declaration { - try diagnoseTypeOverrideForNonExistentSchema() - let decls: [Declaration] = try schemas.flatMap { key, value in try translateSchema( componentKey: key, @@ -77,18 +75,4 @@ extension TypesFileTranslator { ) return componentsSchemasEnum } - private func diagnoseTypeOverrideForNonExistentSchema() throws { - let nonExistentOverrides = config.typeOverrides.schemas.keys - .filter { key in - guard let componentKey = OpenAPI.ComponentKey(rawValue: key) else { return false } - return !self.components.schemas.contains(key: componentKey) - } - .sorted() - - for override in nonExistentOverrides { - try diagnostics.emit( - .warning(message: "A type override defined for schema '\(override)' is not defined in the OpenAPI document.") - ) - } - } } From 34aa46a4a88c54e2933fec983b8a237fedb3c6da Mon Sep 17 00:00:00 2001 From: simonbility Date: Mon, 16 Jun 2025 07:31:40 +0200 Subject: [PATCH 25/35] simplify run description --- .../GenerateOptions+runGenerator.swift | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift b/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift index 996663da8..b852c5a04 100644 --- a/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift +++ b/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift @@ -50,13 +50,6 @@ extension _GenerateOptions { } let (diagnostics, finalizeDiagnostics) = preparedDiagnosticsCollector(outputPath: diagnosticsOutputPath) let doc = self.docPath - let typeOverridesDescription = """ - - - Schemas: \(resolvedTypeOverrides.schemas.isEmpty ? "" : resolvedTypeOverrides.schemas - .sorted(by: { $0.key < $1.key }) - .map { "\"\($0.key)\"->\"\($0.value)\"" } - .joined(separator: ", ")) - """ print( """ Swift OpenAPI Generator is running with the following configuration: @@ -68,7 +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 ? "" : typeOverridesDescription) + - Type overrides: \(resolvedTypeOverrides.schemas.isEmpty ? "" : resolvedTypeOverrides.schemas + .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) From 384f4279cfce41e8a140fbe10410edd6b1d5e113 Mon Sep 17 00:00:00 2001 From: simonbility Date: Mon, 16 Jun 2025 07:50:32 +0200 Subject: [PATCH 26/35] Migrate to SnippedBasedReferenceTests --- .../DeclarationHelpers.swift | 66 -------------- .../TypesTranslator/Test_typeOverrides.swift | 88 ------------------- .../SnippetBasedReferenceTests.swift | 70 +++++++++++++++ 3 files changed, 70 insertions(+), 154 deletions(-) delete mode 100644 Tests/OpenAPIGeneratorCoreTests/DeclarationHelpers.swift delete mode 100644 Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift diff --git a/Tests/OpenAPIGeneratorCoreTests/DeclarationHelpers.swift b/Tests/OpenAPIGeneratorCoreTests/DeclarationHelpers.swift deleted file mode 100644 index e33e6a1f4..000000000 --- a/Tests/OpenAPIGeneratorCoreTests/DeclarationHelpers.swift +++ /dev/null @@ -1,66 +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 -// -//===----------------------------------------------------------------------===// -@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/Translator/TypesTranslator/Test_typeOverrides.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift deleted file mode 100644 index 35f470f12..000000000 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeOverrides.swift +++ /dev/null @@ -1,88 +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_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, - schemaOverrides: ["UUID": "Foundation.UUID"] - ) - let translated = try translator.translateSchemas(components.schemas, multipartSchemaNames: []) - .strippingTopComment - guard let enumDecl = translated.enum else { return XCTFail("Expected enum declaration") } - let typeAliases = enumDecl.members.compactMap(\.strippingTopComment.typealias) - XCTAssertEqual( - typeAliases, - [ - TypealiasDescription( - accessModifier: .internal, - name: "UUID", - existingType: .member(["Foundation", "UUID"]) - ) - ] - ) - } - - func testTypeOverrideWithNameOverride() throws { - let components = try loadComponentsFromYAML( - #""" - schemas: - User: - type: object - properties: - id: - $ref: '#/components/schemas/UUID' - UUID: - type: string - format: uuid - """# - ) - let translator = makeTranslator( - components: components, - nameOverrides: ["UUID": "MyUUID"], - schemaOverrides: ["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"]) - ) - ] - ) - } -} diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index 81cdeb108..6338342ef 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -1915,6 +1915,68 @@ final class SnippetBasedReferenceTests: XCTestCase { """ ) } + func testTypeOverrides() throws { + try assertSchemasTranslation( + typeOverrides: TypeOverrides(schemas: ["UUID": "Foundation.UUID"]), + """ + schemas: + User: + type: object + properties: + id: + $ref: '#/components/schemas/UUID' + UUID: + type: string + format: uuid + """, + """ + public enum Schemas { + public struct User: Codable, Hashable, Sendable { + public var id: Components.Schemas.UUID? + public init(id: Components.Schemas.UUID? = nil) { + self.id = id + } + public enum CodingKeys: String, CodingKey { + case id + } + } + public typealias UUID = Foundation.UUID + } + """ + ) + } + + func testTypeOverridesWithNameOverrides() throws { + try assertSchemasTranslation( + nameOverrides: ["UUID": "MyUUID"], + typeOverrides: TypeOverrides(schemas: ["UUID": "Foundation.UUID"]), + """ + schemas: + User: + type: object + properties: + id: + $ref: '#/components/schemas/UUID' + UUID: + type: string + format: uuid + """, + """ + public enum Schemas { + public struct User: Codable, Hashable, Sendable { + public var id: Components.Schemas.MyUUID? + public init(id: Components.Schemas.MyUUID? = nil) { + self.id = id + } + public enum CodingKeys: String, CodingKey { + case id + } + } + public typealias MyUUID = Foundation.UUID + } + """ + ) + } func testComponentsResponsesResponseNoBody() throws { try self.assertResponsesTranslation( @@ -5877,6 +5939,8 @@ extension SnippetBasedReferenceTests { func makeTypesTranslator( accessModifier: AccessModifier = .public, namingStrategy: NamingStrategy = .defensive, + nameOverrides: [String: String] = [:], + typeOverrides: TypeOverrides = .init(), featureFlags: FeatureFlags = [], ignoredDiagnosticMessages: Set = [], componentsYAML: String @@ -5887,6 +5951,8 @@ extension SnippetBasedReferenceTests { mode: .types, access: accessModifier, namingStrategy: namingStrategy, + nameOverrides: nameOverrides, + typeOverrides: typeOverrides, featureFlags: featureFlags ), diagnostics: XCTestDiagnosticCollector(test: self, ignoredDiagnosticMessages: ignoredDiagnosticMessages), @@ -6080,6 +6146,8 @@ extension SnippetBasedReferenceTests { func assertSchemasTranslation( featureFlags: FeatureFlags = [], + nameOverrides: [String: String] = [:], + typeOverrides: TypeOverrides = .init(), ignoredDiagnosticMessages: Set = [], _ componentsYAML: String, _ expectedSwift: String, @@ -6089,6 +6157,8 @@ extension SnippetBasedReferenceTests { ) throws { let translator = try makeTypesTranslator( accessModifier: accessModifier, + nameOverrides: nameOverrides, + typeOverrides: typeOverrides, featureFlags: featureFlags, ignoredDiagnosticMessages: ignoredDiagnosticMessages, componentsYAML: componentsYAML From 31d11d0d7ebc18753d93fb24a32fb3d0db5f57df Mon Sep 17 00:00:00 2001 From: simonbility Date: Mon, 16 Jun 2025 08:03:59 +0200 Subject: [PATCH 27/35] test validation logic --- .../Parser/Test_validateDoc.swift | 28 +++++++++++++++++++ .../TestUtilities.swift | 3 -- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift index ea194aed2..12b7910e7 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift @@ -449,5 +449,33 @@ final class Test_validateDoc: Test_Core { ) } } + + func testValidateTypeOverrides() throws { + let schema = try loadSchemaFromYAML( + #""" + type: string + """# + ) + let doc = OpenAPI.Document( + info: .init(title: "Test", version: "1.0.0"), + servers: [], + paths: [:], + components: .init(schemas: ["MyType": schema]) + ) + let diagnostics = validateTypeOverrides( + doc, + config: .init( + mode: .types, + access: Config.defaultAccessModifier, + namingStrategy: Config.defaultNamingStrategy, + typeOverrides: TypeOverrides(schemas: ["NonExistent": "NonExistent"]) + ) + ) + XCTAssertEqual(diagnostics.count, 1) + XCTAssertEqual( + diagnostics.first?.message, + "A type override defined for schema 'NonExistent' is not defined in the OpenAPI document." + ) + } } diff --git a/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift b/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift index a156adbc8..89b9e47ba 100644 --- a/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift +++ b/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift @@ -82,9 +82,6 @@ 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"]) } From c57b64553167b73226a91c4444c31da4194e4b5d Mon Sep 17 00:00:00 2001 From: simonbility Date: Mon, 16 Jun 2025 08:07:15 +0200 Subject: [PATCH 28/35] undo whitespace change --- .../_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index f2affe3a8..0f036db6d 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -372,6 +372,7 @@ enum Constants { /// The substring used in method names for the multipart coding strategy. static let multipart: String = "Multipart" } + /// Constants related to types used in many components. enum Global { From 60595b9dc000226bd40846572b63d05852f4cf6c Mon Sep 17 00:00:00 2001 From: simonbility Date: Mon, 16 Jun 2025 08:23:20 +0200 Subject: [PATCH 29/35] Add TypeOverrides in Tutorials --- .../Articles/Configuring-the-generator.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Sources/swift-openapi-generator/Documentation.docc/Articles/Configuring-the-generator.md b/Sources/swift-openapi-generator/Documentation.docc/Articles/Configuring-the-generator.md index 256c8cb32..c08e470c6 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Articles/Configuring-the-generator.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Articles/Configuring-the-generator.md @@ -132,3 +132,15 @@ filter: tags: - myTag ``` + +### Type Overrides + +Type Overrides can be used used to replace the default generated type with a custom type. + +```yaml +typeOverrides: + schemas: + UUID: Foundation.UUID +``` + +Check out [SOAR-0014](https://swiftpackageindex.com/apple/swift-openapi-generator/documentation/swift-openapi-generator/soar-0014) for details. From e43134f9bc669d5015b2ef482ce762821065fb4e Mon Sep 17 00:00:00 2001 From: simonbility Date: Mon, 16 Jun 2025 08:27:18 +0200 Subject: [PATCH 30/35] add todo for example project --- Examples/type-overrides-example/Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/type-overrides-example/Package.swift b/Examples/type-overrides-example/Package.swift index e4b456a8a..37ae0f940 100644 --- a/Examples/type-overrides-example/Package.swift +++ b/Examples/type-overrides-example/Package.swift @@ -21,7 +21,7 @@ let package = Package( .library(name: "Types", targets: ["Types"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-openapi-generator", from: "1.6.0"), + .package(url: "https://github.com/apple/swift-openapi-generator", from: "1.9.0" /* TODO: Put the actual version where type-overrides will land here */), .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.7.0"), ], targets: [ From 73cc059a43f888975e5a787dba80336b875b69a1 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 17 Jun 2025 13:01:06 +0200 Subject: [PATCH 31/35] Update Examples/type-overrides-example/Sources/openapi.yaml --- Examples/type-overrides-example/Sources/openapi.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/type-overrides-example/Sources/openapi.yaml b/Examples/type-overrides-example/Sources/openapi.yaml index d3413d1dd..acd2b9810 100644 --- a/Examples/type-overrides-example/Sources/openapi.yaml +++ b/Examples/type-overrides-example/Sources/openapi.yaml @@ -7,11 +7,11 @@ servers: description: Example service deployment. components: schemas: - UUID: # this will be replaced by with Foundation.UUID specified by typeOverrides in open-api-generator-config + UUID: # this will be replaced by with Foundation.UUID specified by typeOverrides in openapi-generator-config type: string format: uuid - PrimeNumber: # this will be replaced by with ExternalLibrary.PrimeNumber specified by typeOverrides in open-api-generator-config + PrimeNumber: # this will be replaced by with ExternalLibrary.PrimeNumber specified by typeOverrides in openapi-generator-config type: string format: uuid From f863db1860dfbdb1b399169f2678e1b995c079e4 Mon Sep 17 00:00:00 2001 From: simonbility Date: Tue, 17 Jun 2025 13:07:24 +0200 Subject: [PATCH 32/35] cleanup --- Examples/type-overrides-example/Package.swift | 2 +- Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift | 3 ++- .../Documentation.docc/Articles/Configuring-the-generator.md | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Examples/type-overrides-example/Package.swift b/Examples/type-overrides-example/Package.swift index 37ae0f940..7b2891cbc 100644 --- a/Examples/type-overrides-example/Package.swift +++ b/Examples/type-overrides-example/Package.swift @@ -21,7 +21,7 @@ let package = Package( .library(name: "Types", targets: ["Types"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-openapi-generator", from: "1.9.0" /* TODO: Put the actual version where type-overrides will land here */), + .package(url: "https://github.com/apple/swift-openapi-generator", from: "1.9.0"), .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.7.0"), ], targets: [ diff --git a/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift b/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift index 9007706e9..8b83a9505 100644 --- a/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift +++ b/Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift @@ -97,7 +97,8 @@ func validateContentTypes(in doc: ParsedOpenAPIRepresentation, validate: (String /// Validates all references from an OpenAPI document represented by a ParsedOpenAPIRepresentation against its components. /// -/// This method iterates through all Type overrides in a config and checks if a schem +/// This method traverses the OpenAPI document to ensure that all references +/// within the document are valid and point to existing components. /// /// - Parameter doc: The OpenAPI document to validate. /// - Throws: `Diagnostic.error` if an external reference is found or a reference is not found in components. diff --git a/Sources/swift-openapi-generator/Documentation.docc/Articles/Configuring-the-generator.md b/Sources/swift-openapi-generator/Documentation.docc/Articles/Configuring-the-generator.md index 713e159b5..cbc6ba056 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Articles/Configuring-the-generator.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Articles/Configuring-the-generator.md @@ -146,7 +146,7 @@ filter: - myTag ``` -### Type Overrides +### Type overrides Type Overrides can be used used to replace the default generated type with a custom type. From 7a271e51e63354a6072c516562211cefbc82f2ae Mon Sep 17 00:00:00 2001 From: simonbility Date: Tue, 17 Jun 2025 13:28:29 +0200 Subject: [PATCH 33/35] Add License Header --- .../Sources/ExternalLibrary/PrimeNumber.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Examples/type-overrides-example/Sources/ExternalLibrary/PrimeNumber.swift b/Examples/type-overrides-example/Sources/ExternalLibrary/PrimeNumber.swift index 3cc11d132..fcfc0be55 100644 --- a/Examples/type-overrides-example/Sources/ExternalLibrary/PrimeNumber.swift +++ b/Examples/type-overrides-example/Sources/ExternalLibrary/PrimeNumber.swift @@ -1,3 +1,16 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2025 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 +// +//===----------------------------------------------------------------------===// /// Example struct to be used instead of the default generated type. /// This illustrates how to introduce a type performing additional validation during Decoding that cannot be expressed with OpenAPI From f9418f3eac7319ca0aa416b103be9c9be08e630b Mon Sep 17 00:00:00 2001 From: simonbility Date: Tue, 17 Jun 2025 13:51:15 +0200 Subject: [PATCH 34/35] Format Code --- Examples/type-overrides-example/Package.swift | 13 +++---------- Sources/_OpenAPIGeneratorCore/Config.swift | 1 - .../Translator/CommonTypes/Constants.swift | 1 - .../swift-openapi-generator/GenerateOptions.swift | 4 +--- Sources/swift-openapi-generator/UserConfig.swift | 1 - 5 files changed, 4 insertions(+), 16 deletions(-) diff --git a/Examples/type-overrides-example/Package.swift b/Examples/type-overrides-example/Package.swift index 7b2891cbc..979140d4d 100644 --- a/Examples/type-overrides-example/Package.swift +++ b/Examples/type-overrides-example/Package.swift @@ -17,9 +17,7 @@ import PackageDescription let package = Package( name: "type-overrides-example", platforms: [.macOS(.v10_15)], - products: [ - .library(name: "Types", targets: ["Types"]), - ], + products: [.library(name: "Types", targets: ["Types"])], dependencies: [ .package(url: "https://github.com/apple/swift-openapi-generator", from: "1.9.0"), .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.7.0"), @@ -27,13 +25,8 @@ let package = Package( targets: [ .target( name: "Types", - dependencies: [ - "ExternalLibrary", - .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime")], + dependencies: ["ExternalLibrary", .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime")], plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")] - ), - .target( - name: "ExternalLibrary" - ), + ), .target(name: "ExternalLibrary"), ] ) diff --git a/Sources/_OpenAPIGeneratorCore/Config.swift b/Sources/_OpenAPIGeneratorCore/Config.swift index 4867dc6a8..74bb6f13e 100644 --- a/Sources/_OpenAPIGeneratorCore/Config.swift +++ b/Sources/_OpenAPIGeneratorCore/Config.swift @@ -62,7 +62,6 @@ 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 schema names to desired custom type names. public var typeOverrides: TypeOverrides diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index 0f036db6d..f2affe3a8 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -372,7 +372,6 @@ enum Constants { /// The substring used in method names for the multipart coding strategy. static let multipart: String = "Multipart" } - /// Constants related to types used in many components. enum Global { diff --git a/Sources/swift-openapi-generator/GenerateOptions.swift b/Sources/swift-openapi-generator/GenerateOptions.swift index 1066e80a4..7bfa4b141 100644 --- a/Sources/swift-openapi-generator/GenerateOptions.swift +++ b/Sources/swift-openapi-generator/GenerateOptions.swift @@ -111,9 +111,7 @@ extension _GenerateOptions { /// - Parameter config: The configuration specified by the user. /// - Returns: The type overrides requested by the user. func resolvedTypeOverrides(_ config: _UserConfig?) -> TypeOverrides { - guard let schemaOverrides = config?.typeOverrides?.schemas, !schemaOverrides.isEmpty else { - return .init() - } + guard let schemaOverrides = config?.typeOverrides?.schemas, !schemaOverrides.isEmpty else { return .init() } return TypeOverrides(schemas: schemaOverrides) } diff --git a/Sources/swift-openapi-generator/UserConfig.swift b/Sources/swift-openapi-generator/UserConfig.swift index f5153f6db..f83dd71fd 100644 --- a/Sources/swift-openapi-generator/UserConfig.swift +++ b/Sources/swift-openapi-generator/UserConfig.swift @@ -68,7 +68,6 @@ struct _UserConfig: Codable { /// A container of type overrides. struct TypeOverrides: Codable { - /// A dictionary of overrides for replacing the types generated from schemas with manually provided types. var schemas: [String: String]? } From 88706b7b61e7cfb008a769679bd92123f0ea3589 Mon Sep 17 00:00:00 2001 From: simonbility Date: Tue, 17 Jun 2025 13:59:14 +0200 Subject: [PATCH 35/35] Format Code in Tests --- Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift | 1 - .../SnippetBasedReferenceTests.swift | 1 - 2 files changed, 2 deletions(-) diff --git a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift index 12b7910e7..a4a48f39b 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_validateDoc.swift @@ -449,7 +449,6 @@ final class Test_validateDoc: Test_Core { ) } } - func testValidateTypeOverrides() throws { let schema = try loadSchemaFromYAML( #""" diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index cd22c947f..98cb2f15e 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -1945,7 +1945,6 @@ final class SnippetBasedReferenceTests: XCTestCase { """ ) } - func testTypeOverridesWithNameOverrides() throws { try assertSchemasTranslation( nameOverrides: ["UUID": "MyUUID"],