From 140f5c0919fecaebbb189a0762f530259ad0c957 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 13 Nov 2024 18:26:53 +0100 Subject: [PATCH] [Experiment] Optimistic naming strategy as opt-in --- Sources/_OpenAPIGeneratorCore/Config.swift | 13 + .../translateClientMethod.swift | 2 +- .../CommonTranslations/SwiftSafeNames.swift | 328 +++++++++++++++++- .../translateAllAnyOneOf.swift | 2 +- .../translateStringEnum.swift | 4 +- .../CommonTypes/DiscriminatorExtensions.swift | 2 +- .../CommonTypes/StructBlueprint.swift | 2 +- .../Translator/FileTranslator.swift | 21 +- .../Multipart/MultipartContentInspector.swift | 2 +- .../Multipart/translateMultipart.swift | 6 +- .../Operations/OperationDescription.swift | 13 +- .../Parameters/TypedParameter.swift | 2 +- .../Responses/TypedResponseHeader.swift | 2 +- .../translateServerMethod.swift | 3 +- .../TypeAssignment/TypeAssigner.swift | 18 +- .../translateServersVariables.swift | 10 +- .../FilterCommand.swift | 11 +- .../GenerateOptions+runGenerator.swift | 8 + .../GenerateOptions.swift | 16 +- .../swift-openapi-generator/UserConfig.swift | 10 + .../Extensions/Test_String.swift | 70 ---- .../Extensions/Test_SwiftSafeNames.swift | 127 +++++++ .../TestUtilities.swift | 34 +- .../Test_OperationDescription.swift | 2 +- .../TypeAssignment/Test_TypeAssigner.swift | 2 +- 25 files changed, 589 insertions(+), 121 deletions(-) delete mode 100644 Tests/OpenAPIGeneratorCoreTests/Extensions/Test_String.swift create mode 100644 Tests/OpenAPIGeneratorCoreTests/Extensions/Test_SwiftSafeNames.swift diff --git a/Sources/_OpenAPIGeneratorCore/Config.swift b/Sources/_OpenAPIGeneratorCore/Config.swift index 6172b709..cabba9dc 100644 --- a/Sources/_OpenAPIGeneratorCore/Config.swift +++ b/Sources/_OpenAPIGeneratorCore/Config.swift @@ -12,6 +12,11 @@ // //===----------------------------------------------------------------------===// +public enum NamingStrategy: String, Sendable, Codable, Equatable { + case defensive + case optimistic +} + /// A structure that contains configuration options for a single execution /// of the generator pipeline run. /// @@ -35,6 +40,10 @@ public struct Config: Sendable { /// Filter to apply to the OpenAPI document before generation. public var filter: DocumentFilter? + public var namingStrategy: NamingStrategy? + + public var nameOverrides: [String: String]? + /// Additional pre-release features to enable. public var featureFlags: FeatureFlags @@ -50,12 +59,16 @@ public struct Config: Sendable { access: AccessModifier, additionalImports: [String] = [], filter: DocumentFilter? = nil, + namingStrategy: NamingStrategy? = nil, + nameOverrides: [String: String]? = nil, featureFlags: FeatureFlags = [] ) { self.mode = mode self.access = access self.additionalImports = additionalImports self.filter = filter + self.namingStrategy = namingStrategy + self.nameOverrides = nameOverrides self.featureFlags = featureFlags } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift index 853c373e..0c217fbd 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/translateClientMethod.swift @@ -145,7 +145,7 @@ extension ClientFileTranslator { func translateClientMethod(_ description: OperationDescription) throws -> Declaration { let operationTypeExpr = Expression.identifierType(.member(Constants.Operations.namespace)) - .dot(description.methodName) + .dot(description.operationTypeName) let operationArg = FunctionArgumentDescription(label: "forOperation", expression: operationTypeExpr.dot("id")) let inputArg = FunctionArgumentDescription( diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/SwiftSafeNames.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/SwiftSafeNames.swift index 9a52bc04..8494b08e 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/SwiftSafeNames.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/SwiftSafeNames.swift @@ -13,6 +13,16 @@ //===----------------------------------------------------------------------===// import Foundation +struct SwiftNameOptions: OptionSet { + let rawValue: Int32 + + static let none = SwiftNameOptions([]) + + static let capitalize = SwiftNameOptions(rawValue: 1 << 0) + + static let all: SwiftNameOptions = [.capitalize] +} + extension String { /// Returns a string sanitized to be usable as a Swift identifier. @@ -26,7 +36,7 @@ extension String { /// /// In addition to replacing illegal characters, it also /// ensures that the identifier starts with a letter and not a number. - var safeForSwiftCode: String { + func safeForSwiftCode_defensive(options: SwiftNameOptions) -> String { guard !isEmpty else { return "_empty" } let firstCharSet: CharacterSet = .letters.union(.init(charactersIn: "_")) @@ -67,6 +77,200 @@ extension String { return "_\(validString)" } + /// Returns a string sanitized to be usable as a Swift identifier, and tries to produce UpperCamelCase + /// or lowerCamelCase string, the casing is controlled using the provided options. + /// + /// If the string contains any illegal characters, falls back to the behavior + /// matching `safeForSwiftCode_defensive`. + func safeForSwiftCode_optimistic(options: SwiftNameOptions) -> String { + let capitalize = options.contains(.capitalize) + if isEmpty { + return capitalize ? "_Empty_" : "_empty_" + } + + // Detect cases like HELLO_WORLD, sometimes used for constants. + let isAllUppercase = allSatisfy { + // Must check that no characters are lowercased, as non-letter characters + // don't return `true` to `isUppercase`. + !$0.isLowercase + } + + // 1. Leave leading underscores as-are + // 2. In the middle: word separators: ["_", "-", ] -> remove and capitalize next word + // 3. In the middle: period: ["."] -> replace with "_" + + var buffer: [Character] = [] + buffer.reserveCapacity(count) + + enum State { + case modifying + case preFirstWord + case accumulatingWord + case waitingForWordStarter + } + var state: State = .preFirstWord + for char in self { + let _state = state + state = .modifying + switch _state { + case .preFirstWord: + if char == "_" { + // Leading underscores are kept. + buffer.append(char) + state = .preFirstWord + } else if char.isNumber { + // Prefix with an underscore if the first character is a number. + buffer.append("_") + buffer.append(char) + state = .accumulatingWord + } else if char.isLetter { + // First character in the identifier. + buffer.append(contentsOf: capitalize ? char.uppercased() : char.lowercased()) + state = .accumulatingWord + } else { + // Illegal character, fall back to the defensive strategy. + return safeForSwiftCode_defensive(options: options) + } + case .accumulatingWord: + if char.isLetter || char.isNumber { + if isAllUppercase { + buffer.append(contentsOf: char.lowercased()) + } else { + buffer.append(char) + } + state = .accumulatingWord + } else if char == "_" || char == "-" || char == " " { + // In the middle of an identifier, dashes, underscores, and spaces are considered + // word separators, so we remove the character and end the current word. + state = .waitingForWordStarter + } else if char == "." { + // In the middle of an identifier, a period gets replaced with an underscore, but continues + // the current word. + buffer.append("_") + state = .accumulatingWord + } else { + // Illegal character, fall back to the defensive strategy. + return safeForSwiftCode_defensive(options: options) + } + case .waitingForWordStarter: + if char == "_" || char == "-" { + // Between words, just drop dashes, underscores, and spaces, since + // we're already between words anyway. + state = .waitingForWordStarter + } else if char.isLetter || char.isNumber { + // Starting a new word in the middle of the identifier. + buffer.append(contentsOf: char.uppercased()) + state = .accumulatingWord + } else { + // Illegal character, fall back to the defensive strategy. + return safeForSwiftCode_defensive(options: options) + } + case .modifying: + preconditionFailure("Logic error in \(#function), string: '\(self)'") + } + precondition(state != .modifying, "Logic error in \(#function), string: '\(self)'") + } + if buffer.isEmpty || state == .preFirstWord { + return safeForSwiftCode_defensive(options: options) + } + // Check for keywords + let newString = String(buffer) + if Self.keywords.contains(newString) { + return "_\(newString)" + } + return newString + } + + private static let identifierHeadCharactersRanges: [ClosedRange] = { + // https://docs.swift.org/swift-book/documentation/the-swift-programming-language/lexicalstructure/#Identifiers + var ranges: [ClosedRange] = [] + // identifier-head → Upper- or lowercase letter A through Z + ranges.append("A"..."Z") + ranges.append("a"..."z") + // identifier-head → _ + ranges.append("_") + // identifier-head → U+00A8, U+00AA, U+00AD, U+00AF, U+00B2–U+00B5, or U+00B7–U+00BA + ranges.appendFromSet([0x00A8, 0x00AA, 0x00AD, 0x00AF]) + ranges.appendFromScalars(0x00B2...0x00B5) + ranges.appendFromScalars(0x00B7...0x00BA) + // identifier-head → U+00BC–U+00BE, U+00C0–U+00D6, U+00D8–U+00F6, or U+00F8–U+00FF + ranges.appendFromScalars(0x00BC...0x00BE) + ranges.appendFromScalars(0x00C0...0x00D6) + ranges.appendFromScalars(0x00D8...0x00F6) + ranges.appendFromScalars(0x00F8...0x00FF) + // identifier-head → U+0100–U+02FF, U+0370–U+167F, U+1681–U+180D, or U+180F–U+1DBF + ranges.appendFromScalars(0x0100...0x02FF) + ranges.appendFromScalars(0x0370...0x167F) + ranges.appendFromScalars(0x1681...0x180D) + ranges.appendFromScalars(0x180F...0x1DBF) + // identifier-head → U+1E00–U+1FFF + ranges.appendFromScalars(0x1E00...0x1FFF) + // identifier-head → U+200B–U+200D, U+202A–U+202E, U+203F–U+2040, U+2054, or U+2060–U+206F + ranges.appendFromScalars(0x200B...0x200D) + ranges.appendFromScalars(0x202A...0x202E) + ranges.appendFromScalars(0x203F...0x2040) + ranges.appendFromScalar(0x2054) + ranges.appendFromScalars(0x2060...0x206F) + // identifier-head → U+2070–U+20CF, U+2100–U+218F, U+2460–U+24FF, or U+2776–U+2793 + ranges.appendFromScalars(0x2070...0x20CF) + ranges.appendFromScalars(0x2100...0x218F) + ranges.appendFromScalars(0x2460...0x24FF) + ranges.appendFromScalars(0x2776...0x2793) + // identifier-head → U+2C00–U+2DFF or U+2E80–U+2FFF + ranges.appendFromScalars(0x2C00...0x2DFF) + ranges.appendFromScalars(0x2E80...0x2FFF) + // identifier-head → U+3004–U+3007, U+3021–U+302F, U+3031–U+303F, or U+3040–U+D7FF + ranges.appendFromScalars(0x3004...0x3007) + ranges.appendFromScalars(0x3021...0x302F) + ranges.appendFromScalars(0x3031...0x303F) + ranges.appendFromScalars(0x3040...0xD7FF) + // identifier-head → U+F900–U+FD3D, U+FD40–U+FDCF, U+FDF0–U+FE1F, or U+FE30–U+FE44 + ranges.appendFromScalars(0xF900...0xFD3D) + ranges.appendFromScalars(0xFD40...0xFDCF) + ranges.appendFromScalars(0xFDF0...0xFE1F) + ranges.appendFromScalars(0xFE30...0xFE44) + // identifier-head → U+FE47–U+FFFD + ranges.appendFromScalars(0xFE47...0xFFFD) + // identifier-head → U+10000–U+1FFFD, U+20000–U+2FFFD, U+30000–U+3FFFD, or U+40000–U+4FFFD + ranges.appendFromScalars(0x10000...0x1FFFD) + ranges.appendFromScalars(0x20000...0x2FFFD) + ranges.appendFromScalars(0x30000...0x3FFFD) + ranges.appendFromScalars(0x40000...0x4FFFD) + // identifier-head → U+50000–U+5FFFD, U+60000–U+6FFFD, U+70000–U+7FFFD, or U+80000–U+8FFFD + ranges.appendFromScalars(0x50000...0x5FFFD) + ranges.appendFromScalars(0x60000...0x6FFFD) + ranges.appendFromScalars(0x70000...0x7FFFD) + ranges.appendFromScalars(0x80000...0x8FFFD) + // identifier-head → U+90000–U+9FFFD, U+A0000–U+AFFFD, U+B0000–U+BFFFD, or U+C0000–U+CFFFD + ranges.appendFromScalars(0x90000...0x9FFFD) + ranges.appendFromScalars(0xA0000...0xAFFFD) + ranges.appendFromScalars(0xB0000...0xBFFFD) + ranges.appendFromScalars(0xC0000...0xCFFFD) + // identifier-head → U+D0000–U+DFFFD or U+E0000–U+EFFFD + ranges.appendFromScalars(0xD0000...0xDFFFD) + ranges.appendFromScalars(0xE0000...0xEFFFD) + return ranges + }() + + private static let identifierNonHeadCharactersRanges: [ClosedRange] = { + var ranges: [ClosedRange] = [] + // identifier-character → Digit 0 through 9 + ranges.append("0"..."9") + // identifier-character → U+0300–U+036F, U+1DC0–U+1DFF, U+20D0–U+20FF, or U+FE20–U+FE2F + ranges.appendFromScalars(0x0300...0x036F) + ranges.appendFromScalars(0x1DC0...0x1DFF) + ranges.appendFromScalars(0x20D0...0x20FF) + ranges.appendFromScalars(0xFE20...0xFE2F) + return ranges + }() + + private static let identifierCharactersRanges: [ClosedRange] = { + var ranges: [ClosedRange] = [] + ranges.append(contentsOf: identifierHeadCharactersRanges) + ranges.append(contentsOf: identifierNonHeadCharactersRanges) + return ranges + }() + /// A list of Swift keywords. /// /// Copied from SwiftSyntax/TokenKind.swift @@ -87,3 +291,125 @@ extension String { "\\": "bsol", "]": "rbrack", "^": "hat", "`": "grave", "{": "lcub", "|": "verbar", "}": "rcub", "~": "tilde", ] } + +extension [ClosedRange] { + func contains(_ char: Character) -> Bool { + // TODO: This could be optimized if we create a data structure of sorted ranges and use binary search. + for range in self { + if range.contains(char) { + return true + } + } + return false + } + + mutating func appendFromScalar(_ scalar: Int) { + append(Character(UnicodeScalar(scalar)!)...Character(UnicodeScalar(scalar)!)) + } + + mutating func appendFromSet(_ set: Set) { + append(contentsOf: ClosedRange.fromSet(set)) + } + + mutating func appendFromScalars(_ range: ClosedRange) { + append(Character(UnicodeScalar(range.lowerBound)!)...Character(UnicodeScalar(range.upperBound)!)) + } +} + +extension ClosedRange: @retroactive ExpressibleByUnicodeScalarLiteral where Bound == Character { + public typealias UnicodeScalarLiteralType = Character + public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) { + let char = Character(unicodeScalarLiteral: value) + self = char...char + } +} + +extension ClosedRange where Bound == Character { + static func fromSet(_ set: Set) -> [Self] { + set.map { + Character(UnicodeScalar($0)!)...Character(UnicodeScalar($0)!) + } + } +} + +fileprivate extension Set where Element == Character { + mutating func insert(charactersInString string: String) { + for character in string { + insert(character) + } + } + + mutating func insert(allFrom lowerBound: Character, upTo upperBound: Character) { + for byte in lowerBound.asciiValue!...upperBound.asciiValue! { + insert(Character(UnicodeScalar(byte))) + } + } + + mutating func insert(scalarWithCode code: Int) { + insert(Character(UnicodeScalar(code)!)) + } + + mutating func insert(scalarInSet set: Set) { + for code in set { + insert(scalarWithCode: code) + } + } + + mutating func insert(scalarsInRangeFrom lowerBound: Int, upTo upperBound: Int) { + for code in lowerBound...upperBound { + insert(scalarWithCode: code) + } + } +} + +@available(*, deprecated) +fileprivate extension UInt8 { + + var isUppercaseLetter: Bool { + (0x41...0x5a).contains(self) + } + + var isLowercaseLetter: Bool { + (0x61...0x7a).contains(self) + } + + var isLetter: Bool { + isUppercaseLetter || isLowercaseLetter + } + + var isNumber: Bool { + (0x30...0x39).contains(self) + } + + var isLowercaseLetterOrNumber: Bool { + isLowercaseLetter || isNumber + } + + var isAlphanumeric: Bool { + isLetter || isNumber + } + + var asUppercase: UInt8 { + if isUppercaseLetter || isNumber { + return self + } + if isLowercaseLetter { + return self - 0x20 + } + preconditionFailure("Cannot uppercase not a letter or number") + } + + var asLowercase: UInt8 { + if isLowercaseLetter || isNumber { + return self + } + if isUppercaseLetter { + return self + 0x20 + } + preconditionFailure("Cannot lowercase not a letter or number") + } + + var isWordSeparator: Bool { + self == 0x2d /* - */ || self == 0x5f /* _ */ + } +} diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift index 37ac8830..e77493ee 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateAllAnyOneOf.swift @@ -209,7 +209,7 @@ extension TypesFileTranslator { let decoder: Declaration if let discriminator { let originalName = discriminator.propertyName - let swiftName = context.asSwiftSafeName(originalName) + let swiftName = context.asSwiftSafeName(originalName, .none) codingKeysDecls = [ .enum( accessModifier: config.access, diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift index 9add9482..7296626f 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTranslations/translateStringEnum.swift @@ -49,11 +49,11 @@ extension FileTranslator { // In nullable enum schemas, empty strings are parsed as Void. // This is unlikely to be fixed, so handling that case here. // https://github.com/apple/swift-openapi-generator/issues/118 - if isNullable && anyValue is Void { return (context.asSwiftSafeName(""), .string("")) } + if isNullable && anyValue is Void { return (context.asSwiftSafeName("", .none), .string("")) } guard let rawValue = anyValue as? String else { throw GenericError(message: "Disallowed value for a string enum '\(typeName)': \(anyValue)") } - let caseName = context.asSwiftSafeName(rawValue) + let caseName = context.asSwiftSafeName(rawValue, .none) return (caseName, .string(rawValue)) case .integer: let rawValue: Int diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/DiscriminatorExtensions.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/DiscriminatorExtensions.swift index 1bc1cd7f..922542d4 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/DiscriminatorExtensions.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/DiscriminatorExtensions.swift @@ -80,7 +80,7 @@ extension FileTranslator { /// - Parameter type: The `OneOfMappedType` for which to determine the case name. /// - Returns: A string representing the safe Swift name for the specified `OneOfMappedType`. func safeSwiftNameForOneOfMappedType(_ type: OneOfMappedType) -> String { - context.asSwiftSafeName(type.rawNames[0]) + context.asSwiftSafeName(type.rawNames[0], .capitalize) } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/StructBlueprint.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/StructBlueprint.swift index 9eef5d6d..c295a5bd 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/StructBlueprint.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/StructBlueprint.swift @@ -153,7 +153,7 @@ struct PropertyBlueprint { extension PropertyBlueprint { /// A name that is verified to be a valid Swift identifier. - var swiftSafeName: String { context.asSwiftSafeName(originalName) } + var swiftSafeName: String { context.asSwiftSafeName(originalName, .none) } /// The JSON path to the property. /// diff --git a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift index 4f246521..15bada2a 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift @@ -47,7 +47,24 @@ protocol FileTranslator { extension FileTranslator { /// A new context from the file translator. - var context: TranslatorContext { TranslatorContext(asSwiftSafeName: { $0.safeForSwiftCode }) } + var context: TranslatorContext { + let asSwiftSafeName: (String, SwiftNameOptions) -> String + switch config.namingStrategy { + case .defensive, .none: + asSwiftSafeName = { $0.safeForSwiftCode_defensive(options: $1) } + case .optimistic: + asSwiftSafeName = { $0.safeForSwiftCode_optimistic(options: $1) } + } + let overrides = config.nameOverrides ?? [:] + return TranslatorContext( + asSwiftSafeName: { name, options in + if let override = overrides[name] { + return override + } + return asSwiftSafeName(name, options) + } + ) + } } /// A set of configuration values for concrete file translators. @@ -57,5 +74,5 @@ struct TranslatorContext { /// /// - Parameter string: The string to convert to be safe for Swift. /// - Returns: A Swift-safe version of the input string. - var asSwiftSafeName: (String) -> String + var asSwiftSafeName: (String, SwiftNameOptions) -> String } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift index f5a83882..f69692de 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/MultipartContentInspector.swift @@ -120,7 +120,7 @@ extension FileTranslator { } var parts: [MultipartSchemaTypedContent] = try topLevelObject.properties.compactMap { (key, value) -> MultipartSchemaTypedContent? in - let swiftSafeName = context.asSwiftSafeName(key) + let swiftSafeName = context.asSwiftSafeName(key, .capitalize) let typeName = typeName.appending( swiftComponent: swiftSafeName + Constants.Global.inlineTypeSuffix, jsonComponent: key diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/translateMultipart.swift b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/translateMultipart.swift index b288ff1a..21163449 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Multipart/translateMultipart.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Multipart/translateMultipart.swift @@ -137,7 +137,7 @@ extension TypesFileTranslator { switch part { case .documentedTyped(let documentedPart): let caseDecl: Declaration = .enumCase( - name: context.asSwiftSafeName(documentedPart.originalName), + name: context.asSwiftSafeName(documentedPart.originalName, .none), kind: .nameWithAssociatedValues([.init(type: .init(part.wrapperTypeUsage))]) ) let decl = try translateMultipartPartContent( @@ -404,7 +404,7 @@ extension FileTranslator { switch part { case .documentedTyped(let part): let originalName = part.originalName - let identifier = context.asSwiftSafeName(originalName) + let identifier = context.asSwiftSafeName(originalName, .none) let contentType = part.partInfo.contentType let partTypeName = part.typeName let schema = part.schema @@ -613,7 +613,7 @@ extension FileTranslator { switch part { case .documentedTyped(let part): let originalName = part.originalName - let identifier = context.asSwiftSafeName(originalName) + let identifier = context.asSwiftSafeName(originalName, .none) let contentType = part.partInfo.contentType let headersTypeName = part.typeName.appending( swiftComponent: Constants.Operation.Output.Payload.Headers.typeName, diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift b/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift index 2a72252b..bceb302c 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Operations/OperationDescription.swift @@ -83,7 +83,14 @@ extension OperationDescription { /// Uses the `operationID` value in the OpenAPI operation, if one was /// specified. Otherwise, computes a unique name from the operation's /// path and HTTP method. - var methodName: String { context.asSwiftSafeName(operationID) } + var methodName: String { context.asSwiftSafeName(operationID, .none) } + + /// Returns a Swift-safe type name for the operation. + /// + /// Uses the `operationID` value in the OpenAPI operation, if one was + /// specified. Otherwise, computes a unique name from the operation's + /// path and HTTP method. + var operationTypeName: String { context.asSwiftSafeName(operationID, .capitalize) } /// Returns the identifier for the operation. /// @@ -103,7 +110,7 @@ extension OperationDescription { .init( components: [.root, .init(swift: Constants.Operations.namespace, json: "paths")] + path.components.map { .init(swift: nil, json: $0) } + [ - .init(swift: methodName, json: httpMethod.rawValue) + .init(swift: operationTypeName, json: httpMethod.rawValue) ] ) } @@ -292,7 +299,7 @@ extension OperationDescription { } let newPath = OpenAPI.Path(newComponents, trailingSlash: path.trailingSlash) let names: [Expression] = orderedPathParameters.map { param in - .identifierPattern("input").dot("path").dot(context.asSwiftSafeName(param)) + .identifierPattern("input").dot("path").dot(context.asSwiftSafeName(param, .none)) } let arrayExpr: Expression = .literal(.array(names)) return (newPath.rawValue, arrayExpr) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift index e8eb0700..b5d8b425 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift @@ -48,7 +48,7 @@ extension TypedParameter { var name: String { parameter.name } /// The name of the parameter sanitized to be a valid Swift identifier. - var variableName: String { context.asSwiftSafeName(name) } + var variableName: String { context.asSwiftSafeName(name, .none) } /// A Boolean value that indicates whether the parameter must be specified /// when performing the OpenAPI operation. diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift b/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift index ad11fcbc..7c16ece6 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Responses/TypedResponseHeader.swift @@ -39,7 +39,7 @@ struct TypedResponseHeader { extension TypedResponseHeader { /// The name of the header sanitized to be a valid Swift identifier. - var variableName: String { context.asSwiftSafeName(name) } + var variableName: String { context.asSwiftSafeName(name, .none) } /// A Boolean value that indicates whether the response header can /// be omitted in the HTTP response. diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift index ca8aef41..4474d74b 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/translateServerMethod.swift @@ -129,9 +129,8 @@ extension ServerFileTranslator { func translateServerMethod(_ description: OperationDescription, serverUrlVariableName: String) throws -> ( registerCall: Expression, functionDecl: Declaration ) { - let operationTypeExpr = Expression.identifierType(.member(Constants.Operations.namespace)) - .dot(description.methodName) + .dot(description.operationTypeName) let operationArg = FunctionArgumentDescription(label: "forOperation", expression: operationTypeExpr.dot("id")) let requestArg = FunctionArgumentDescription(label: "request", expression: .identifierPattern("request")) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift index 7f7a46ff..3a7284f9 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeAssigner.swift @@ -59,7 +59,7 @@ struct TypeAssigner { /// - Returns: A Swift type name for the specified component type. func typeName(forComponentOriginallyNamed originalName: String, in location: TypeLocation) -> TypeName { typeName(forLocation: location) - .appending(swiftComponent: context.asSwiftSafeName(originalName), jsonComponent: originalName) + .appending(swiftComponent: context.asSwiftSafeName(originalName, .capitalize), jsonComponent: originalName) } /// Returns the type name for an OpenAPI-named component namespace. @@ -127,7 +127,7 @@ struct TypeAssigner { { multipartBodyElementTypeName = try typeName(for: ref) } else { - let swiftSafeName = context.asSwiftSafeName(hint) + let swiftSafeName = context.asSwiftSafeName(hint, .capitalize) multipartBodyElementTypeName = parent.appending( swiftComponent: swiftSafeName + Constants.Global.inlineTypeSuffix, jsonComponent: hint @@ -343,7 +343,7 @@ struct TypeAssigner { } return baseType.appending( - swiftComponent: context.asSwiftSafeName(originalName) + suffix, + swiftComponent: context.asSwiftSafeName(originalName, .capitalize) + suffix, jsonComponent: jsonReferenceComponentOverride ?? originalName ) .asUsage.withOptional(try typeMatcher.isOptional(schema, components: components)) @@ -406,7 +406,7 @@ struct TypeAssigner { of componentType: Component.Type ) -> TypeName { typeName(for: Component.self) - .appending(swiftComponent: context.asSwiftSafeName(key.rawValue), jsonComponent: key.rawValue) + .appending(swiftComponent: context.asSwiftSafeName(key.rawValue, .capitalize), jsonComponent: key.rawValue) } /// Returns a type name for a JSON reference. @@ -471,7 +471,7 @@ struct TypeAssigner { throw JSONReferenceParsingError.nonComponentPathsUnsupported(reference.name) } return typeName(for: componentType) - .appending(swiftComponent: context.asSwiftSafeName(name), jsonComponent: name) + .appending(swiftComponent: context.asSwiftSafeName(name, .capitalize), jsonComponent: name) } /// Returns a type name for the namespace for the specified component type. @@ -495,7 +495,7 @@ struct TypeAssigner { { typeNameForComponents() .appending( - swiftComponent: context.asSwiftSafeName(componentType.openAPIComponentsKey).uppercasingFirstLetter, + swiftComponent: context.asSwiftSafeName(componentType.openAPIComponentsKey, .capitalize).uppercasingFirstLetter, jsonComponent: componentType.openAPIComponentsKey ) } @@ -528,14 +528,14 @@ struct TypeAssigner { case "application/pdf": return "pdf" case "image/jpeg": return "jpeg" default: - let safedType = context.asSwiftSafeName(contentType.originallyCasedType) - let safedSubtype = context.asSwiftSafeName(contentType.originallyCasedSubtype) + let safedType = context.asSwiftSafeName(contentType.originallyCasedType, .none) + let safedSubtype = context.asSwiftSafeName(contentType.originallyCasedSubtype, .none) let prefix = "\(safedType)_\(safedSubtype)" let params = contentType.lowercasedParameterPairs guard !params.isEmpty else { return prefix } let safedParams = params.map { pair in - pair.split(separator: "=").map { context.asSwiftSafeName(String($0)) }.joined(separator: "_") + pair.split(separator: "=").map { context.asSwiftSafeName(String($0), .none) }.joined(separator: "_") } .joined(separator: "_") return prefix + "_" + safedParams diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServersVariables.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServersVariables.swift index c8b96ff0..4a67ea3e 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServersVariables.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServersVariables.swift @@ -84,7 +84,7 @@ extension TypesFileTranslator { /// Swift safe identifiers. init(key: String, variable: OpenAPI.Server.Variable, context: TranslatorContext) { self.key = key - swiftSafeKey = context.asSwiftSafeName(key) + swiftSafeKey = context.asSwiftSafeName(key, .none) self.variable = variable } @@ -164,8 +164,8 @@ extension TypesFileTranslator { context: TranslatorContext ) { self.key = key - swiftSafeKey = context.asSwiftSafeName(key) - enumName = context.asSwiftSafeName(key.localizedCapitalized) + swiftSafeKey = context.asSwiftSafeName(key, .none) + enumName = context.asSwiftSafeName(key.localizedCapitalized, .capitalize) self.variable = variable self.enumValues = enumValues self.context = context @@ -199,7 +199,7 @@ extension TypesFileTranslator { .init( label: swiftSafeKey, type: .member([enumName]), - defaultValue: .memberAccess(.dot(context.asSwiftSafeName(variable.default))) + defaultValue: .memberAccess(.dot(context.asSwiftSafeName(variable.default, .none))) ) } @@ -230,7 +230,7 @@ extension TypesFileTranslator { /// - Parameter name: The original name. /// - Returns: A declaration of an enum case. private func translateVariableCase(_ name: String) -> Declaration { - let caseName = context.asSwiftSafeName(name) + let caseName = context.asSwiftSafeName(name, .none) return .enumCase(name: caseName, kind: caseName == name ? .nameOnly : .nameWithRawValue(.string(name))) } } diff --git a/Sources/swift-openapi-generator/FilterCommand.swift b/Sources/swift-openapi-generator/FilterCommand.swift index 4eff9bc4..6066654b 100644 --- a/Sources/swift-openapi-generator/FilterCommand.swift +++ b/Sources/swift-openapi-generator/FilterCommand.swift @@ -81,13 +81,20 @@ private func timing(_ title: String, _ operation: @autoclosure () throws } private let sampleConfig = _UserConfig( - generate: [], + generate: [ + .types, + .client + ], filter: DocumentFilter( operations: ["getGreeting"], tags: ["greetings"], paths: ["/greeting"], schemas: ["Greeting"] - ) + ), + namingStrategy: .optimistic, + nameOverrides: [ + "SPECIAL_NAME": "SpecialName" + ] ) enum OutputFormat: String, ExpressibleByArgument { diff --git a/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift b/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift index 9f3fe83b..9cdf9f06 100644 --- a/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift +++ b/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift @@ -32,6 +32,8 @@ extension _GenerateOptions { let sortedModes = try resolvedModes(config) let resolvedAccessModifier = resolvedAccessModifier(config) ?? Config.defaultAccessModifier let resolvedAdditionalImports = resolvedAdditionalImports(config) + let resolvedNamingStragy = resolvedNamingStrategy(config) + let resolvedNameOverrides = resolvedNameOverrides(config) let resolvedFeatureFlags = resolvedFeatureFlags(config) let configs: [Config] = sortedModes.map { .init( @@ -39,6 +41,8 @@ extension _GenerateOptions { access: resolvedAccessModifier, additionalImports: resolvedAdditionalImports, filter: config?.filter, + namingStrategy: resolvedNamingStragy, + nameOverrides: resolvedNameOverrides, featureFlags: resolvedFeatureFlags ) } @@ -51,6 +55,10 @@ extension _GenerateOptions { - Configuration path: \(self.config?.path ?? "") - Generator modes: \(sortedModes.map(\.rawValue).joined(separator: ", ")) - Access modifier: \(resolvedAccessModifier.rawValue) + - Naming strategy: \(resolvedNamingStragy.rawValue) + - Name overrides: \(resolvedNameOverrides.isEmpty ? "" : resolvedNameOverrides + .sorted(by: { $0.key < $1.key }) + .map { "\"\($0.key)\"->\"\($0.value)\"" }.joined(separator: ", ")) - Feature flags: \(resolvedFeatureFlags.isEmpty ? "" : resolvedFeatureFlags.map(\.rawValue).joined(separator: ", ")) - Output file names: \(sortedModes.map(\.outputFileName).joined(separator: ", ")) - Output directory: \(outputDirectory.path) diff --git a/Sources/swift-openapi-generator/GenerateOptions.swift b/Sources/swift-openapi-generator/GenerateOptions.swift index 8b65db9c..53fbf419 100644 --- a/Sources/swift-openapi-generator/GenerateOptions.swift +++ b/Sources/swift-openapi-generator/GenerateOptions.swift @@ -47,14 +47,6 @@ extension AccessModifier: ExpressibleByArgument {} extension _GenerateOptions { - /// The user-provided user config, not yet resolved with defaults. - var resolvedUserConfig: _UserConfig { - get throws { - let config = try loadedConfig() - return try .init(generate: resolvedModes(config), additionalImports: resolvedAdditionalImports(config)) - } - } - /// Returns a list of the generator modes requested by the user. /// - Parameter config: The configuration specified by the user. /// - Returns: A list of generator modes requested by the user. @@ -83,6 +75,14 @@ extension _GenerateOptions { return [] } + func resolvedNamingStrategy(_ config: _UserConfig?) -> NamingStrategy { + config?.namingStrategy ?? .defensive + } + + func resolvedNameOverrides(_ config: _UserConfig?) -> [String: String] { + config?.nameOverrides ?? [:] + } + /// Returns a list of the feature flags requested by the user. /// - Parameter config: The configuration specified by the user. /// - Returns: A set of feature flags requested by the user. diff --git a/Sources/swift-openapi-generator/UserConfig.swift b/Sources/swift-openapi-generator/UserConfig.swift index 0ee210b7..db349e01 100644 --- a/Sources/swift-openapi-generator/UserConfig.swift +++ b/Sources/swift-openapi-generator/UserConfig.swift @@ -33,6 +33,14 @@ struct _UserConfig: Codable { /// Filter to apply to the OpenAPI document before generation. var filter: DocumentFilter? + /// The strategy to use for naming generated Swift types and members. + var namingStrategy: NamingStrategy? + + /// A dictionary of name overrides for generated types and members. + /// + /// Any names not included use the `namingStrategy` to compute a Swift name. + var nameOverrides: [String: String]? + /// A set of features to explicitly enable. var featureFlags: FeatureFlags? @@ -44,6 +52,8 @@ struct _UserConfig: Codable { case accessModifier case additionalImports case filter + case namingStrategy + case nameOverrides case featureFlags } } diff --git a/Tests/OpenAPIGeneratorCoreTests/Extensions/Test_String.swift b/Tests/OpenAPIGeneratorCoreTests/Extensions/Test_String.swift deleted file mode 100644 index 9d10b45c..00000000 --- a/Tests/OpenAPIGeneratorCoreTests/Extensions/Test_String.swift +++ /dev/null @@ -1,70 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftOpenAPIGenerator open source project -// -// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// -import XCTest -@testable import _OpenAPIGeneratorCore - -final class Test_String: Test_Core { - - func testAsSwiftSafeName() { - let cases: [(String, String)] = [ - // Simple - ("foo", "foo"), - - // Starts with a number - ("3foo", "_3foo"), - - // Keyword - ("default", "_default"), - - // Reserved name - ("Type", "_Type"), - - // Empty string - ("", "_empty"), - - // Special Char in middle - ("inv@lidName", "inv_commat_lidName"), - - // Special Char in first position - ("!nvalidName", "_excl_nvalidName"), - - // Special Char in last position - ("invalidNam?", "invalidNam_quest_"), - - // Valid underscore case - ("__user", "__user"), - - // Invalid underscore case - ("_", "_underscore_"), - - // Special character mixed with character not in map - ("$nake…", "_dollar_nake_x2026_"), - - // Only special character - ("$", "_dollar_"), - - // Only special character not in map - ("……", "_x2026__x2026_"), - - // Non Latin Characters - ("$مرحبا", "_dollar_مرحبا"), - - // Content type components - ("application", "application"), ("vendor1+json", "vendor1_plus_json"), - ] - let translator = makeTranslator() - let asSwiftSafeName: (String) -> String = translator.context.asSwiftSafeName - for (input, sanitized) in cases { XCTAssertEqual(asSwiftSafeName(input), sanitized) } - } -} diff --git a/Tests/OpenAPIGeneratorCoreTests/Extensions/Test_SwiftSafeNames.swift b/Tests/OpenAPIGeneratorCoreTests/Extensions/Test_SwiftSafeNames.swift new file mode 100644 index 00000000..f345a1ae --- /dev/null +++ b/Tests/OpenAPIGeneratorCoreTests/Extensions/Test_SwiftSafeNames.swift @@ -0,0 +1,127 @@ +//===----------------------------------------------------------------------===// +// +// 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 +@testable import _OpenAPIGeneratorCore + +final class Test_SwiftSafeNames: Test_Core { + func testAsSwiftSafeName() { + let cases: [(original: String, defensive: String, optimisticUpper: String, optimisticLower: String)] = [ + + // Simple + ("foo", "foo", "Foo", "foo"), + + // Space + ("Hello world", "Hello_space_world", "HelloWorld", "helloWorld"), + + // Mixed capitalization + ("My_URL_value", "My_URL_value", "MyURLValue", "myURLValue"), + + // Dashes + ("hello-world", "hello_hyphen_world", "HelloWorld", "helloWorld"), + + // Header names + ("Retry-After", "Retry_hyphen_After", "RetryAfter", "retryAfter"), + + // All uppercase + ("HELLO_WORLD", "HELLO_WORLD", "HelloWorld", "helloWorld"), + + // Numbers + ("version 2.0", "version_space_2_period_0", "Version2_0", "version2_0"), + ("V1.2Release", "V1_period_2Release", "V1_2Release", "v1_2Release"), + + // Technical strings + ("file/path/to/resource", "file_sol_path_sol_to_sol_resource", "file_sol_path_sol_to_sol_resource", "file_sol_path_sol_to_sol_resource"), + ("user.name@domain.com", "user_period_name_commat_domain_period_com", "user_period_name_commat_domain_period_com", "user_period_name_commat_domain_period_com"), + ("hello.world.2023", "hello_period_world_period_2023", "Hello_world_2023", "hello_world_2023"), + ("order#123", "order_num_123", "order_num_123", "order_num_123"), + ("pressKeys#123", "pressKeys_num_123", "pressKeys_num_123", "pressKeys_num_123"), + + // Non-English characters + ("naïve café", "naïve_space_café", "NaïveCafé", "naïveCafé"), + + // Starts with a number + ("3foo", "_3foo", "_3foo", "_3foo"), + + // Keyword + ("default", "_default", "Default", "_default"), + + // Reserved name + ("Type", "_Type", "_Type", "_type"), + + // Empty string + ("", "_empty", "_Empty_", "_empty_"), + + // Special Char in middle + ("inv@lidName", "inv_commat_lidName", "inv_commat_lidName", "inv_commat_lidName"), + + // Special Char in first position + ("!nvalidName", "_excl_nvalidName", "_excl_nvalidName", "_excl_nvalidName"), + + // Special Char in last position + ("invalidNam?", "invalidNam_quest_", "invalidNam_quest_", "invalidNam_quest_"), + + // Preserve leading underscores + ("__user", "__user", "__User", "__user"), + + // Preserve only leading underscores + ("user__name", "user__name", "UserName", "userName"), + + // Invalid underscore case + ("_", "_underscore_", "_underscore_", "_underscore_"), + + // Special character mixed with character not in map + ("$nake…", "_dollar_nake_x2026_", "_dollar_nake_x2026_", "_dollar_nake_x2026_"), + + // Only special character + ("$", "_dollar_", "_dollar_", "_dollar_"), + + // Only special character not in map + ("……", "_x2026__x2026_", "_x2026__x2026_", "_x2026__x2026_"), + + // Non Latin Characters combined with a RTL language + ("$مرحبا", "_dollar_مرحبا", "_dollar_مرحبا", "_dollar_مرحبا"), + + // Emoji + ("heart❤️emoji", "heart_x2764_️emoji", "heart_x2764_️emoji", "heart_x2764_️emoji"), + + // Content type components + ("application", "application", "Application", "application"), + ("vendor1+json", "vendor1_plus_json", "vendor1_plus_json", "vendor1_plus_json"), + + // Override + ("MEGA", "m_e_g_a", "m_e_g_a", "m_e_g_a"), + ] + self.continueAfterFailure = true + do { + let translator = makeTranslator( + nameOverrides: ["MEGA": "m_e_g_a"] + ) + let asSwiftSafeName: (String, SwiftNameOptions) -> String = translator.context.asSwiftSafeName + for (input, sanitizedDefensive, _, _) in cases { + XCTAssertEqual(asSwiftSafeName(input, .none), sanitizedDefensive, "Defensive, input: \(input)") + } + } + do { + let translator = makeTranslator( + namingStrategy: .optimistic, + nameOverrides: ["MEGA": "m_e_g_a"] + ) + let asSwiftSafeName: (String, SwiftNameOptions) -> String = translator.context.asSwiftSafeName + for (input, _, optimisticUpper, optimisticLower) in cases { + XCTAssertEqual(asSwiftSafeName(input, .capitalize), optimisticUpper, "Optimistic upper, input: \(input)") + XCTAssertEqual(asSwiftSafeName(input, .none), optimisticLower, "Optimistic lower, input: \(input)") + } + } + } +} diff --git a/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift b/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift index a99d4d30..ab87d1d8 100644 --- a/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift +++ b/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift @@ -28,25 +28,49 @@ class Test_Core: XCTestCase { func makeTranslator( components: OpenAPI.Components = .noComponents, diagnostics: any DiagnosticCollector = PrintingDiagnosticCollector(), + namingStrategy: NamingStrategy? = nil, + nameOverrides: [String: String] = [:], featureFlags: FeatureFlags = [] ) -> TypesFileTranslator { - makeTypesTranslator(components: components, diagnostics: diagnostics, featureFlags: featureFlags) + makeTypesTranslator( + components: components, + diagnostics: diagnostics, + namingStrategy: namingStrategy, + nameOverrides: nameOverrides, + featureFlags: featureFlags + ) } func makeTypesTranslator( components: OpenAPI.Components = .noComponents, diagnostics: any DiagnosticCollector = PrintingDiagnosticCollector(), + namingStrategy: NamingStrategy? = nil, + nameOverrides: [String: String] = [:], featureFlags: FeatureFlags = [] ) -> TypesFileTranslator { TypesFileTranslator( - config: makeConfig(featureFlags: featureFlags), + config: makeConfig( + namingStrategy: namingStrategy, + nameOverrides: nameOverrides, + featureFlags: featureFlags + ), diagnostics: diagnostics, components: components ) } - func makeConfig(featureFlags: FeatureFlags = []) -> Config { - .init(mode: .types, access: Config.defaultAccessModifier, featureFlags: featureFlags) + func makeConfig( + namingStrategy: NamingStrategy? = nil, + nameOverrides: [String: String] = [:], + featureFlags: FeatureFlags = [] + ) -> Config { + .init( + mode: .types, + access: Config.defaultAccessModifier, + namingStrategy: namingStrategy, + nameOverrides: nameOverrides, + featureFlags: featureFlags + ) } func loadSchemaFromYAML(_ yamlString: String) throws -> JSONSchema { @@ -61,7 +85,7 @@ class Test_Core: XCTestCase { var context: TranslatorContext { makeTranslator().context } - var asSwiftSafeName: (String) -> String { context.asSwiftSafeName } + var asSwiftSafeName: (String, SwiftNameOptions) -> String { context.asSwiftSafeName } func makeProperty(originalName: String, typeUsage: TypeUsage) -> PropertyBlueprint { .init(originalName: originalName, typeUsage: typeUsage, context: context) diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/Operations/Test_OperationDescription.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/Operations/Test_OperationDescription.swift index 6b37703c..7f69d464 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/Operations/Test_OperationDescription.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/Operations/Test_OperationDescription.swift @@ -144,7 +144,7 @@ final class Test_OperationDescription: Test_Core { endpoint: endpoint, pathParameters: pathItem.parameters, components: .init(), - context: .init(asSwiftSafeName: { $0 }) + context: .init(asSwiftSafeName: { string, _ in string }) ) } } diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeAssigner.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeAssigner.swift index c76ec4c5..c8e8b008 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeAssigner.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeAssigner.swift @@ -56,7 +56,7 @@ class Test_TypeAssigner: Test_Core { "enum": "_enum", ] for (componentKey, expectedSwiftTypeName) in expectedSchemaTypeNames { - XCTAssertEqual(asSwiftSafeName(componentKey.rawValue), expectedSwiftTypeName) + XCTAssertEqual(asSwiftSafeName(componentKey.rawValue, .none), expectedSwiftTypeName) } }