diff --git a/Package.swift b/Package.swift index f14beec..d0f0b87 100644 --- a/Package.swift +++ b/Package.swift @@ -24,5 +24,9 @@ let package = Package( .product(name: "SwiftFormat", package: "SwiftFormat") ] ), + .testTarget( + name: "SotoCodeGeneratorTests", + dependencies: [.byName(name: "SotoCodeGenerator")] + ) ] ) diff --git a/Sources/SotoCodeGenerator/AwsService+shapes.swift b/Sources/SotoCodeGenerator/AwsService+shapes.swift index 3354aac..4f88040 100644 --- a/Sources/SotoCodeGenerator/AwsService+shapes.swift +++ b/Sources/SotoCodeGenerator/AwsService+shapes.swift @@ -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: "_") @@ -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) diff --git a/Sources/SotoCodeGenerator/String.swift b/Sources/SotoCodeGenerator/String.swift index 07070e2..b20e2d1 100644 --- a/Sources/SotoCodeGenerator/String.swift +++ b/Sources/SotoCodeGenerator/String.swift @@ -14,64 +14,13 @@ import Foundation -let swiftReservedWords: Set = [ - "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 { @@ -79,13 +28,9 @@ extension String { } 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 @@ -93,54 +38,14 @@ extension 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 = "" @@ -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)) @@ -188,11 +126,11 @@ 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() } @@ -200,3 +138,106 @@ extension String { 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.. 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 = [ + "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", +] diff --git a/Tests/SotoCodeGeneratorTests/TypeNameTests.swift b/Tests/SotoCodeGeneratorTests/TypeNameTests.swift new file mode 100644 index 0000000..3c575b9 --- /dev/null +++ b/Tests/SotoCodeGeneratorTests/TypeNameTests.swift @@ -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") + } +}