Skip to content

Commit eb66fa9

Browse files
simonbilitysimonbilityczechboy0
authored
Allow substituting types (#764)
### Motivation Picking up after some time from this issue #375 There are some usecases described here that i think could be addressed by this. I suspect there are some "bigger picture" decisions (maybe proposals) needing to happen so i wanted to get the ball rolling :) ### Modifications I made a small change allowing to "swap in" any type instead of generating one, by using a vendor-extension (`x-swift-open-api-substitute-type`) ### Result The following spec ```yaml openapi: 3.1.0 info: title: api version: 1.0.0 components: schemas: MyCustomString: type: string x-swift-open-api-substitute-type: MyLibrary.MyCustomString ``` would generate code like this (abbreviated) ```swift public enum Components { public enum Schemas { /// - Remark: Generated from `#/components/schemas/MyCustomString`. public typealias MyCustomString = MyLibrary.MyCustomString } } ``` ### Test Plan I did write a test but suspect theres, other parts affected that i missed --------- Co-authored-by: simonbility <[email protected]> Co-authored-by: Honza Dvorsky <[email protected]>
1 parent 09ed6c4 commit eb66fa9

File tree

20 files changed

+351
-3
lines changed

20 files changed

+351
-3
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.DS_Store
2+
.build
3+
/Packages
4+
/*.xcodeproj
5+
xcuserdata/
6+
DerivedData/
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.vscode
9+
/Package.resolved
10+
.ci/
11+
.docc-build/
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// swift-tools-version:5.10
2+
//===----------------------------------------------------------------------===//
3+
//
4+
// This source file is part of the SwiftOpenAPIGenerator open source project
5+
//
6+
// Copyright (c) 2025 Apple Inc. and the SwiftOpenAPIGenerator project authors
7+
// Licensed under Apache License v2.0
8+
//
9+
// See LICENSE.txt for license information
10+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
11+
//
12+
// SPDX-License-Identifier: Apache-2.0
13+
//
14+
//===----------------------------------------------------------------------===//
15+
import PackageDescription
16+
17+
let package = Package(
18+
name: "type-overrides-example",
19+
platforms: [.macOS(.v10_15)],
20+
products: [.library(name: "Types", targets: ["Types"])],
21+
dependencies: [
22+
.package(url: "https://github.com/apple/swift-openapi-generator", from: "1.9.0"),
23+
.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.7.0"),
24+
],
25+
targets: [
26+
.target(
27+
name: "Types",
28+
dependencies: ["ExternalLibrary", .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime")],
29+
plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")]
30+
), .target(name: "ExternalLibrary"),
31+
]
32+
)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Overriding types
2+
3+
An example project using [Swift OpenAPI Generator](https://github.com/apple/swift-openapi-generator).
4+
5+
> **Disclaimer:** This example is deliberately simplified and is intended for illustrative purposes only.
6+
7+
## Overview
8+
9+
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.
10+
11+
## Usage
12+
13+
Build:
14+
15+
```console
16+
% swift build
17+
Build complete!
18+
```
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
/// Example struct to be used instead of the default generated type.
16+
/// This illustrates how to introduce a type performing additional validation during Decoding that cannot be expressed with OpenAPI
17+
public struct PrimeNumber: Codable, Hashable, RawRepresentable, Sendable {
18+
public let rawValue: Int
19+
public init?(rawValue: Int) {
20+
if !rawValue.isPrime { return nil }
21+
self.rawValue = rawValue
22+
}
23+
24+
public init(from decoder: any Decoder) throws {
25+
let container = try decoder.singleValueContainer()
26+
let number = try container.decode(Int.self)
27+
guard let value = PrimeNumber(rawValue: number) else {
28+
throw DecodingError.dataCorruptedError(in: container, debugDescription: "The number is not prime.")
29+
}
30+
self = value
31+
}
32+
33+
public func encode(to encoder: any Encoder) throws {
34+
var container = encoder.singleValueContainer()
35+
try container.encode(self.rawValue)
36+
}
37+
}
38+
39+
extension Int {
40+
fileprivate var isPrime: Bool {
41+
if self <= 1 { return false }
42+
if self <= 3 { return true }
43+
44+
var i = 2
45+
while i * i <= self {
46+
if self % i == 0 { return false }
47+
i += 1
48+
}
49+
return true
50+
}
51+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
generate:
2+
- types
3+
accessModifier: package
4+
namingStrategy: idiomatic
5+
additionalImports:
6+
- Foundation
7+
- ExternalLibrary
8+
typeOverrides:
9+
schemas:
10+
UUID: Foundation.UUID
11+
PrimeNumber: ExternalLibrary.PrimeNumber
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../openapi.yaml
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
openapi: '3.1.0'
2+
info:
3+
title: GreetingService
4+
version: 1.0.0
5+
servers:
6+
- url: https://example.com/api
7+
description: Example service deployment.
8+
components:
9+
schemas:
10+
UUID: # this will be replaced by with Foundation.UUID specified by typeOverrides in openapi-generator-config
11+
type: string
12+
format: uuid
13+
14+
PrimeNumber: # this will be replaced by with ExternalLibrary.PrimeNumber specified by typeOverrides in openapi-generator-config
15+
type: string
16+
format: uuid
17+
18+
User:
19+
type: object
20+
properties:
21+
id:
22+
$ref: '#/components/schemas/UUID'
23+
name:
24+
type: string
25+
favorite_prime_number:
26+
$ref: '#/components/schemas/PrimeNumber'

Sources/_OpenAPIGeneratorCore/Config.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ public struct Config: Sendable {
6262

6363
/// A map of OpenAPI identifiers to desired Swift identifiers, used instead of the naming strategy.
6464
public var nameOverrides: [String: String]
65+
/// A map of OpenAPI schema names to desired custom type names.
66+
public var typeOverrides: TypeOverrides
6567

6668
/// Additional pre-release features to enable.
6769
public var featureFlags: FeatureFlags
@@ -77,6 +79,7 @@ public struct Config: Sendable {
7779
/// Defaults to `defensive`.
7880
/// - nameOverrides: A map of OpenAPI identifiers to desired Swift identifiers, used instead
7981
/// of the naming strategy.
82+
/// - typeOverrides: A map of OpenAPI schema names to desired custom type names.
8083
/// - featureFlags: Additional pre-release features to enable.
8184
public init(
8285
mode: GeneratorMode,
@@ -86,6 +89,7 @@ public struct Config: Sendable {
8689
filter: DocumentFilter? = nil,
8790
namingStrategy: NamingStrategy,
8891
nameOverrides: [String: String] = [:],
92+
typeOverrides: TypeOverrides = .init(),
8993
featureFlags: FeatureFlags = []
9094
) {
9195
self.mode = mode
@@ -95,6 +99,7 @@ public struct Config: Sendable {
9599
self.filter = filter
96100
self.namingStrategy = namingStrategy
97101
self.nameOverrides = nameOverrides
102+
self.typeOverrides = typeOverrides
98103
self.featureFlags = featureFlags
99104
}
100105
}

Sources/_OpenAPIGeneratorCore/Parser/validateDoc.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,28 @@ func validateReferences(in doc: ParsedOpenAPIRepresentation) throws {
252252
}
253253
}
254254

255+
/// Validates all type overrides from a Config are present in the components of a ParsedOpenAPIRepresentation.
256+
///
257+
/// 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.
258+
///
259+
/// - Parameters:
260+
/// - doc: The OpenAPI document to validate.
261+
/// - config: The generator config.
262+
/// - Returns: An array of diagnostic messages representing type overrides for nonexistent schemas.
263+
func validateTypeOverrides(_ doc: ParsedOpenAPIRepresentation, config: Config) -> [Diagnostic] {
264+
let nonExistentOverrides = config.typeOverrides.schemas.keys
265+
.filter { key in
266+
guard let componentKey = OpenAPI.ComponentKey(rawValue: key) else { return false }
267+
return !doc.components.schemas.contains(key: componentKey)
268+
}
269+
.sorted()
270+
return nonExistentOverrides.map { override in
271+
Diagnostic.warning(
272+
message: "A type override defined for schema '\(override)' is not defined in the OpenAPI document."
273+
)
274+
}
275+
}
276+
255277
/// Runs validation steps on the incoming OpenAPI document.
256278
/// - Parameters:
257279
/// - doc: The OpenAPI document to validate.
@@ -263,6 +285,7 @@ func validateDoc(_ doc: ParsedOpenAPIRepresentation, config: Config) throws -> [
263285
try validateContentTypes(in: doc) { contentType in
264286
(try? _OpenAPIGeneratorCore.ContentType(string: contentType)) != nil
265287
}
288+
let typeOverrideDiagnostics = validateTypeOverrides(doc, config: config)
266289

267290
// Run OpenAPIKit's built-in validation.
268291
// Pass `false` to `strict`, however, because we don't
@@ -283,5 +306,5 @@ func validateDoc(_ doc: ParsedOpenAPIRepresentation, config: Config) throws -> [
283306
]
284307
)
285308
}
286-
return diagnostics
309+
return typeOverrideDiagnostics + diagnostics
287310
}

Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateSchema.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414
import OpenAPIKit
15+
import Foundation
1516

1617
extension TypesFileTranslator {
1718

@@ -88,6 +89,17 @@ extension TypesFileTranslator {
8889
)
8990
}
9091

92+
// Apply type overrides.
93+
if let jsonPath = typeName.shortJSONName, let typeOverride = config.typeOverrides.schemas[jsonPath] {
94+
let typeOverride = TypeName(swiftKeyPath: typeOverride.components(separatedBy: "."))
95+
let typealiasDecl = try translateTypealias(
96+
named: typeName,
97+
userDescription: overrides.userDescription ?? schema.description,
98+
to: typeOverride.asUsage
99+
)
100+
return [typealiasDecl]
101+
}
102+
91103
// If this type maps to a referenceable schema, define a typealias
92104
if let builtinType = try typeMatcher.tryMatchReferenceableType(for: schema, components: components) {
93105
let typealiasDecl = try translateTypealias(

Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,6 @@ enum Constants {
372372
/// The substring used in method names for the multipart coding strategy.
373373
static let multipart: String = "Multipart"
374374
}
375-
376375
/// Constants related to types used in many components.
377376
enum Global {
378377

Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateSchemas.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ extension TypesFileTranslator {
5757
_ schemas: OpenAPI.ComponentDictionary<JSONSchema>,
5858
multipartSchemaNames: Set<OpenAPI.ComponentKey>
5959
) throws -> Declaration {
60-
6160
let decls: [Declaration] = try schemas.flatMap { key, value in
6261
try translateSchema(
6362
componentKey: key,
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
/// A container of schema type overrides.
16+
public struct TypeOverrides: Sendable {
17+
/// A dictionary of overrides for replacing named schemas from the OpenAPI document with custom types.
18+
public var schemas: [String: String]
19+
20+
/// Creates a new instance.
21+
/// - Parameter schemas: A dictionary of overrides for replacing named schemas from the OpenAPI document with custom types.
22+
public init(schemas: [String: String] = [:]) { self.schemas = schemas }
23+
}

Sources/swift-openapi-generator/Documentation.docc/Articles/Configuring-the-generator.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,15 @@ filter:
145145
tags:
146146
- myTag
147147
```
148+
149+
### Type overrides
150+
151+
Type Overrides can be used used to replace the default generated type with a custom type.
152+
153+
```yaml
154+
typeOverrides:
155+
schemas:
156+
UUID: Foundation.UUID
157+
```
158+
159+
Check out [SOAR-0014](https://swiftpackageindex.com/apple/swift-openapi-generator/documentation/swift-openapi-generator/soar-0014) for details.

Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ extension _GenerateOptions {
3535
let resolvedAdditionalFileComments = resolvedAdditionalFileComments(config)
3636
let resolvedNamingStragy = resolvedNamingStrategy(config)
3737
let resolvedNameOverrides = resolvedNameOverrides(config)
38+
let resolvedTypeOverrides = resolvedTypeOverrides(config)
3839
let resolvedFeatureFlags = resolvedFeatureFlags(config)
3940
let configs: [Config] = sortedModes.map {
4041
.init(
@@ -45,6 +46,7 @@ extension _GenerateOptions {
4546
filter: config?.filter,
4647
namingStrategy: resolvedNamingStragy,
4748
nameOverrides: resolvedNameOverrides,
49+
typeOverrides: resolvedTypeOverrides,
4850
featureFlags: resolvedFeatureFlags
4951
)
5052
}
@@ -61,6 +63,9 @@ extension _GenerateOptions {
6163
- Name overrides: \(resolvedNameOverrides.isEmpty ? "<none>" : resolvedNameOverrides
6264
.sorted(by: { $0.key < $1.key })
6365
.map { "\"\($0.key)\"->\"\($0.value)\"" }.joined(separator: ", "))
66+
- Type overrides: \(resolvedTypeOverrides.schemas.isEmpty ? "<none>" : resolvedTypeOverrides.schemas
67+
.sorted(by: { $0.key < $1.key })
68+
.map { "\"\($0.key)\"->\"\($0.value)\"" }.joined(separator: ", "))
6469
- Feature flags: \(resolvedFeatureFlags.isEmpty ? "<none>" : resolvedFeatureFlags.map(\.rawValue).joined(separator: ", "))
6570
- Output file names: \(sortedModes.map(\.outputFileName).joined(separator: ", "))
6671
- Output directory: \(outputDirectory.path)

Sources/swift-openapi-generator/GenerateOptions.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,14 @@ extension _GenerateOptions {
107107
/// - Returns: The name overrides requested by the user
108108
func resolvedNameOverrides(_ config: _UserConfig?) -> [String: String] { config?.nameOverrides ?? [:] }
109109

110+
/// Returns the type overrides requested by the user.
111+
/// - Parameter config: The configuration specified by the user.
112+
/// - Returns: The type overrides requested by the user.
113+
func resolvedTypeOverrides(_ config: _UserConfig?) -> TypeOverrides {
114+
guard let schemaOverrides = config?.typeOverrides?.schemas, !schemaOverrides.isEmpty else { return .init() }
115+
return TypeOverrides(schemas: schemaOverrides)
116+
}
117+
110118
/// Returns a list of the feature flags requested by the user.
111119
/// - Parameter config: The configuration specified by the user.
112120
/// - Returns: A set of feature flags requested by the user.

Sources/swift-openapi-generator/UserConfig.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ struct _UserConfig: Codable {
4545
/// Any names not included use the `namingStrategy` to compute a Swift name.
4646
var nameOverrides: [String: String]?
4747

48+
/// A dictionary of overrides for replacing the types of generated with manually provided types
49+
var typeOverrides: TypeOverrides?
50+
4851
/// A set of features to explicitly enable.
4952
var featureFlags: FeatureFlags?
5053

@@ -59,6 +62,13 @@ struct _UserConfig: Codable {
5962
case filter
6063
case namingStrategy
6164
case nameOverrides
65+
case typeOverrides
6266
case featureFlags
6367
}
68+
69+
/// A container of type overrides.
70+
struct TypeOverrides: Codable {
71+
/// A dictionary of overrides for replacing the types generated from schemas with manually provided types.
72+
var schemas: [String: String]?
73+
}
6474
}

0 commit comments

Comments
 (0)