10000 Allow substituting types (#764) · apple/swift-openapi-generator@eb66fa9 · GitHub
[go: up one dir, main page]

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 <simon.anreiter@wegfinder.at> Co-authored-by: Honza Dvorsky <honza@apple.com>
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(

0 commit comments

Comments
 (0)
0