Skip to content

Commit 4bd4904

Browse files
committed
PR feedback
1 parent acb471e commit 4bd4904

File tree

14 files changed

+303
-295
lines changed

14 files changed

+303
-295
lines changed

Sources/_OpenAPIGeneratorCore/Config.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,7 @@ public struct Config: Sendable {
4141
public var filter: DocumentFilter?
4242

4343
public var namingStrategy: NamingStrategy?
44-
4544
public var nameOverrides: [String: String]?
46-
4745
/// Additional pre-release features to enable.
4846
public var featureFlags: FeatureFlags
4947

@@ -53,6 +51,8 @@ public struct Config: Sendable {
5351
/// - access: The access modifier to use for generated declarations.
5452
/// - additionalImports: Additional imports to add to each generated file.
5553
/// - filter: Filter to apply to the OpenAPI document before generation.
54+
/// - namingStrategy: The OpenAPI -> Swift name conversion strategy.
55+
/// - nameOverrides: A map of OpenAPI -> Swift name overrides.
5656
/// - featureFlags: Additional pre-release features to enable.
5757
public init(
5858
mode: GeneratorMode,

Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/SwiftSafeNames.swift

Lines changed: 87 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,8 @@ import Foundation
1515

1616
struct SwiftNameOptions: OptionSet {
1717
let rawValue: Int32
18-
1918
static let none = SwiftNameOptions([])
20-
2119
static let capitalize = SwiftNameOptions(rawValue: 1 << 0)
22-
2320
static let all: SwiftNameOptions = [.capitalize]
2421
}
2522

@@ -36,7 +33,7 @@ extension String {
3633
///
3734
/// In addition to replacing illegal characters, it also
3835
/// ensures that the identifier starts with a letter and not a number.
39-
func safeForSwiftCode_defensive(options: SwiftNameOptions) -> String {
36+
func safeForSwiftCode_defensive(options: SwiftNameOptions) -> String {
4037
guard !isEmpty else { return "_empty" }
4138

4239
let firstCharSet: CharacterSet = .letters.union(.init(charactersIn: "_"))
@@ -84,10 +81,7 @@ extension String {
8481
/// matching `safeForSwiftCode_defensive`.
8582
func safeForSwiftCode_idiomatic(options: SwiftNameOptions) -> String {
8683
let capitalize = options.contains(.capitalize)
87-
if isEmpty {
88-
return capitalize ? "_Empty_" : "_empty_"
89-
}
90-
84+
if isEmpty { return capitalize ? "_Empty_" : "_empty_" }
9185
// Detect cases like HELLO_WORLD, sometimes used for constants.
9286
let isAllUppercase = allSatisfy {
9387
// Must check that no characters are lowercased, as non-letter characters
@@ -96,22 +90,24 @@ extension String {
9690
}
9791

9892
// 1. Leave leading underscores as-are
99-
// 2. In the middle: word separators: ["_", "-", <space>] -> remove and capitalize next word
100-
// 3. In the middle: period: [".", "/"] -> replace with "_"
93+
// 2. In the middle: word separators: ["_", "-", "/", <space>] -> remove and capitalize next word
94+
// 3. In the middle: period: ["."] -> replace with "_"
10195
// 4. In the middle: drop ["{", "}"] -> replace with ""
102-
96+
10397
var buffer: [Character] = []
10498
buffer.reserveCapacity(count)
105-
106-
enum State {
99+
enum State: Equatable {
107100
case modifying
108-
case fallback
109101
case preFirstWord
102+
struct AccumulatingFirstWordContext: Equatable { var isAccumulatingInitialUppercase: Bool }
103+
case accumulatingFirstWord(AccumulatingFirstWordContext)
110104
case accumulatingWord
111105
case waitingForWordStarter
106+
case fallback
112107
}
113108
var state: State = .preFirstWord
114-
for char in self {
109+
for index in self[...].indices {
110+
let char = self[index]
115111
let _state = state
116112
state = .modifying
117113
switch _state {
@@ -124,30 +120,93 @@ extension String {
124120
// Prefix with an underscore if the first character is a number.
125121
buffer.append("_")
126122
buffer.append(char)
127-
state = .accumulatingWord
123+
state = .accumulatingFirstWord(.init(isAccumulatingInitialUppercase: false))
128124
} else if char.isLetter {
129125
// First character in the identifier.
130126
buffer.append(contentsOf: capitalize ? char.uppercased() : char.lowercased())
131-
state = .accumulatingWord
127+
state = .accumulatingFirstWord(
128+
.init(isAccumulatingInitialUppercase: !capitalize && char.isUppercase)
129+
)
132130
} else {
133131
// Illegal character, fall back to the defensive strategy.
134132
state = .fallback
135133
}
136-
case .accumulatingWord:
134+
case .accumulatingFirstWord(var context):
137135
if char.isLetter || char.isNumber {
138136
if isAllUppercase {
139137
buffer.append(contentsOf: char.lowercased())
138+
} else if context.isAccumulatingInitialUppercase {
139+
// Example: "HTTPProxy"/"HTTP_Proxy"/"HTTP_proxy"" should all
140+
// become "httpProxy" when capitalize == false.
141+
// This means treating the first word differently.
142+
// Here we are on the second or later character of the first word (the first
143+
// character is handled in `.preFirstWord`.
144+
// If the first character was uppercase, and we're in lowercasing mode,
145+
// we need to lowercase every consequtive uppercase character while there's
146+
// another uppercase character after it.
147+
if char.isLowercase {
148+
// No accumulating anymore, just append it and turn off accumulation.
149+
buffer.append(char)
150+
context.isAccumulatingInitialUppercase = false
151+
} else {
152+
let suffix = suffix(from: self.index(after: index))
153+
if suffix.count >= 2 {
154+
let next = suffix.first!
155+
let secondNext = suffix.dropFirst().first!
156+
if next.isUppercase && secondNext.isLowercase {
157+
// Finished lowercasing.
158+
context.isAccumulatingInitialUppercase = false
159+
buffer.append(contentsOf: char.lowercased())
160+
} else if Self.wordSeparators.contains(next) {
161+
// Finished lowercasing.
162+
context.isAccumulatingInitialUppercase = false
163+
buffer.append(contentsOf: char.lowercased())
164+
} else if next.isUppercase {
165+
// Keep lowercasing.
166+
buffer.append(contentsOf: char.lowercased())
167+
} else {
168+
// Append as-is, stop accumulating.
169+
context.isAccumulatingInitialUppercase = false
170+
buffer.append(char)
171+
}
172+
} else {
173+
// This is the last or second to last character,
174+
// since we were accumulating capitals, lowercase it.
175+
buffer.append(contentsOf: char.lowercased())
176+
context.isAccumulatingInitialUppercase = false
177+
}
178+
}
140179
} else {
141180
buffer.append(char)
142181
}
182+
state = .accumulatingFirstWord(context)
183+
} else if ["_", "-", " ", "/"].contains(char) {
184+
// In the middle of an identifier, these are considered
185+
// word separators, so we remove the character and end the current word.
186+
state = .waitingForWordStarter
187+
} else if ["."].contains(char) {
188+
// In the middle of an identifier, these get replaced with
189+
// an underscore, but continue the current word.
190+
buffer.append("_")
191+
state = .accumulatingFirstWord(.init(isAccumulatingInitialUppercase: false))
192+
} else if ["{", "}"].contains(char) {
193+
// In the middle of an identifier, curly braces are dropped.
194+
state = .accumulatingFirstWord(.init(isAccumulatingInitialUppercase: false))
195+
} else {
196+
// Illegal character, fall back to the defensive strategy.
197+
state = .fallback
198+
}
199+
case .accumulatingWord:
200+
if char.isLetter || char.isNumber {
201+
if isAllUppercase { buffer.append(contentsOf: char.lowercased()) } else { buffer.append(char) }
143202
state = .accumulatingWord
144-
} else if ["_", "-", " "].contains(char) {
145-
// In the middle of an identifier, dashes, underscores, and spaces are considered
203+
} else if Self.wordSeparators.contains(char) {
204+
// In the middle of an identifier, these are considered
146205
// word separators, so we remove the character and end the current word.
147206
state = .waitingForWordStarter
148-
} else if [".", "/"].contains(char) {
149-
// In the middle of an identifier, a period or a slash gets replaced with
150-
// an underscore, but continues the current word.
207+
} else if ["."].contains(char) {
208+
// In the middle of an identifier, these get replaced with
209+
// an underscore, but continue the current word.
151210
buffer.append("_")
152211
state = .accumulatingWord
153212
} else if ["{", "}"].contains(char) {
@@ -170,24 +229,18 @@ extension String {
170229
// Illegal character, fall back to the defensive strategy.
171230
state = .fallback
172231
}
173-
case .modifying, .fallback:
174-
preconditionFailure("Logic error in \(#function), string: '\(self)'")
232+
case .modifying, .fallback: preconditionFailure("Logic error in \(#function), string: '\(self)'")
175233
}
176234
precondition(state != .modifying, "Logic error in \(#function), string: '\(self)'")
177-
if case .fallback = state {
178-
return safeForSwiftCode_defensive(options: options)
179-
}
180-
}
181-
if buffer.isEmpty || state == .preFirstWord {
182-
return safeForSwiftCode_defensive(options: options)
235+
if case .fallback = state { return safeForSwiftCode_defensive(options: options) }
183236
}
237+
if buffer.isEmpty || state == .preFirstWord { return safeForSwiftCode_defensive(options: options) }
184238
// Check for keywords
185239
let newString = String(buffer)
186-
if Self.keywords.contains(newString) {
187-
return "_\(newString)"
188-
}
240+
if Self.keywords.contains(newString) { return "_\(newString)" }
189241
return newString
190242
}
243+
private static let wordSeparators: Set<Character> = ["_", "-", " ", "/"]
191244

192245
/// A list of Swift keywords.
193246
///

Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,14 @@ extension FileTranslator {
5050
var context: TranslatorContext {
5151
let asSwiftSafeName: (String, SwiftNameOptions) -> String
5252
switch config.namingStrategy {
53-
case .defensive, .none:
54-
asSwiftSafeName = { $0.safeForSwiftCode_defensive(options: $1) }
55-
case .idiomatic:
56-
asSwiftSafeName = { $0.safeForSwiftCode_idiomatic(options: $1) }
53+
case .defensive, .none: asSwiftSafeName = { $0.safeForSwiftCode_defensive(options: $1) }
54+
case .idiomatic: asSwiftSafeName = { $0.safeForSwiftCode_idiomatic(options: $1) }
5755
}
5856
let overrides = config.nameOverrides ?? [:]
59-
return TranslatorContext(
60-
asSwiftSafeName: { name, options in
61-
if let override = overrides[name] {
62-
return override
63-
}
64-
return asSwiftSafeName(name, options)
65-
}
66-
)
57+
return TranslatorContext(asSwiftSafeName: { name, options in
58+
if let override = overrides[name] { return override }
59+
return asSwiftSafeName(name, options)
60+
})
6761
}
6862
}
6963

Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ extension FileTranslator {
305305
return nil
306306
}
307307
let finalContentTypeSource: MultipartPartInfo.ContentTypeSource
308-
if let encoding, let contentType = encoding.contentType {
308+
if let contentTypes = encoding?.contentTypes, let contentType = contentTypes.first, contentTypes.count == 1 {
309309
finalContentTypeSource = try .explicit(contentType.asGeneratorContentType)
310310
} else {
311311
finalContentTypeSource = candidateSource

Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,8 @@ struct TypeAssigner {
495495
{
496496
typeNameForComponents()
497497
.appending(
498-
swiftComponent: context.asSwiftSafeName(componentType.openAPIComponentsKey, .capitalize).uppercasingFirstLetter,
498+
swiftComponent: context.asSwiftSafeName(componentType.openAPIComponentsKey, .capitalize)
499+
.uppercasingFirstLetter,
499500
jsonComponent: componentType.openAPIComponentsKey
500501
)
501502
}

Sources/swift-openapi-generator/Extensions.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@ import Yams
1818

1919
#if $RetroactiveAttribute
2020
extension URL: @retroactive ExpressibleByArgument {}
21+
#if compiler(<6.0)
2122
extension GeneratorMode: @retroactive ExpressibleByArgument {}
2223
extension FeatureFlag: @retroactive ExpressibleByArgument {}
2324
#else
25+
extension GeneratorMode: ExpressibleByArgument {}
26+
extension FeatureFlag: ExpressibleByArgument {}
27+
#endif
28+
#else
2429
extension URL: ExpressibleByArgument {}
2530
extension GeneratorMode: ExpressibleByArgument {}
2631
extension FeatureFlag: ExpressibleByArgument {}

Sources/swift-openapi-generator/FilterCommand.swift

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,20 +81,15 @@ private func timing<Output>(_ title: String, _ operation: @autoclosure () throws
8181
}
8282

8383
private let sampleConfig = _UserConfig(
84-
generate: [
85-
.types,
86-
.client
87-
],
84+
generate: [.types, .client],
8885
filter: DocumentFilter(
8986
operations: ["getGreeting"],
9087
tags: ["greetings"],
9188
paths: ["/greeting"],
9289
schemas: ["Greeting"]
9390
),
9491
namingStrategy: .idiomatic,
95-
nameOverrides: [
96-
"SPECIAL_NAME": "SpecialName"
97-
]
92+
nameOverrides: ["SPECIAL_NAME": "SpecialName"]
9893
)
9994

10095
enum OutputFormat: String, ExpressibleByArgument {

Sources/swift-openapi-generator/GenerateOptions.swift

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,8 @@ extension _GenerateOptions {
7575
return []
7676
}
7777

78-
func resolvedNamingStrategy(_ config: _UserConfig?) -> NamingStrategy {
79-
config?.namingStrategy ?? .defensive
80-
}
81-
82-
func resolvedNameOverrides(_ config: _UserConfig?) -> [String: String] {
83-
config?.nameOverrides ?? [:]
84-
}
85-
78+
func resolvedNamingStrategy(_ config: _UserConfig?) -> NamingStrategy { config?.namingStrategy ?? .defensive }
79+
func resolvedNameOverrides(_ config: _UserConfig?) -> [String: String] { config?.nameOverrides ?? [:] }
8680
/// Returns a list of the feature flags requested by the user.
8781
/// - Parameter config: The configuration specified by the user.
8882
/// - Returns: A set of feature flags requested by the user.

Sources/swift-openapi-generator/UserConfig.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ struct _UserConfig: Codable {
3737
var namingStrategy: NamingStrategy?
3838

3939
/// A dictionary of name overrides for generated types and members.
40-
///
40+
///
4141
/// Any names not included use the `namingStrategy` to compute a Swift name.
4242
var nameOverrides: [String: String]?
4343

0 commit comments

Comments
 (0)