From 68ef99c0b7ebd59cb974e4c08810094dae81fbfb Mon Sep 17 00:00:00 2001 From: simonbility Date: Thu, 24 Apr 2025 20:45:01 +0200 Subject: [PATCH 1/9] Allow substituting types --- .../CommonTranslations/translateSchema.swift | 15 +++++++++++ .../Test_translateSchemas.swift | 26 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift index e92b5605..dc2cac4b 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift @@ -87,6 +87,21 @@ extension TypesFileTranslator { ) ) } + if let substituteTypeName = schema.vendorExtensions["x-swift-open-api-substitute-type"]?.value + as? String + { + try diagnostics.emit(.note(message: "Substituting type \(typeName) with \(substituteTypeName)")) + let substitutedType = TypeName(swiftKeyPath: substituteTypeName.components(separatedBy: ".")).asUsage + + let typealiasDecl = try translateTypealias( + named: typeName, + userDescription: overrides.userDescription ?? schema.description, + to: substitutedType.withOptional( + overrides.isOptional ?? typeMatcher.isOptional(schema, components: components) + ) + ) + return [typealiasDecl] + } // If this type maps to a referenceable schema, define a typealias if let builtinType = try typeMatcher.tryMatchReferenceableType(for: schema, components: components) { diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_translateSchemas.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_translateSchemas.swift index 1d1e89f5..6ae27414 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_translateSchemas.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_translateSchemas.swift @@ -47,4 +47,30 @@ class Test_translateSchemas: Test_Core { XCTAssertEqual(collector.diagnostics.map(\.description), diagnosticDescriptions) } } + + func testSchemaTypeSubstitution() throws { + let typeName = TypeName(swiftKeyPath: ["Foo"]) + + let schema = try loadSchemaFromYAML( + #""" + type: string + x-swift-open-api-substitute-type: MyLibrary.MyCustomType + """# + ) + let collector = AccumulatingDiagnosticCollector() + let translator = makeTranslator(diagnostics: collector) + let translated = try translator.translateSchema(typeName: typeName, schema: schema, overrides: .none) + + XCTAssertEqual( + collector.diagnostics.map(\.description), + ["note: Substituting type Foo with MyLibrary.MyCustomType"] + ) + XCTAssertTrue(translated.count == 1, "Should have one translated schema") + guard case let .typealias(typeAliasDescription) = translated.first?.strippingTopComment else { + XCTFail("Expected typealias description got") + return + } + XCTAssertEqual(typeAliasDescription.name, "Foo") + XCTAssertEqual(typeAliasDescription.existingType, .member(["MyLibrary", "MyCustomType"])) + } } From 762fbdd6697216784f49d1f8b9dcdb658d78baf8 Mon Sep 17 00:00:00 2001 From: simonbility Date: Wed, 14 May 2025 13:42:55 +0200 Subject: [PATCH 2/9] Support replacement also in inline defined schemas --- .../CommonTranslations/translateSchema.swift | 11 ++--- .../translateSubstitutedType.swift | 36 ++++++++++++++ .../Translator/CommonTypes/Constants.swift | 2 + .../TypeAssignment/TypeMatcher.swift | 49 +++++++++++++++++-- 4 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSubstitutedType.swift diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift index dc2cac4b..839db3f2 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift @@ -87,16 +87,11 @@ extension TypesFileTranslator { ) ) } - if let substituteTypeName = schema.vendorExtensions["x-swift-open-api-substitute-type"]?.value - as? String - { - try diagnostics.emit(.note(message: "Substituting type \(typeName) with \(substituteTypeName)")) - let substitutedType = TypeName(swiftKeyPath: substituteTypeName.components(separatedBy: ".")).asUsage - - let typealiasDecl = try translateTypealias( + if let substituteType = schema.value.substituteType() { + let typealiasDecl = try translateSubstitutedType( named: typeName, userDescription: overrides.userDescription ?? schema.description, - to: substitutedType.withOptional( + to: substituteType.asUsage.withOptional( overrides.isOptional ?? typeMatcher.isOptional(schema, components: components) ) ) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSubstitutedType.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSubstitutedType.swift new file mode 100644 index 00000000..6973d4fa --- /dev/null +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSubstitutedType.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import OpenAPIKit + +extension FileTranslator { + + /// Returns a declaration of a typealias. + /// - Parameters: + /// - typeName: The name of the type to give to the declared typealias. + /// - userDescription: A user-specified description from the OpenAPI document. + /// - existingTypeUsage: The existing type the alias points to. + /// - Throws: An error if there is an issue during translation. + /// - Returns: A declaration representing the translated typealias. + func translateSubstitutedType(named typeName: TypeName, userDescription: String?, to existingTypeUsage: TypeUsage) throws + -> Declaration + { + let typealiasDescription = TypealiasDescription( + accessModifier: config.access, + name: typeName.shortSwiftName, + existingType: .init(existingTypeUsage.withOptional(false)) + ) + let typealiasComment: Comment? = typeName.docCommentWithUserDescription(userDescription) + return .commentable(typealiasComment, .typealias(typealiasDescription)) + } +} diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index d1fbedcf..66bc10d7 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -372,6 +372,8 @@ enum Constants { /// The substring used in method names for the multipart coding strategy. static let multipart: String = "Multipart" } + /// Constants related to the vendor extensions.. + enum VendorExtension { static let replaceType: String = "x-swift-open-api-replace-type" } /// Constants related to types used in many components. enum Global { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift index 1c503ae7..716cef85 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// import OpenAPIKit +import Foundation /// A set of functions that match Swift types onto OpenAPI types. struct TypeMatcher { @@ -46,7 +47,11 @@ struct TypeMatcher { matchedArrayHandler: { elementType, nullableItems in nullableItems ? elementType.asOptional.asArray : elementType.asArray }, - genericArrayHandler: { TypeName.arrayContainer.asUsage } + genericArrayHandler: { TypeName.arrayContainer.asUsage }, + substitutedTypeHandler: { substitute in + // never built-in + nil + } ) } @@ -75,7 +80,10 @@ struct TypeMatcher { matchedArrayHandler: { elementType, nullableItems in nullableItems ? elementType.asOptional.asArray : elementType.asArray }, - genericArrayHandler: { TypeName.arrayContainer.asUsage } + genericArrayHandler: { TypeName.arrayContainer.asUsage }, + substitutedTypeHandler: { substitute in + substitute.asUsage + } )? .withOptional(isOptional(schema, components: components)) } @@ -98,7 +106,8 @@ struct TypeMatcher { return true }, matchedArrayHandler: { elementIsReferenceable, _ in elementIsReferenceable }, - genericArrayHandler: { true } + genericArrayHandler: { true }, + substitutedTypeHandler: { _ in true } ) ?? false } @@ -353,8 +362,10 @@ struct TypeMatcher { for schema: JSONSchema.Schema, test: (JSONSchema.Schema) throws -> R?, matchedArrayHandler: (R, _ nullableItems: Bool) -> R, - genericArrayHandler: () -> R + genericArrayHandler: () -> R, + substitutedTypeHandler: (TypeName) -> R? ) rethrows -> R? { + if let substitute = schema.substituteType() { return substitutedTypeHandler(substitute) } switch schema { case let .array(_, arrayContext): guard let items = arrayContext.items else { return genericArrayHandler() } @@ -363,7 +374,8 @@ struct TypeMatcher { for: items.value, test: test, matchedArrayHandler: matchedArrayHandler, - genericArrayHandler: genericArrayHandler + genericArrayHandler: genericArrayHandler, + substitutedTypeHandler: substitutedTypeHandler ) else { return nil } return matchedArrayHandler(itemsResult, items.nullable) @@ -371,3 +383,30 @@ struct TypeMatcher { } } } + +extension JSONSchema.Schema { + func substituteType() -> TypeName? { + let extensions: [String: AnyCodable] = + switch self { + case .null(let context): context.vendorExtensions + case .boolean(let context): context.vendorExtensions + case .number(let context, _): context.vendorExtensions + case .integer(let context, _): context.vendorExtensions + case .string(let context, _): context.vendorExtensions + case .object(let context, _): context.vendorExtensions + case .array(let context, _): context.vendorExtensions + case .all(of: _, core: let context): context.vendorExtensions + case .one(of: _, core: let context): context.vendorExtensions + case .any(of: _, core: let context): context.vendorExtensions + case .not: [:] + case .reference(_, let context): context.vendorExtensions + case .fragment(let context): context.vendorExtensions + } + guard let substituteTypeName = extensions[Constants.VendorExtension.replaceType]?.value as? String else { + return nil + } + assert(!substituteTypeName.isEmpty) + + return TypeName(swiftKeyPath: substituteTypeName.components(separatedBy: ".")) + } +} From cff57605279fb25909a9d5ee1d626fc4f76d6687 Mon Sep 17 00:00:00 2001 From: simonbility Date: Wed, 14 May 2025 13:43:04 +0200 Subject: [PATCH 3/9] More tests --- .../Test_translateSchemas.swift | 26 --- .../Test_typeSubstitutions.swift | 216 ++++++++++++++++++ 2 files changed, 216 insertions(+), 26 deletions(-) create mode 100644 Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeSubstitutions.swift diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_translateSchemas.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_translateSchemas.swift index 6ae27414..1d1e89f5 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_translateSchemas.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_translateSchemas.swift @@ -47,30 +47,4 @@ class Test_translateSchemas: Test_Core { XCTAssertEqual(collector.diagnostics.map(\.description), diagnosticDescriptions) } } - - func testSchemaTypeSubstitution() throws { - let typeName = TypeName(swiftKeyPath: ["Foo"]) - - let schema = try loadSchemaFromYAML( - #""" - type: string - x-swift-open-api-substitute-type: MyLibrary.MyCustomType - """# - ) - let collector = AccumulatingDiagnosticCollector() - let translator = makeTranslator(diagnostics: collector) - let translated = try translator.translateSchema(typeName: typeName, schema: schema, overrides: .none) - - XCTAssertEqual( - collector.diagnostics.map(\.description), - ["note: Substituting type Foo with MyLibrary.MyCustomType"] - ) - XCTAssertTrue(translated.count == 1, "Should have one translated schema") - guard case let .typealias(typeAliasDescription) = translated.first?.strippingTopComment else { - XCTFail("Expected typealias description got") - return - } - XCTAssertEqual(typeAliasDescription.name, "Foo") - XCTAssertEqual(typeAliasDescription.existingType, .member(["MyLibrary", "MyCustomType"])) - } } diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeSubstitutions.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeSubstitutions.swift new file mode 100644 index 00000000..2e0f2f63 --- /dev/null +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypesTranslator/Test_typeSubstitutions.swift @@ -0,0 +1,216 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +import OpenAPIKit +import Yams +@testable import _OpenAPIGeneratorCore + +class Test_typeSubstitutions: Test_Core { + + func testSchemaString() throws { + func _test( + schema schemaString: String, + expectedType: ExistingTypeDescription, + file: StaticString = #file, + line: UInt = #line + ) throws { + let typeName = TypeName(swiftKeyPath: ["Foo"]) + + let schema = try loadSchemaFromYAML(schemaString) + let collector = AccumulatingDiagnosticCollector() + let translator = makeTranslator(diagnostics: collector) + let translated = try translator.translateSchema(typeName: typeName, schema: schema, overrides: .none) + if translated.count != 1 { + XCTFail("Expected only a single translated schema, got: \(translated.count)", file: file, line: line) + return + } + XCTAssertTrue(translated.count == 1, "Should have one translated schema") + guard case let .typealias(typeAliasDescription) = translated.first?.strippingTopComment else { + XCTFail("Expected typealias description got", file: file, line: line) + return + } + XCTAssertEqual(typeAliasDescription.name, "Foo", file: file, line: line) + XCTAssertEqual(typeAliasDescription.existingType, expectedType, file: file, line: line) + } + try _test( + schema: #""" + type: string + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """#, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + try _test( + schema: """ + type: array + items: + type: integer + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + try _test( + schema: """ + type: string + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + try _test( + schema: """ + type: array + items: + type: integer + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .array(.member(["MyLibrary", "MyCustomType"])) + ) + // TODO: Investigate if vendor-extensions are allowed in anyOf, allOf, oneOf + try _test( + schema: """ + anyOf: + - type: string + - type: integer + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + try _test( + schema: """ + allOf: + - type: object + properties: + foo: + type: string + - type: object + properties: + bar: + type: string + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + try _test( + schema: """ + oneOf: + - type: object + properties: + foo: + type: string + - type: object + properties: + bar: + type: string + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + } + func testSimpleInlinePropertiesReplacements() throws { + func _testInlineProperty( + schema schemaString: String, + expectedType: ExistingTypeDescription, + file: StaticString = #file, + line: UInt = #line + ) throws { + let typeName = TypeName(swiftKeyPath: ["Foo"]) + + let propertySchema = try YAMLDecoder().decode(JSONSchema.self, from: schemaString).requiredSchemaObject() + let schema = JSONSchema.object(properties: ["property": propertySchema]) + let collector = AccumulatingDiagnosticCollector() + let translator = makeTranslator(diagnostics: collector) + let translated = try translator.translateSchema(typeName: typeName, schema: schema, overrides: .none) + if translated.count != 1 { + XCTFail("Expected only a single translated schema, got: \(translated.count)", file: file, line: line) + return + } + guard case let .struct(structDescription) = translated.first?.strippingTopComment else { + throw GenericError(message: "Expected struct") + } + let variables: [VariableDescription] = structDescription.members.compactMap { member in + guard case let .variable(variableDescription) = member.strippingTopComment else { return nil } + return variableDescription + } + if variables.count != 1 { + XCTFail("Expected only a single variable, got: \(variables.count)", file: file, line: line) + return + } + XCTAssertEqual(variables[0].type, expectedType, file: file, line: line) + } + try _testInlineProperty( + schema: """ + type: array + items: + type: integer + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + try _testInlineProperty( + schema: """ + type: string + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + try _testInlineProperty( + schema: """ + type: array + items: + type: integer + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .array(.member(["MyLibrary", "MyCustomType"])) + ) + // TODO: Investigate if vendor-extensions are allowed in anyOf, allOf, oneOf + try _testInlineProperty( + schema: """ + anyOf: + - type: string + - type: integer + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + try _testInlineProperty( + schema: """ + allOf: + - type: object + properties: + foo: + type: string + - type: object + properties: + bar: + type: string + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + try _testInlineProperty( + schema: """ + oneOf: + - type: object + properties: + foo: + type: string + - type: object + properties: + bar: + type: string + x-swift-open-api-replace-type: MyLibrary.MyCustomType + """, + expectedType: .member(["MyLibrary", "MyCustomType"]) + ) + } +} From 0227843a58e053f837e110d45db73470f24b8d56 Mon Sep 17 00:00:00 2001 From: simonbility Date: Wed, 14 May 2025 14:23:17 +0200 Subject: [PATCH 4/9] WIP Proposal --- .../Documentation.docc/Proposals/Proposals.md | 1 + .../Documentation.docc/Proposals/SOAR-0014.md | 72 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md diff --git a/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md b/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md index bdaff6a5..c8a91e34 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/Proposals.md @@ -55,3 +55,4 @@ If you have any questions, tag [Honza Dvorsky](https://github.com/czechboy0) or - - - +- diff --git a/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md new file mode 100644 index 00000000..59b69d72 --- /dev/null +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md @@ -0,0 +1,72 @@ +# SOAR-0014: Support Type Substitutions + +Allow using user-defined types instead of generated ones, utilizing vendor-extensions + +## Overview + +- Proposal: SOAR-0014 +- Author(s): [simonbility](https://github.com/simonbility) +- Status: **Awaiting Review** +- Issue: [apple/swift-openapi-generator#375](https://github.com/apple/swift-openapi-generator/issues/375) +- Implementation: + - [apple/swift-openapi-generator#764](https://github.com/apple/swift-openapi-generator/pull/764) +- Affected components: + - generator + +### Introduction + +The goal of this proposal is to allow users to specify custom types for generated code using vendor extensions. This will enable users to use their own types instead of the default generated ones, allowing for greater flexibility. + +### Motivation + +This proposal would enable more flexibility in the generated code. +Some usecases include: +- Using custom types that are already defined in the user's codebase or even coming from a third party library, instead of generating new ones. +- workaround missing support for `format` +- Implement custom validation/encoding/decoding logic that cannot be expressed using the OpenAPI spec + +This is intended as a "escape hatch" for use-cases that (currently) cannot be expressed. +Using this comes with the risk of user-provided types not being compliant with the original OpenAPI spec. + + +### Proposed solution + +The proposed solution is to allow the `x-swift-open-api-replace-type` vendor-extension to prevent the generation of types as usual, and instead use the specified type. +This should be supported anywhere within a OpenAPI document where a schema can be defined. +This includes: +* "TopLevel" schemas in `components.schemas` +* "Inline" schema definitions in object + +```diff + + +``` + + +Describe your solution to the problem. Provide examples and describe how they work. Show how your solution is better than current workarounds. + +This section should focus on what will change for the _adopters_ of Swift OpenAPI Generator. + +### Detailed design + +Describe the implementation of the feature, a link to a prototype implementation is encouraged here. + +This section should focus on what will change for the _contributors_ to Swift OpenAPI Generator. + +### API stability + +Discuss the API implications, making sure to considering all of: +- runtime public API +- runtime "Generated" SPI +- existing transport and middleware implementations +- generator implementation affected by runtime API changes +- generator API (config file, CLI, plugin) +- existing and new generated adopter code + +### Future directions + +Discuss any potential future improvements to the feature. + +### Alternatives considered + +Discuss the alternative solutions considered, even during the review process itself. From 8fe84b72219d89e25df800bd1e1df652e1ea96c4 Mon Sep 17 00:00:00 2001 From: simonbility Date: Wed, 14 May 2025 14:02:47 +0200 Subject: [PATCH 5/9] Add Example Project --- Examples/replace-types-example/.gitignore | 11 + Examples/replace-types-example/Package.swift | 37 +++ Examples/replace-types-example/README.md | 40 +++ .../ExternalLibrary/ExternalObject.swift | 4 + .../Sources/ExternalLibrary/PrimeNumber.swift | 35 +++ .../Sources/Types/Generated/Types.swift | 257 ++++++++++++++++++ .../Types/openapi-generator-config.yaml | 4 + .../Sources/Types/openapi.yaml | 1 + .../Sources/openapi.yaml | 46 ++++ 9 files changed, 435 insertions(+) create mode 100644 Examples/replace-types-example/.gitignore create mode 100644 Examples/replace-types-example/Package.swift create mode 100644 Examples/replace-types-example/README.md create mode 100644 Examples/replace-types-example/Sources/ExternalLibrary/ExternalObject.swift create mode 100644 Examples/replace-types-example/Sources/ExternalLibrary/PrimeNumber.swift create mode 100644 Examples/replace-types-example/Sources/Types/Generated/Types.swift create mode 100644 Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml create mode 120000 Examples/replace-types-example/Sources/Types/openapi.yaml create mode 100644 Examples/replace-types-example/Sources/openapi.yaml diff --git a/Examples/replace-types-example/.gitignore b/Examples/replace-types-example/.gitignore new file mode 100644 index 00000000..f6f5465e --- /dev/null +++ b/Examples/replace-types-example/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.vscode +/Package.resolved +.ci/ +.docc-build/ diff --git a/Examples/replace-types-example/Package.swift b/Examples/replace-types-example/Package.swift new file mode 100644 index 00000000..4f0f4d41 --- /dev/null +++ b/Examples/replace-types-example/Package.swift @@ -0,0 +1,37 @@ +// swift-tools-version:5.9 +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import PackageDescription + +let package = Package( + name: "replace-types-example", + platforms: [.macOS(.v14)], + products: [ + .library(name: "Types", targets: ["Types"]), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.7.0"), + ], + targets: [ + .target( + name: "Types", + dependencies: [ + "ExternalLibrary", + .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime")] + ), + .target( + name: "ExternalLibrary" + ), + ] +) diff --git a/Examples/replace-types-example/README.md b/Examples/replace-types-example/README.md new file mode 100644 index 00000000..974e6d52 --- /dev/null +++ b/Examples/replace-types-example/README.md @@ -0,0 +1,40 @@ +# Replacing types + +An example project using [Swift OpenAPI Generator](https://github.com/apple/swift-openapi-generator). + +> **Disclaimer:** This example is deliberately simplified and is intended for illustrative purposes only. + +## Overview + +This example shows how you can structure a Swift package to share the types +from an OpenAPI document between a client and server module by having a common +target that runs the generator in `types` mode only. + +This allows you to write extensions or other helper functions that use these +types and use them in both the client and server code. + +## Usage + +Build and run the server using: + +```console +% swift run hello-world-server +Build complete! +... +info HummingBird : [HummingbirdCore] Server started and listening on 127.0.0.1:8080 +``` + +Then, in another terminal window, run the client: + +```console +% swift run hello-world-client +Build complete! ++––––––––––––––––––+ +|+––––––––––––––––+| +||Hello, Stranger!|| +|+––––––––––––––––+| ++––––––––––––––––––+ +``` + +Note how the message is boxed twice: once by the server and once by the client, +both using an extension on a shared type, defined in the `Types` module. diff --git a/Examples/replace-types-example/Sources/ExternalLibrary/ExternalObject.swift b/Examples/replace-types-example/Sources/ExternalLibrary/ExternalObject.swift new file mode 100644 index 00000000..5082fd55 --- /dev/null +++ b/Examples/replace-types-example/Sources/ExternalLibrary/ExternalObject.swift @@ -0,0 +1,4 @@ +public struct ExternalObject: Codable, Hashable, Sendable { + public let foo: String + public let bar: String +} diff --git a/Examples/replace-types-example/Sources/ExternalLibrary/PrimeNumber.swift b/Examples/replace-types-example/Sources/ExternalLibrary/PrimeNumber.swift new file mode 100644 index 00000000..3d7dddd4 --- /dev/null +++ b/Examples/replace-types-example/Sources/ExternalLibrary/PrimeNumber.swift @@ -0,0 +1,35 @@ +public struct PrimeNumber: Codable, Hashable, RawRepresentable, Sendable { + public let rawValue: Int + public init?(rawValue: Int) { + if !rawValue.isPrime { return nil } + self.rawValue = rawValue + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let number = try container.decode(Int.self) + guard let value = PrimeNumber(rawValue: number) else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "The number is not prime.") + } + self = value + } + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.rawValue) + } + +} + +extension Int { + fileprivate var isPrime: Bool { + if self <= 1 { return false } + if self <= 3 { return true } + + var i = 2 + while i * i <= self { + if self % i == 0 { return false } + i += 1 + } + return true + } +} diff --git a/Examples/replace-types-example/Sources/Types/Generated/Types.swift b/Examples/replace-types-example/Sources/Types/Generated/Types.swift new file mode 100644 index 00000000..c5dc2180 --- /dev/null +++ b/Examples/replace-types-example/Sources/Types/Generated/Types.swift @@ -0,0 +1,257 @@ +// Generated by swift-openapi-generator, do not modify. +@_spi(Generated) import OpenAPIRuntime +#if os(Linux) +@preconcurrency import struct Foundation.URL +@preconcurrency import struct Foundation.Data +@preconcurrency import struct Foundation.Date +#else +import struct Foundation.URL +import struct Foundation.Data +import struct Foundation.Date +#endif +/// A type that performs HTTP operations defined by the OpenAPI document. +package protocol APIProtocol: Sendable { + /// - Remark: HTTP `GET /user`. + /// - Remark: Generated from `#/paths//user/get(getUser)`. + func getUser(_ input: Operations.GetUser.Input) async throws -> Operations.GetUser.Output +} + +/// Convenience overloads for operation inputs. +extension APIProtocol { + /// - Remark: HTTP `GET /user`. + /// - Remark: Generated from `#/paths//user/get(getUser)`. + package func getUser( + query: Operations.GetUser.Input.Query = .init(), + headers: Operations.GetUser.Input.Headers = .init() + ) async throws -> Operations.GetUser.Output { + try await getUser(Operations.GetUser.Input( + query: query, + headers: headers + )) + } +} + +/// Server URLs defined in the OpenAPI document. +package enum Servers { + /// Example service deployment. + package enum Server1 { + /// Example service deployment. + package static func url() throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://example.com/api", + variables: [] + ) + } + } + /// Example service deployment. + @available(*, deprecated, renamed: "Servers.Server1.url") + package static func server1() throws -> Foundation.URL { + try Foundation.URL( + validatingOpenAPIServerURL: "https://example.com/api", + variables: [] + ) + } +} + +/// Types generated from the components section of the OpenAPI document. +package enum Components { + /// Types generated from the `#/components/schemas` section of the OpenAPI document. + package enum Schemas { + /// - Remark: Generated from `#/components/schemas/UUID`. + package typealias Uuid = Swift.String + /// A value with the greeting contents. + /// + /// - Remark: Generated from `#/components/schemas/User`. + package struct User: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/User/id`. + package var id: Components.Schemas.Uuid? + /// - Remark: Generated from `#/components/schemas/User/favorite_prime_number`. + package var favoritePrimeNumber: Swift.Int? + /// - Remark: Generated from `#/components/schemas/User/foo`. + package struct FooPayload: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/User/foo/foo`. + package var foo: Swift.String? + /// - Remark: Generated from `#/components/schemas/User/foo/bar`. + package var bar: Swift.String? + /// Creates a new `FooPayload`. + /// + /// - Parameters: + /// - foo: + /// - bar: + package init( + foo: Swift.String? = nil, + bar: Swift.String? = nil + ) { + self.foo = foo + self.bar = bar + } + package enum CodingKeys: String, CodingKey { + case foo + case bar + } + } + /// - Remark: Generated from `#/components/schemas/User/foo`. + package var foo: Components.Schemas.User.FooPayload? + /// Creates a new `User`. + /// + /// - Parameters: + /// - id: + /// - favoritePrimeNumber: + /// - foo: + package init( + id: Components.Schemas.Uuid? = nil, + favoritePrimeNumber: Swift.Int? = nil, + foo: Components.Schemas.User.FooPayload? = nil + ) { + self.id = id + self.favoritePrimeNumber = favoritePrimeNumber + self.foo = foo + } + package enum CodingKeys: String, CodingKey { + case id + case favoritePrimeNumber = "favorite_prime_number" + case foo + } + } + } + /// Types generated from the `#/components/parameters` section of the OpenAPI document. + package enum Parameters {} + /// Types generated from the `#/components/requestBodies` section of the OpenAPI document. + package enum RequestBodies {} + /// Types generated from the `#/components/responses` section of the OpenAPI document. + package enum Responses {} + /// Types generated from the `#/components/headers` section of the OpenAPI document. + package enum Headers {} +} + +/// API operations, with input and output types, generated from `#/paths` in the OpenAPI document. +package enum Operations { + /// - Remark: HTTP `GET /user`. + /// - Remark: Generated from `#/paths//user/get(getUser)`. + package enum GetUser { + package static let id: Swift.String = "getUser" + package struct Input: Sendable, Hashable { + /// - Remark: Generated from `#/paths/user/GET/query`. + package struct Query: Sendable, Hashable { + /// The name of the user + /// + /// - Remark: Generated from `#/paths/user/GET/query/name`. + package var name: Swift.String? + /// Creates a new `Query`. + /// + /// - Parameters: + /// - name: The name of the user + package init(name: Swift.String? = nil) { + self.name = name + } + } + package var query: Operations.GetUser.Input.Query + /// - Remark: Generated from `#/paths/user/GET/header`. + package struct Headers: Sendable, Hashable { + package var accept: [OpenAPIRuntime.AcceptHeaderContentType] + /// Creates a new `Headers`. + /// + /// - Parameters: + /// - accept: + package init(accept: [OpenAPIRuntime.AcceptHeaderContentType] = .defaultValues()) { + self.accept = accept + } + } + package var headers: Operations.GetUser.Input.Headers + /// Creates a new `Input`. + /// + /// - Parameters: + /// - query: + /// - headers: + package init( + query: Operations.GetUser.Input.Query = .init(), + headers: Operations.GetUser.Input.Headers = .init() + ) { + self.query = query + self.headers = headers + } + } + @frozen package enum Output: Sendable, Hashable { + package struct Ok: Sendable, Hashable { + /// - Remark: Generated from `#/paths/user/GET/responses/200/content`. + @frozen package enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/user/GET/responses/200/content/application\/json`. + case json(Components.Schemas.User) + /// The associated value of the enum case if `self` is `.json`. + /// + /// - Throws: An error if `self` is not `.json`. + /// - SeeAlso: `.json`. + package var json: Components.Schemas.User { + get throws { + switch self { + case let .json(body): + return body + } + } + } + } + /// Received HTTP response body + package var body: Operations.GetUser.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - body: Received HTTP response body + package init(body: Operations.GetUser.Output.Ok.Body) { + self.body = body + } + } + /// A success response with the user. + /// + /// - Remark: Generated from `#/paths//user/get(getUser)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.GetUser.Output.Ok) + /// The associated value of the enum case if `self` is `.ok`. + /// + /// - Throws: An error if `self` is not `.ok`. + /// - SeeAlso: `.ok`. + package var ok: Operations.GetUser.Output.Ok { + get throws { + switch self { + case let .ok(response): + return response + default: + try throwUnexpectedResponseStatus( + expectedStatus: "ok", + response: self + ) + } + } + } + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Swift.Int, OpenAPIRuntime.UndocumentedPayload) + } + @frozen package enum AcceptableContentType: AcceptableProtocol { + case json + case other(Swift.String) + package init?(rawValue: Swift.String) { + switch rawValue.lowercased() { + case "application/json": + self = .json + default: + self = .other(rawValue) + } + } + package var rawValue: Swift.String { + switch self { + case let .other(string): + return string + case .json: + return "application/json" + } + } + package static var allCases: [Self] { + [ + .json + ] + } + } + } +} diff --git a/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml b/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml new file mode 100644 index 00000000..a12e67bf --- /dev/null +++ b/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml @@ -0,0 +1,4 @@ +generate: + - types +accessModifier: package +namingStrategy: idiomatic diff --git a/Examples/replace-types-example/Sources/Types/openapi.yaml b/Examples/replace-types-example/Sources/Types/openapi.yaml new file mode 120000 index 00000000..1c2a243e --- /dev/null +++ b/Examples/replace-types-example/Sources/Types/openapi.yaml @@ -0,0 +1 @@ +../openapi.yaml \ No newline at end of file diff --git a/Examples/replace-types-example/Sources/openapi.yaml b/Examples/replace-types-example/Sources/openapi.yaml new file mode 100644 index 00000000..bcdfb7a1 --- /dev/null +++ b/Examples/replace-types-example/Sources/openapi.yaml @@ -0,0 +1,46 @@ +openapi: '3.1.0' +info: + title: GreetingService + version: 1.0.0 +servers: + - url: https://example.com/api + description: Example service deployment. +paths: + /user: + get: + operationId: getUser + parameters: + - name: name + required: false + in: query + description: The name of the user + schema: + type: string + responses: + '200': + description: A success response with the user. + content: + application/json: + schema: + $ref: '#/components/schemas/User' +components: + schemas: + UUID: + type: string + format: uuid + + User: + type: object + description: A value with the greeting contents. + properties: + id: + $ref: '#/components/schemas/UUID' + favorite_prime_number: + type: integer + foo: + type: object + properties: + foo: + type: string + bar: + type: string From 8646df857879128e3a1ad2cbaf576ce50d770f66 Mon Sep 17 00:00:00 2001 From: simonbility Date: Wed, 14 May 2025 14:38:34 +0200 Subject: [PATCH 6/9] Apply x-swift-open-api-replace-type extension --- .../Sources/Types/Generated/Types.swift | 35 ++++--------------- .../Types/openapi-generator-config.yaml | 3 ++ .../Sources/openapi.yaml | 4 +++ 3 files changed, 14 insertions(+), 28 deletions(-) diff --git a/Examples/replace-types-example/Sources/Types/Generated/Types.swift b/Examples/replace-types-example/Sources/Types/Generated/Types.swift index c5dc2180..d617ebe7 100644 --- a/Examples/replace-types-example/Sources/Types/Generated/Types.swift +++ b/Examples/replace-types-example/Sources/Types/Generated/Types.swift @@ -9,6 +9,8 @@ import struct Foundation.URL import struct Foundation.Data import struct Foundation.Date #endif +import Foundation +import ExternalLibrary /// A type that performs HTTP operations defined by the OpenAPI document. package protocol APIProtocol: Sendable { /// - Remark: HTTP `GET /user`. @@ -58,7 +60,7 @@ package enum Components { /// Types generated from the `#/components/schemas` section of the OpenAPI document. package enum Schemas { /// - Remark: Generated from `#/components/schemas/UUID`. - package typealias Uuid = Swift.String + package typealias Uuid = Foundation.UUID /// A value with the greeting contents. /// /// - Remark: Generated from `#/components/schemas/User`. @@ -66,32 +68,9 @@ package enum Components { /// - Remark: Generated from `#/components/schemas/User/id`. package var id: Components.Schemas.Uuid? /// - Remark: Generated from `#/components/schemas/User/favorite_prime_number`. - package var favoritePrimeNumber: Swift.Int? + package var favoritePrimeNumber: ExternalLibrary.PrimeNumber? /// - Remark: Generated from `#/components/schemas/User/foo`. - package struct FooPayload: Codable, Hashable, Sendable { - /// - Remark: Generated from `#/components/schemas/User/foo/foo`. - package var foo: Swift.String? - /// - Remark: Generated from `#/components/schemas/User/foo/bar`. - package var bar: Swift.String? - /// Creates a new `FooPayload`. - /// - /// - Parameters: - /// - foo: - /// - bar: - package init( - foo: Swift.String? = nil, - bar: Swift.String? = nil - ) { - self.foo = foo - self.bar = bar - } - package enum CodingKeys: String, CodingKey { - case foo - case bar - } - } - /// - Remark: Generated from `#/components/schemas/User/foo`. - package var foo: Components.Schemas.User.FooPayload? + package var foo: ExternalLibrary.ExternalObject? /// Creates a new `User`. /// /// - Parameters: @@ -100,8 +79,8 @@ package enum Components { /// - foo: package init( id: Components.Schemas.Uuid? = nil, - favoritePrimeNumber: Swift.Int? = nil, - foo: Components.Schemas.User.FooPayload? = nil + favoritePrimeNumber: ExternalLibrary.PrimeNumber? = nil, + foo: ExternalLibrary.ExternalObject? = nil ) { self.id = id self.favoritePrimeNumber = favoritePrimeNumber diff --git a/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml b/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml index a12e67bf..585b5e7d 100644 --- a/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml +++ b/Examples/replace-types-example/Sources/Types/openapi-generator-config.yaml @@ -2,3 +2,6 @@ generate: - types accessModifier: package namingStrategy: idiomatic +additionalImports: + - Foundation + - ExternalLibrary diff --git a/Examples/replace-types-example/Sources/openapi.yaml b/Examples/replace-types-example/Sources/openapi.yaml index bcdfb7a1..dc75821a 100644 --- a/Examples/replace-types-example/Sources/openapi.yaml +++ b/Examples/replace-types-example/Sources/openapi.yaml @@ -28,6 +28,7 @@ components: UUID: type: string format: uuid + x-swift-open-api-replace-type: Foundation.UUID User: type: object @@ -37,6 +38,7 @@ components: $ref: '#/components/schemas/UUID' favorite_prime_number: type: integer + x-swift-open-api-replace-type: ExternalLibrary.PrimeNumber foo: type: object properties: @@ -44,3 +46,5 @@ components: type: string bar: type: string + + x-swift-open-api-replace-type: ExternalLibrary.ExternalObject From 37b854432a3e0c97e259a2c2e32347ba46d377f0 Mon Sep 17 00:00:00 2001 From: simonbility Date: Fri, 16 May 2025 15:20:03 +0200 Subject: [PATCH 7/9] Add additionalProperties to examples --- .../Sources/Types/Generated/Types.swift | 43 ++++++++++++++++++- .../Sources/openapi.yaml | 6 ++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/Examples/replace-types-example/Sources/Types/Generated/Types.swift b/Examples/replace-types-example/Sources/Types/Generated/Types.swift index d617ebe7..6fa854ce 100644 --- a/Examples/replace-types-example/Sources/Types/Generated/Types.swift +++ b/Examples/replace-types-example/Sources/Types/Generated/Types.swift @@ -71,26 +71,67 @@ package enum Components { package var favoritePrimeNumber: ExternalLibrary.PrimeNumber? /// - Remark: Generated from `#/components/schemas/User/foo`. package var foo: ExternalLibrary.ExternalObject? + /// A container of undocumented properties. + package var additionalProperties: [String: OpenAPIRuntime.OpenAPIObjectContainer] /// Creates a new `User`. /// /// - Parameters: /// - id: /// - favoritePrimeNumber: /// - foo: + /// - additionalProperties: A container of undocumented properties. package init( id: Components.Schemas.Uuid? = nil, favoritePrimeNumber: ExternalLibrary.PrimeNumber? = nil, - foo: ExternalLibrary.ExternalObject? = nil + foo: ExternalLibrary.ExternalObject? = nil, + additionalProperties: [String: OpenAPIRuntime.OpenAPIObjectContainer] = .init() ) { self.id = id self.favoritePrimeNumber = favoritePrimeNumber self.foo = foo + self.additionalProperties = additionalProperties } package enum CodingKeys: String, CodingKey { case id case favoritePrimeNumber = "favorite_prime_number" case foo } + package init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decodeIfPresent( + Components.Schemas.Uuid.self, + forKey: .id + ) + self.favoritePrimeNumber = try container.decodeIfPresent( + ExternalLibrary.PrimeNumber.self, + forKey: .favoritePrimeNumber + ) + self.foo = try container.decodeIfPresent( + ExternalLibrary.ExternalObject.self, + forKey: .foo + ) + additionalProperties = try decoder.decodeAdditionalProperties(knownKeys: [ + "id", + "favorite_prime_number", + "foo" + ]) + } + package func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent( + self.id, + forKey: .id + ) + try container.encodeIfPresent( + self.favoritePrimeNumber, + forKey: .favoritePrimeNumber + ) + try container.encodeIfPresent( + self.foo, + forKey: .foo + ) + try encoder.encodeAdditionalProperties(additionalProperties) + } } } /// Types generated from the `#/components/parameters` section of the OpenAPI document. diff --git a/Examples/replace-types-example/Sources/openapi.yaml b/Examples/replace-types-example/Sources/openapi.yaml index dc75821a..69a30002 100644 --- a/Examples/replace-types-example/Sources/openapi.yaml +++ b/Examples/replace-types-example/Sources/openapi.yaml @@ -46,5 +46,9 @@ components: type: string bar: type: string - x-swift-open-api-replace-type: ExternalLibrary.ExternalObject + default: + foo: "foo" + bar: "bar" + additionalProperties: + type: object From dbf467d059caa5835a4858c20fb2c4be0c678fbf Mon Sep 17 00:00:00 2001 From: simonbility Date: Fri, 16 May 2025 15:23:58 +0200 Subject: [PATCH 8/9] Replace in additionalProperties --- .../Sources/Types/Generated/Types.swift | 4 +-- .../Sources/openapi.yaml | 27 ++++++++++--------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/Examples/replace-types-example/Sources/Types/Generated/Types.swift b/Examples/replace-types-example/Sources/Types/Generated/Types.swift index 6fa854ce..56d45551 100644 --- a/Examples/replace-types-example/Sources/Types/Generated/Types.swift +++ b/Examples/replace-types-example/Sources/Types/Generated/Types.swift @@ -72,7 +72,7 @@ package enum Components { /// - Remark: Generated from `#/components/schemas/User/foo`. package var foo: ExternalLibrary.ExternalObject? /// A container of undocumented properties. - package var additionalProperties: [String: OpenAPIRuntime.OpenAPIObjectContainer] + package var additionalProperties: [String: ExternalLibrary.PrimeNumber] /// Creates a new `User`. /// /// - Parameters: @@ -84,7 +84,7 @@ package enum Components { id: Components.Schemas.Uuid? = nil, favoritePrimeNumber: ExternalLibrary.PrimeNumber? = nil, foo: ExternalLibrary.ExternalObject? = nil, - additionalProperties: [String: OpenAPIRuntime.OpenAPIObjectContainer] = .init() + additionalProperties: [String: ExternalLibrary.PrimeNumber] = .init() ) { self.id = id self.favoritePrimeNumber = favoritePrimeNumber diff --git a/Examples/replace-types-example/Sources/openapi.yaml b/Examples/replace-types-example/Sources/openapi.yaml index 69a30002..8d1e4d33 100644 --- a/Examples/replace-types-example/Sources/openapi.yaml +++ b/Examples/replace-types-example/Sources/openapi.yaml @@ -37,18 +37,19 @@ components: id: $ref: '#/components/schemas/UUID' favorite_prime_number: - type: integer - x-swift-open-api-replace-type: ExternalLibrary.PrimeNumber + type: integer + x-swift-open-api-replace-type: ExternalLibrary.PrimeNumber foo: - type: object - properties: - foo: - type: string - bar: - type: string - x-swift-open-api-replace-type: ExternalLibrary.ExternalObject - default: - foo: "foo" - bar: "bar" + type: object + properties: + foo: + type: string + bar: + type: string + x-swift-open-api-replace-type: ExternalLibrary.ExternalObject + default: + foo: "foo" + bar: "bar" additionalProperties: - type: object + type: integer + x-swift-open-api-replace-type: ExternalLibrary.PrimeNumber From 108386aa79dcc00dfaec78505da335d00613bbee Mon Sep 17 00:00:00 2001 From: simonbility Date: Fri, 16 May 2025 15:49:46 +0200 Subject: [PATCH 9/9] Update ProposalText --- .../Documentation.docc/Proposals/SOAR-0014.md | 100 ++++++++++++++---- 1 file changed, 80 insertions(+), 20 deletions(-) diff --git a/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md index 59b69d72..528a9aee 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Proposals/SOAR-0014.md @@ -22,7 +22,7 @@ The goal of this proposal is to allow users to specify custom types for generate This proposal would enable more flexibility in the generated code. Some usecases include: - Using custom types that are already defined in the user's codebase or even coming from a third party library, instead of generating new ones. -- workaround missing support for `format` +- workaround missing support for `format` for strings - Implement custom validation/encoding/decoding logic that cannot be expressed using the OpenAPI spec This is intended as a "escape hatch" for use-cases that (currently) cannot be expressed. @@ -32,41 +32,101 @@ Using this comes with the risk of user-provided types not being compliant with t ### Proposed solution The proposed solution is to allow the `x-swift-open-api-replace-type` vendor-extension to prevent the generation of types as usual, and instead use the specified type. -This should be supported anywhere within a OpenAPI document where a schema can be defined. -This includes: -* "TopLevel" schemas in `components.schemas` -* "Inline" schema definitions in object +This should be supported anywhere within a OpenAPI document where a schema can be defined. (e.g. `components.schemas`, `properites`, `additionalProperties`, etc.) -```diff +It can be used in "top-level" schemas, defined in `components.schemas` +```diff + components: + schemas: + UUID: + type: string + format: uuid ++ x-swift-open-api-replace-type: Foundation.UUID +``` +Will affect the generated code in the following way: +```diff + /// Types generated from the `#/components/schemas` section of the OpenAPI document. + enum Schemas { + /// - Remark: Generated from `#/components/schemas/UUID`. +- package typealias Uuid = Swift.String ++ package typealias Uuid = Foundation.UUID ``` -Describe your solution to the problem. Provide examples and describe how they work. Show how your solution is better than current workarounds. +This will also work for properties defined inline -This section should focus on what will change for the _adopters_ of Swift OpenAPI Generator. +```diff + components: + schemas: + UUID: + type: string + format: uuid + + User: + type: object + properties: + id: + type: string + name: + type: string ++ x-swift-open-api-replace-type: ExternalLibrary.ExternallyDefinedUser +``` -### Detailed design +Will affect the generated code in the following way: -Describe the implementation of the feature, a link to a prototype implementation is encouraged here. +```diff +enum Schemas { + /// - Remark: Generated from `#/components/schemas/User`. +- package struct User: Codable, Hashable, Sendable { +- /// - Remark: Generated from `#/components/schemas/User/id`. +- package var id: Components.Schemas.Uuid? +- /// - Remark: Generated from `#/components/schemas/User/name`. +- package var name: Swift.String? +- /// Creates a new `User`. +- /// +- /// - Parameters: +- /// - id: +- /// - name: +- package init( +- id: Components.Schemas.Uuid? = nil, +- name: Swift.String? = nil +- ) { +- self.id = id +- self.name = name +- } +- package enum CodingKeys: String, CodingKey { +- case id +- case name +- } +- } ++ package typealias User = ExternalLibrary.ExternallyDefinedUser +} +``` + +### Detailed design -This section should focus on what will change for the _contributors_ to Swift OpenAPI Generator. +The implementation modifies the Translator and the TypeAssignement logic to account for the presence of the vendor extension. ### API stability -Discuss the API implications, making sure to considering all of: -- runtime public API -- runtime "Generated" SPI -- existing transport and middleware implementations -- generator implementation affected by runtime API changes -- generator API (config file, CLI, plugin) -- existing and new generated adopter code +While this proposal does affect the generated code, it requires the addition of a very specific vendor-extension. + +This is interpreted as a "strong enough" signal of the user to opt into this behaviour, to justify NOT introducing a feauture-flag or considering this a breaking change. + ### Future directions -Discuss any potential future improvements to the feature. +None so far. ### Alternatives considered +An alternative to relying on vendor-extension, was to allow specifying the types to be replaced via paths in the config file like this + +```yaml +... +replaceTypes: + #/components/schemas/User: Foundation.UUID +``` -Discuss the alternative solutions considered, even during the review process itself. +The advantage of this approach is that it could also be used without modifying the OpenAPI document. (which is not always possible/straightforward when using third party API-specs)