Skip to content

Commit

Permalink
Improve capitalisation of swift types (#36)
Browse files Browse the repository at this point in the history
* Fixed up String.camelCased to lowercase acronyms

* Use toSwiftEnumCase when outputting enum case names

* Fixup swift 5.4 errors

* Add async and await to reserved words

* Tidy up String extensions a little

* Remove unnecessary code

* Add tests

This requires making the code generator use swift 5.5

* Revert to swift 5.3 to CI working
  • Loading branch information
adam-fowler authored Dec 6, 2021
1 parent 57e256f commit 127d008
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 111 deletions.
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,9 @@ let package = Package(
.product(name: "SwiftFormat", package: "SwiftFormat")
]
),
.testTarget(
name: "SotoCodeGeneratorTests",
dependencies: [.byName(name: "SotoCodeGenerator")]
)
]
)
14 changes: 6 additions & 8 deletions Sources/SotoCodeGenerator/AwsService+shapes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ extension AwsService {
var valueContexts: [EnumMemberContext] = []
let enumDefinitions = trait.value.sorted { $0.value < $1.value }
for value in enumDefinitions {
var key = value.value.lowercased()
var key = value.value
.replacingOccurrences(of: ".", with: "_")
.replacingOccurrences(of: ":", with: "_")
.replacingOccurrences(of: "-", with: "_")
Expand All @@ -79,17 +79,15 @@ extension AwsService {
.replacingOccurrences(of: "(", with: "_")
.replacingOccurrences(of: ")", with: "_")
.replacingOccurrences(of: "*", with: "all")
.toSwiftEnumCase()

if Int(String(key[key.startIndex])) != nil { key = "_" + key }

var caseName = key.camelCased().reservedwordEscaped()
if caseName.allLetterIsNumeric() {
caseName = "\(shapeName.toSwiftVariableCase())\(caseName)"
if key.allLetterIsNumeric() {
key = "\(shapeName.toSwiftVariableCase())\(key)"
}
valueContexts.append(EnumMemberContext(case: caseName, documentation: processDocs(value.documentation), string: value.value))
valueContexts.append(EnumMemberContext(case: key, documentation: processDocs(value.documentation), string: value.value))
}
return EnumContext(
name: shapeName.toSwiftClassCase().reservedwordEscaped(),
name: shapeName.toSwiftClassCase(),
documentation: processDocs(from: shape),
values: valueContexts,
isExtensible: shape.hasTrait(type: SotoExtensibleEnumTrait.self)
Expand Down
247 changes: 144 additions & 103 deletions Sources/SotoCodeGenerator/String.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,133 +14,38 @@

import Foundation

let swiftReservedWords: Set<String> = [
"as",
"break",
"case",
"catch",
"class",
"continue",
"default",
"defer",
"do",
"else",
"enum",
"extension",
"false",
"for",
"func",
"if",
"import",
"in",
"internal",
"is",
"nil",
"operator",
"private",
"protocol",
"public",
"repeat",
"return",
"self",
"static",
"struct",
"switch",
"true",
"try",
"where",
]

extension String {
public func lowerFirst() -> String {
return String(self[startIndex]).lowercased() + self[index(after: startIndex)...]
}

public func upperFirst() -> String {
return String(self[self.startIndex]).uppercased() + self[index(after: startIndex)...]
}

public func toSwiftLabelCase() -> String {
if self.allLetterIsUppercasedAlnum() {
return self.lowercased()
}
return self.replacingOccurrences(of: "-", with: "_").camelCased()
}

public func reservedwordEscaped() -> String {
if swiftReservedWords.contains(self.lowercased()) {
return "`\(self)`"
let snakeCase = self.replacingOccurrences(of: "-", with: "_")
if snakeCase.allLetterIsSnakeUppercased() {
return snakeCase.lowercased().camelCased(capitalize: false)
}
return self
return snakeCase.camelCased(capitalize: false)
}

public func toSwiftVariableCase() -> String {
return self.toSwiftLabelCase().reservedwordEscaped()
}

public func toSwiftClassCase() -> String {
if self == "Type" {
return "`\(self)`"
}

return self.replacingOccurrences(of: "-", with: "_")
.camelCased()
.upperFirst()
.camelCased(capitalize: true)
.reservedwordEscaped()
}

// for some reason the Region and Partition enum are not camel cased
public func toSwiftRegionEnumCase() -> String {
return self.replacingOccurrences(of: "-", with: "")
}

public func camelCased(separator: Character = "_") -> String {
let items = self.split(separator: separator)
var camelCase = ""
items.enumerated().forEach {
camelCase += 0 == $0 ? String($1) : $1.capitalized
}
return camelCase.lowerFirst()
}

public func toSwiftEnumCase() -> String {
return self.toSwiftLabelCase().reservedwordEscaped()
}

private func allLetterIsUppercasedAlnum() -> Bool {
for character in self {
guard let ascii = character.unicodeScalars.first?.value else {
return false
}
if !(0x30..<0x39).contains(ascii), !(0x41..<0x5A).contains(ascii) {
return false
}
}
return true
}

public func tagStriped() -> String {
return self.replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression, range: nil)
}

func allLetterIsNumeric() -> Bool {
for character in self {
if let ascii = character.unicodeScalars.first?.value, (0x30..<0x39).contains(ascii) {
continue
} else {
return false
}
}
return true
}

private static let backslashEncodeMap: [String.Element: String] = [
"\"": "\\\"",
"\\": "\\\\",
"\n": "\\n",
"\t": "\\t",
"\r": "\\r",
]

/// back slash encode special characters
public func addingBackslashEncoding() -> String {
var newString = ""
Expand All @@ -154,6 +59,39 @@ extension String {
return newString
}

func camelCased(capitalize: Bool) -> String {
let items = self.split(separator: "_")
let firstWord = items.first!
let firstWordProcessed: String
if capitalize {
firstWordProcessed = firstWord.upperFirst()
} else {
firstWordProcessed = firstWord.lowerFirstWord()
}
let remainingItems = items.dropFirst().map { word -> String in
if word.allLetterIsSnakeUppercased() {
return String(word)
}
return word.capitalized
}
return firstWordProcessed + remainingItems.joined()
}

func reservedwordEscaped() -> String {
if swiftReservedWords.contains(self) {
return "`\(self)`"
}
return self
}

private static let backslashEncodeMap: [String.Element: String] = [
"\"": "\\\"",
"\\": "\\\\",
"\n": "\\n",
"\t": "\\t",
"\r": "\\r",
]

func deletingPrefix(_ prefix: String) -> String {
guard self.hasPrefix(prefix) else { return self }
return String(self.dropFirst(prefix.count))
Expand Down Expand Up @@ -188,15 +126,118 @@ extension String {
self = self.removingCharacterSet(in: characterset)
}

func capitalizingFirstLetter() -> String {
fileprivate func capitalizingFirstLetter() -> String {
return prefix(1).capitalized + dropFirst()
}

mutating func capitalizeFirstLetter() {
fileprivate mutating func capitalizeFirstLetter() {
self = self.capitalizingFirstLetter()
}

mutating func trimCharacters(in characterset: CharacterSet) {
self = self.trimmingCharacters(in: characterset)
}
}

extension StringProtocol {
func allLetterIsNumeric() -> Bool {
for c in self {
if !c.isNumber {
return false
}
}
return true
}

fileprivate func lowerFirst() -> String {
return String(self[startIndex]).lowercased() + self[index(after: startIndex)...]
}

fileprivate func upperFirst() -> String {
return String(self[self.startIndex]).uppercased() + self[index(after: startIndex)...]
}

/// Lowercase first letter, or if first word is an uppercase acronym then lowercase the whole of the acronym
fileprivate func lowerFirstWord() -> String {
var firstLowercase = self.startIndex
var lastUppercaseOptional: Self.Index? = nil
// get last uppercase character, first lowercase character
while firstLowercase != self.endIndex, self[firstLowercase].isSnakeUppercase() {
lastUppercaseOptional = firstLowercase
firstLowercase = self.index(after: firstLowercase)
}
// if first character was never set first character must be lowercase
guard let lastUppercase = lastUppercaseOptional else {
return String(self)
}
if firstLowercase == self.endIndex {
// if first lowercase letter is the end index then whole word is uppercase and
// should be wholly lowercased
return self.lowercased()
} else if lastUppercase == self.startIndex {
// if last uppercase letter is the first letter then only lower that character
return self.lowerFirst()
} else {
// We have an acronym at the start, lowercase the whole of it
return self[startIndex..<lastUppercase].lowercased() + self[lastUppercase...]
}
}

fileprivate func allLetterIsSnakeUppercased() -> Bool {
for c in self {
if !c.isSnakeUppercase() {
return false
}
}
return true
}
}

extension Character {
fileprivate func isSnakeUppercase() -> Bool {
return self.isNumber || ("A"..."Z").contains(self) || self == "_"
}
}

fileprivate let swiftReservedWords: Set<String> = [
"as",
"async",
"await",
"break",
"case",
"catch",
"class",
"continue",
"default",
"defer",
"do",
"else",
"enum",
"extension",
"false",
"for",
"func",
"if",
"import",
"in",
"internal",
"is",
"nil",
"operator",
"private",
"protocol",
"Protocol",
"public",
"repeat",
"return",
"self",
"Self",
"static",
"struct",
"switch",
"true",
"try",
"Type",
"where",
"while",
]
55 changes: 55 additions & 0 deletions Tests/SotoCodeGeneratorTests/TypeNameTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Soto for AWS open source project
//
// Copyright (c) 2017-2021 the Soto project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Soto project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

@testable import SotoCodeGenerator
import XCTest

final class TypeNameTests: XCTestCase {
func testLabels() {
XCTAssertEqual("testLabel".toSwiftLabelCase(), "testLabel")
XCTAssertEqual("test-label".toSwiftLabelCase(), "testLabel")
XCTAssertEqual("TEST-LABEL".toSwiftLabelCase(), "testLabel")
XCTAssertEqual("TEST-label".toSwiftLabelCase(), "testLabel")
XCTAssertEqual("test_label".toSwiftLabelCase(), "testLabel")
XCTAssertEqual("TEST_LABEL".toSwiftLabelCase(), "testLabel")
XCTAssertEqual("TEST_label".toSwiftLabelCase(), "testLabel")
XCTAssertEqual("TESTLabel".toSwiftLabelCase(), "testLabel")
}

func testVariableNames() {
XCTAssertEqual("testVariable".toSwiftVariableCase(), "testVariable")
XCTAssertEqual("test-variable".toSwiftVariableCase(), "testVariable")
XCTAssertEqual("TEST-VARIABLE".toSwiftVariableCase(), "testVariable")
XCTAssertEqual("TEST-variable".toSwiftVariableCase(), "testVariable")
XCTAssertEqual("test_variable".toSwiftVariableCase(), "testVariable")
XCTAssertEqual("TEST_VARIABLE".toSwiftVariableCase(), "testVariable")
XCTAssertEqual("TEST_variable".toSwiftVariableCase(), "testVariable")
XCTAssertEqual("TESTVariable".toSwiftVariableCase(), "testVariable")
XCTAssertEqual("async".toSwiftVariableCase(), "`async`")
XCTAssertEqual("for".toSwiftVariableCase(), "`for`")
XCTAssertEqual("while".toSwiftVariableCase(), "`while`")
XCTAssertEqual("repeat".toSwiftVariableCase(), "`repeat`")
}

func testClassNames() {
XCTAssertEqual("testLabel".toSwiftClassCase(), "TestLabel")
XCTAssertEqual("test-label".toSwiftClassCase(), "TestLabel")
XCTAssertEqual("TEST-LABEL".toSwiftClassCase(), "TESTLABEL")
XCTAssertEqual("TEST-label".toSwiftClassCase(), "TESTLabel")
XCTAssertEqual("test_label".toSwiftClassCase(), "TestLabel")
XCTAssertEqual("TEST_LABEL".toSwiftClassCase(), "TESTLABEL")
XCTAssertEqual("TEST_label".toSwiftClassCase(), "TESTLabel")
XCTAssertEqual("TESTLabel".toSwiftClassCase(), "TESTLabel")
}
}

0 comments on commit 127d008

Please sign in to comment.