@@ -26,28 +26,72 @@ struct SwiftNameOptions {
2626 static let noncapitalized = SwiftNameOptions ( isCapitalized: false )
2727}
2828
29- extension String {
29+ /// Computes a string sanitized to be usable as a Swift identifier in various contexts.
30+ protocol SafeNameGenerator {
3031
31- /// Returns a string sanitized to be usable as a Swift identifier.
32- ///
33- /// See the proposal SOAR-0001 for details.
34- ///
35- /// For example, the string `$nake…` would be returned as `_dollar_nake_x2026_`, because
36- /// both the dollar and ellipsis sign are not valid characters in a Swift identifier.
37- /// So, it replaces such characters with their html entity equivalents or unicode hex representation,
38- /// in case it's not present in the `specialCharsMap`. It marks this replacement with `_` as a delimiter.
39- ///
40- /// In addition to replacing illegal characters, it also
41- /// ensures that the identifier starts with a letter and not a number.
42- func safeForSwiftCode_defensive( options: SwiftNameOptions ) -> String {
43- guard !isEmpty else { return " _empty " }
32+ /// Returns a string sanitized to be usable as a Swift identifier in a general context.
33+ /// - Parameters:
34+ /// - documentedName: The input unsanitized string from the OpenAPI document.
35+ /// - options: Additional context for how the sanitized string will be used.
36+ /// - Returns: The sanitized string.
37+ func swiftName( for documentedName: String , options: SwiftNameOptions ) -> String
38+
39+ /// Returns a string sanitized to be usable as a Swift identifier for the provided content type.
40+ /// - Parameter contentType: The content type for which to compute a Swift identifier.
41+ /// - Returns: A Swift identifier for the provided content type.
42+ func contentTypeSwiftName( for contentType: ContentType ) -> String
43+ }
44+
45+ extension SafeNameGenerator {
46+
47+ /// Returns a Swift identifier override for the provided content type.
48+ /// - Parameter contentType: A content type.
49+ /// - Returns: A Swift identifer for the content type, or nil if the provided content type doesn't
50+ /// have an override.
51+ func swiftNameOverride( for contentType: ContentType ) -> String ? {
52+ let rawContentType = contentType. lowercasedTypeSubtypeAndParameters
53+ switch rawContentType {
54+ case " application/json " : return " json "
55+ case " application/x-www-form-urlencoded " : return " urlEncodedForm "
56+ case " multipart/form-data " : return " multipartForm "
57+ case " text/plain " : return " plainText "
58+ case " */* " : return " any "
59+ case " application/xml " : return " xml "
60+ case " application/octet-stream " : return " binary "
61+ case " text/html " : return " html "
62+ case " application/yaml " : return " yaml "
63+ case " text/csv " : return " csv "
64+ case " image/png " : return " png "
65+ case " application/pdf " : return " pdf "
66+ case " image/jpeg " : return " jpeg "
67+ default :
68+ return nil
69+ }
70+ }
71+ }
72+
73+ /// Returns a string sanitized to be usable as a Swift identifier.
74+ ///
75+ /// See the proposal SOAR-0001 for details.
76+ ///
77+ /// For example, the string `$nake…` would be returned as `_dollar_nake_x2026_`, because
78+ /// both the dollar and ellipsis sign are not valid characters in a Swift identifier.
79+ /// So, it replaces such characters with their html entity equivalents or unicode hex representation,
80+ /// in case it's not present in the `specialCharsMap`. It marks this replacement with `_` as a delimiter.
81+ ///
82+ /// In addition to replacing illegal characters, it also
83+ /// ensures that the identifier starts with a letter and not a number.
84+ struct DefensiveSafeNameGenerator : SafeNameGenerator {
85+
86+ func swiftName( for documentedName: String , options: SwiftNameOptions ) -> String {
87+ guard !documentedName. isEmpty else { return " _empty " }
4488
4589 let firstCharSet : CharacterSet = . letters. union ( . init( charactersIn: " _ " ) )
4690 let numbers : CharacterSet = . decimalDigits
4791 let otherCharSet : CharacterSet = . alphanumerics. union ( . init( charactersIn: " _ " ) )
4892
4993 var sanitizedScalars : [ Unicode . Scalar ] = [ ]
50- for (index, scalar) in unicodeScalars. enumerated ( ) {
94+ for (index, scalar) in documentedName . unicodeScalars. enumerated ( ) {
5195 let allowedSet = index == 0 ? firstCharSet : otherCharSet
5296 let outScalar : Unicode . Scalar
5397 if allowedSet. contains ( scalar) {
@@ -70,7 +114,7 @@ extension String {
70114 sanitizedScalars. append ( outScalar)
71115 }
72116
73- let validString = String ( UnicodeScalarView ( sanitizedScalars) )
117+ let validString = String ( String . UnicodeScalarView ( sanitizedScalars) )
74118
75119 // Special case for a single underscore.
76120 // We can't add it to the map as its a valid swift identifier in other cases.
@@ -80,19 +124,73 @@ extension String {
80124 return " _ \( validString) "
81125 }
82126
83- /// Returns a string sanitized to be usable as a Swift identifier, and tries to produce UpperCamelCase
84- /// or lowerCamelCase string, the casing is controlled using the provided options.
85- ///
86- /// If the string contains any illegal characters, falls back to the behavior
87- /// matching `safeForSwiftCode_defensive`.
127+ func contentTypeSwiftName( for contentType: ContentType ) -> String {
128+ if let common = swiftNameOverride ( for: contentType) {
129+ return common
130+ }
131+ let safedType = swiftName ( for: contentType. originallyCasedType, options: . noncapitalized)
132+ let safedSubtype = swiftName ( for: contentType. originallyCasedSubtype, options: . noncapitalized)
133+ let componentSeparator = " _ "
134+ let prefix = " \( safedType) \( componentSeparator) \( safedSubtype) "
135+ let params = contentType. lowercasedParameterPairs
136+ guard !params. isEmpty else { return prefix }
137+ let safedParams =
138+ params. map { pair in
139+ pair. split ( separator: " = " )
140+ . map { component in
141+ swiftName ( for: String ( component) , options: . noncapitalized)
142+ }
143+ . joined ( separator: componentSeparator)
144+ }
145+ . joined ( separator: componentSeparator)
146+ return prefix + componentSeparator + safedParams
147+ }
148+
149+ /// A list of Swift keywords.
88150 ///
89- /// Check out [SOAR-0013](https://swiftpackageindex.com/apple/swift-openapi-generator/documentation/swift-openapi-generator/soar-0013) for details.
90- func safeForSwiftCode_idiomatic( options: SwiftNameOptions ) -> String {
151+ /// Copied from SwiftSyntax/TokenKind.swift
152+ private static let keywords : Set < String > = [
153+ " associatedtype " , " class " , " deinit " , " enum " , " extension " , " func " , " import " , " init " , " inout " , " let " , " operator " ,
154+ " precedencegroup " , " protocol " , " struct " , " subscript " , " typealias " , " var " , " fileprivate " , " internal " , " private " ,
155+ " public " , " static " , " defer " , " if " , " guard " , " do " , " repeat " , " else " , " for " , " in " , " while " , " return " , " break " ,
156+ " continue " , " fallthrough " , " switch " , " case " , " default " , " where " , " catch " , " throw " , " as " , " Any " , " false " , " is " ,
157+ " nil " , " rethrows " , " super " , " self " , " Self " , " true " , " try " , " throws " , " yield " , " String " , " Error " , " Int " , " Bool " ,
158+ " Array " , " Type " , " type " , " Protocol " , " await " ,
159+ ]
160+
161+ /// A map of ASCII printable characters to their HTML entity names. Used to reduce collisions in generated names.
162+ private static let specialCharsMap : [ Unicode . Scalar : String ] = [
163+ " " : " space " , " ! " : " excl " , " \" " : " quot " , " # " : " num " , " $ " : " dollar " , " % " : " percnt " , " & " : " amp " , " ' " : " apos " ,
164+ " ( " : " lpar " , " ) " : " rpar " , " * " : " ast " , " + " : " plus " , " , " : " comma " , " - " : " hyphen " , " . " : " period " , " / " : " sol " ,
165+ " : " : " colon " , " ; " : " semi " , " < " : " lt " , " = " : " equals " , " > " : " gt " , " ? " : " quest " , " @ " : " commat " , " [ " : " lbrack " ,
166+ " \\ " : " bsol " , " ] " : " rbrack " , " ^ " : " hat " , " ` " : " grave " , " { " : " lcub " , " | " : " verbar " , " } " : " rcub " , " ~ " : " tilde " ,
167+ ]
168+ }
169+
170+ extension SafeNameGenerator where Self == DefensiveSafeNameGenerator {
171+ static var defensive : DefensiveSafeNameGenerator {
172+ DefensiveSafeNameGenerator ( )
173+ }
174+ }
175+
176+ /// Returns a string sanitized to be usable as a Swift identifier, and tries to produce UpperCamelCase
177+ /// or lowerCamelCase string, the casing is controlled using the provided options.
178+ ///
179+ /// If the string contains any illegal characters, falls back to the behavior
180+ /// matching `safeForSwiftCode_defensive`.
181+ ///
182+ /// Check out [SOAR-0013](https://swiftpackageindex.com/apple/swift-openapi-generator/documentation/swift-openapi-generator/soar-0013) for details.
183+ struct IdiomaticSafeNameGenerator : SafeNameGenerator {
184+
185+ /// The defensive strategy to use as fallback.
186+ var defensive : DefensiveSafeNameGenerator
187+
188+ func swiftName( for documentedName: String , options: SwiftNameOptions ) -> String {
91189 let capitalize = options. isCapitalized
92- if isEmpty { return capitalize ? " _Empty_ " : " _empty_ " }
190+ if documentedName . isEmpty { return capitalize ? " _Empty_ " : " _empty_ " }
93191
94192 // Detect cases like HELLO_WORLD, sometimes used for constants.
95- let isAllUppercase = allSatisfy {
193+ let isAllUppercase = documentedName . allSatisfy {
96194 // Must check that no characters are lowercased, as non-letter characters
97195 // don't return `true` to `isUppercase`.
98196 !$0. isLowercase
@@ -105,7 +203,7 @@ extension String {
105203 // 4. In the middle: drop ["{", "}"] -> replace with ""
106204
107205 var buffer : [ Character ] = [ ]
108- buffer. reserveCapacity ( count)
206+ buffer. reserveCapacity ( documentedName . count)
109207 enum State : Equatable {
110208 case modifying
111209 case preFirstWord
@@ -115,8 +213,8 @@ extension String {
115213 case waitingForWordStarter
116214 }
117215 var state : State = . preFirstWord
118- for index in self [ ... ] . indices {
119- let char = self [ index]
216+ for index in documentedName [ ... ] . indices {
217+ let char = documentedName [ index]
120218 let _state = state
121219 state = . modifying
122220 switch _state {
@@ -158,7 +256,7 @@ extension String {
158256 buffer. append ( char)
159257 context. isAccumulatingInitialUppercase = false
160258 } else {
161- let suffix = suffix ( from: self . index ( after: index) )
259+ let suffix = documentedName . suffix ( from: documentedName . index ( after: index) )
162260 if suffix. count >= 2 {
163261 let next = suffix. first!
164262 let secondNext = suffix. dropFirst ( ) . first!
@@ -245,29 +343,51 @@ extension String {
245343 }
246344 precondition ( state != . modifying, " Logic error in \( #function) , string: ' \( self ) ' " )
247345 }
248- return String ( buffer) . safeForSwiftCode_defensive ( options: options)
346+ return defensive. swiftName ( for: String ( buffer) , options: options)
347+ }
348+
349+ func contentTypeSwiftName( for contentType: ContentType ) -> String {
350+ if let common = swiftNameOverride ( for: contentType) {
351+ return common
352+ }
353+ let safedType = swiftName ( for: contentType. originallyCasedType, options: . noncapitalized)
354+ let safedSubtype = swiftName ( for: contentType. originallyCasedSubtype, options: . noncapitalized)
355+ let prettifiedSubtype = safedSubtype. uppercasingFirstLetter
356+ let prefix = " \( safedType) \( prettifiedSubtype) "
357+ let params = contentType. lowercasedParameterPairs
358+ guard !params. isEmpty else { return prefix }
359+ let safedParams =
360+ params. map { pair in
361+ pair. split ( separator: " = " )
362+ . map { component in
363+ let safedComponent = swiftName ( for: String ( component) , options: . noncapitalized)
364+ return safedComponent. uppercasingFirstLetter
365+ }
366+ . joined ( )
367+ }
368+ . joined ( )
369+ return prefix + safedParams
249370 }
250371
251372 /// A list of word separator characters for the idiomatic naming strategy.
252373 private static let wordSeparators : Set < Character > = [ " _ " , " - " , " " , " / " , " + " ]
374+ }
253375
254- /// A list of Swift keywords.
255- ///
256- /// Copied from SwiftSyntax/TokenKind.swift
257- private static let keywords : Set < String > = [
258- " associatedtype " , " class " , " deinit " , " enum " , " extension " , " func " , " import " , " init " , " inout " , " let " , " operator " ,
259- " precedencegroup " , " protocol " , " struct " , " subscript " , " typealias " , " var " , " fileprivate " , " internal " , " private " ,
260- " public " , " static " , " defer " , " if " , " guard " , " do " , " repeat " , " else " , " for " , " in " , " while " , " return " , " break " ,
261- " continue " , " fallthrough " , " switch " , " case " , " default " , " where " , " catch " , " throw " , " as " , " Any " , " false " , " is " ,
262- " nil " , " rethrows " , " super " , " self " , " Self " , " true " , " try " , " throws " , " yield " , " String " , " Error " , " Int " , " Bool " ,
263- " Array " , " Type " , " type " , " Protocol " , " await " ,
264- ]
376+ extension SafeNameGenerator where Self == DefensiveSafeNameGenerator {
377+ static var idiomatic : IdiomaticSafeNameGenerator {
378+ IdiomaticSafeNameGenerator ( defensive: . defensive)
379+ }
380+ }
265381
266- /// A map of ASCII printable characters to their HTML entity names. Used to reduce collisions in generated names.
267- private static let specialCharsMap : [ Unicode . Scalar : String ] = [
268- " " : " space " , " ! " : " excl " , " \" " : " quot " , " # " : " num " , " $ " : " dollar " , " % " : " percnt " , " & " : " amp " , " ' " : " apos " ,
269- " ( " : " lpar " , " ) " : " rpar " , " * " : " ast " , " + " : " plus " , " , " : " comma " , " - " : " hyphen " , " . " : " period " , " / " : " sol " ,
270- " : " : " colon " , " ; " : " semi " , " < " : " lt " , " = " : " equals " , " > " : " gt " , " ? " : " quest " , " @ " : " commat " , " [ " : " lbrack " ,
271- " \\ " : " bsol " , " ] " : " rbrack " , " ^ " : " hat " , " ` " : " grave " , " { " : " lcub " , " | " : " verbar " , " } " : " rcub " , " ~ " : " tilde " ,
272- ]
382+ extension String {
383+
384+ @available ( * , deprecated)
385+ func safeForSwiftCode_defensive( options: SwiftNameOptions ) -> String {
386+ DefensiveSafeNameGenerator ( ) . swiftName ( for: self , options: options)
387+ }
388+
389+ @available ( * , deprecated)
390+ func safeForSwiftCode_idiomatic( options: SwiftNameOptions ) -> String {
391+ IdiomaticSafeNameGenerator ( defensive: DefensiveSafeNameGenerator ( ) ) . swiftName ( for: self , options: options)
392+ }
273393}
0 commit comments