diff --git a/StringCatalogEnum/Makefile b/StringCatalogEnum/Makefile index 57f2bdf..1d5d0e8 100644 --- a/StringCatalogEnum/Makefile +++ b/StringCatalogEnum/Makefile @@ -2,7 +2,7 @@ NAME=xcstrings-enum-generate FILENAME_USAGE=USAGE.md # TODO: get from ENV -EXPORT_PROJECT_ROOT=~/prj/business/quible/quible-ios +EXPORT_PROJECT_ROOT=~/prj/quible/quible-ios # TODO: remove other hard-coded paths all: debug @@ -34,4 +34,4 @@ doc: ./$(NAME) --help >> $(FILENAME_USAGE) echo '```' >> $(FILENAME_USAGE) -publish: release doc +publish: release doc \ No newline at end of file diff --git a/StringCatalogEnum/Package.resolved b/StringCatalogEnum/Package.resolved index 50808b1..198829f 100644 --- a/StringCatalogEnum/Package.resolved +++ b/StringCatalogEnum/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", "state": { "branch": null, - "revision": "a23ded2c91df9156628a6996ab4f347526f17b6b", - "version": "2.1.2" + "revision": "dc9af4781f2afdd1e68e90f80b8603be73ea7abc", + "version": "2.2.0" } }, { diff --git a/StringCatalogEnum/Package.swift b/StringCatalogEnum/Package.swift index 88cb89b..6235dca 100644 --- a/StringCatalogEnum/Package.swift +++ b/StringCatalogEnum/Package.swift @@ -5,18 +5,18 @@ import PackageDescription // TODO: clean up this local CLI package dependency /* -let packageCLIGit = Package.Dependency.package( - name: "StringCatalogEnum", // <- Not sure why this is needed, help? - url: "https://github.com/superarts/swift-cli-core", - .revision("70d7df4e862be86799e9d514e5e55ca92585e7f8") -) -let packageCLILocal = Package.Dependency.package( - name: "StringCatalogEnum", // <- Not sure why this is needed, help? - path: "../../" -) -let packageCLI = packageCLILocal -//let packageCLI = packageCLIGit -*/ + let packageCLIGit = Package.Dependency.package( + name: "StringCatalogEnum", // <- Not sure why this is needed, help? + url: "https://github.com/superarts/swift-cli-core", + .revision("70d7df4e862be86799e9d514e5e55ca92585e7f8") + ) + let packageCLILocal = Package.Dependency.package( + name: "StringCatalogEnum", // <- Not sure why this is needed, help? + path: "../../" + ) + let packageCLI = packageCLILocal + //let packageCLI = packageCLIGit + */ let package = Package( name: "StringCatalogEnum", @@ -24,22 +24,23 @@ let package = Package( .macOS(.v10_12), ], products: [ - .executable(name: "xcstrings-enum-generate", targets: ["StringCatalogEnum"]) + .executable(name: "xcstrings-enum-generate", targets: ["StringCatalogEnum"]), + .library(name: "StringCatalogEnumLibrary", targets: ["StringCatalogEnumLibrary"]), ], dependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), .package( - url: "https://github.com/apple/swift-argument-parser", + url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "1.2.3") ), // packageCLI, .package( - url: "https://github.com/Quick/Quick.git", + url: "https://github.com/Quick/Quick.git", .upToNextMinor(from: "7.3.0") ), .package( - url: "https://github.com/Quick/Nimble.git", + url: "https://github.com/Quick/Nimble.git", .upToNextMinor(from: "13.0.0") ), ], @@ -49,21 +50,28 @@ let package = Package( .target( name: "StringCatalogEnum", dependencies: [ + "StringCatalogEnumLibrary", .product( name: "ArgumentParser", package: "swift-argument-parser" ), /* - .product( - name: "packageCLI", - package: "packageCLI" - ), - */ + .product( + name: "packageCLI", + package: "packageCLI" + ), + */ + ] + ), + .target( + name: "StringCatalogEnumLibrary", + dependencies: [ + // ] ), .testTarget( name: "StringCatalogEnumTests", - dependencies: ["StringCatalogEnum", "Quick", "Nimble"] + dependencies: ["StringCatalogEnumLibrary", "StringCatalogEnum", "Quick", "Nimble"] ), ] -) \ No newline at end of file +) diff --git a/StringCatalogEnum/Sources/StringCatalogEnum/main.swift b/StringCatalogEnum/Sources/StringCatalogEnum/main.swift index aee1c59..b6a2ea1 100644 --- a/StringCatalogEnum/Sources/StringCatalogEnum/main.swift +++ b/StringCatalogEnum/Sources/StringCatalogEnum/main.swift @@ -1,5 +1,6 @@ import ArgumentParser import Foundation +import StringCatalogEnumLibrary struct StringCatalogEnum: ParsableCommand { enum Error: Swift.Error { @@ -29,6 +30,7 @@ struct StringCatalogEnum: ParsableCommand { var enumTypealias: String = "XCS" func run() throws { + let helper = StringEnumHelper() print("LOADING: \(xcstringsPath)") let url = URL(fileURLWithPath: xcstringsPath) let data = try Data(contentsOf: url) @@ -37,143 +39,52 @@ struct StringCatalogEnum: ParsableCommand { guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { throw Error.unexpectedJSON(message: "cannot parse first level object") } - + guard let strings = json["strings"] as? [String: Any] else { throw Error.unexpectedJSON(message: "cannot parse `strings`") } var output = """ - // This file is generated by XcodeStringEnum. Please do *NOT* update it manually. - // As a common practice, swiftLint is disabled for generated files. - // swiftlint:disable all - - import SwiftUI - - /// Makes it a bit easier to type. - typealias \(enumTypealias) = \(enumName) - - /// Generated by StringCatalogEnum, this enum contains all existing Strin Category keys. - enum \(enumName): String, CaseIterable { - - """ - - var cases = [String]() - var knownCases = [String]() - for (key, _) in strings { - guard let name = convertToVariableName(key: key) else { - print("SKIPPING: \(key)") - continue - } - guard key == name else { - continue - } - guard !knownCases.contains(name) else { - cases.append(" // TODO: fix duplicated entry - case \(name)\n") - continue - } - knownCases.append(name) - - // print("\(name):\t\(key)") - // TODO: extract `localizations.en.stringUnit.value` and add in comments as inline documents - if Keyword.allCases.map({ $0.rawValue }).contains(name) { - cases.append(" case `\(name)`\n") - } else { - cases.append(" case \(name)\n") - } - } - cases.sort() - cases.forEach { string in - output += string - } + // This file is generated by XcodeStringEnum. Please do *NOT* update it manually. + // As a common practice, swiftLint is disabled for generated files. + // swiftlint:disable all - output += """ + import SwiftUI - // MARK: - The following cases should be manually replaced in your codebase. - - - """ - cases.removeAll() - for (key, _) in strings { - guard let name = convertToVariableName(key: key) else { - print("SKIPPING: \(key)") - continue - } - guard key != name else { - continue - } - guard !knownCases.contains(name) else { - cases.append(" // TODO: fix duplicated entry - case \(name)\n") - continue - } - knownCases.append(name) - - // print("\(name):\t\(key)") - // TODO: probably missing " handling? - if Keyword.allCases.map({ $0.rawValue }).contains(name) { - cases.append(" case `\(name)` = \"\(key.replacingOccurrences(of: "\n", with: ""))\"\n") - } else { - cases.append(" case \(name) = \"\(key.replacingOccurrences(of: "\n", with: ""))\"\n") - } - } - // cases = Array(Set(cases)) - cases.sort() - cases.forEach { string in - output += string - } + /// Makes it a bit easier to type. + typealias \(enumTypealias) = \(enumName) + + /// Generated by StringCatalogEnum, this enum contains all existing Strin Category keys. + enum \(enumName): String, CaseIterable { + + """ + let keywordRawValues = getKeywordRawValues() + let firstCases = helper.createEnumKeys(with: strings, keyNameMatches: true, keywordEnum: keywordRawValues) + let secondCases = helper.createEnumKeys(with: strings, keyNameMatches: false, keywordEnum: keywordRawValues) output += """ + \(firstCases) + // MARK: - The following cases should be manually replaced in your codebase. + + \(secondCases) + /// Usage: `SwiftUI.Text(\(enumTypealias).yourStringCatalogKey.key)` + var key: LocalizedStringKey { LocalizedStringKey(rawValue) } - /// Usage: `SwiftUI.Text(\(enumTypealias).yourStringCatalogKey.key)` - var key: LocalizedStringKey { LocalizedStringKey(rawValue) } + var string: String { NSLocalizedString(self.rawValue, comment: "Generated localization from String Catalog key: \\(key)") } - var string: String { NSLocalizedString(self.rawValue, comment: "Generated localization from String Catalog key: \\(key)") } + // var text: String.LocalizationValue { String.LocalizationValue(rawValue) } + } + // swiftlint:enable all + """ - // var text: String.LocalizationValue { String.LocalizationValue(rawValue) } - } - // swiftlint:enable all - """ print(output) let outputURL = URL(fileURLWithPath: outputFilename) try output.write(to: outputURL, atomically: true, encoding: .utf8) print("Written to: \(outputFilename)") } - /// Convert a Strint Catalog key to a Swift variable name. - private func convertToVariableName(key: String) -> String? { - // Leave only letters and numeric characters - var result = key.components(separatedBy: CharacterSet.letters.union(CharacterSet.alphanumerics).inverted).joined() - - // Remove leading numeric characters - while !result.isEmpty { - let firstLetter = result.prefix(1) - let digitsCharacters = CharacterSet(charactersIn: "0123456789") - if CharacterSet(charactersIn: String(firstLetter)).isSubset(of: digitsCharacters) { - // print("dropping first of: \(result)") - result = String(result.dropFirst()) - } else { - break - } - } - - // Return nil if empty - guard !result.isEmpty else { - return nil - } - - // Return lowercased string if there's only 1 character - guard result.count > 1 else { - return result.lowercased() - } - - // Change the first character to lowercase - let firstLetter = result.prefix(1).lowercased() - let remainingLetters = result.dropFirst() - result = firstLetter + remainingLetters - - // TODO: uppercase remaining words, e.g. "an example" to "anExample"; currently it's "anexample" - // TODO: lowercase capitalized words, e.g. "EXAMPLE" to "example"; currently it's "eXAMPLE" - - return result + func getKeywordRawValues() -> [String] { + Keyword.allCases.map(\.rawValue) } } diff --git a/StringCatalogEnum/Sources/StringCatalogEnum/model.swift b/StringCatalogEnum/Sources/StringCatalogEnum/model.swift index 584e087..5f8a01d 100644 --- a/StringCatalogEnum/Sources/StringCatalogEnum/model.swift +++ b/StringCatalogEnum/Sources/StringCatalogEnum/model.swift @@ -1,9 +1,9 @@ // TODO: make XCStrings Decodable by listing all possible models // Example: let obj = try JSONDecoder().decode(XCStrings.self, from: data) /* -struct XCStrings: Decodable { - let sourceLanguage: String - let version: String - let strings: [String: String] -} -*/ \ No newline at end of file + struct XCStrings: Decodable { + let sourceLanguage: String + let version: String + let strings: [Strsing: String] + } + */ diff --git a/StringCatalogEnum/Sources/StringCatalogEnumLibrary/StringEnumHelper.swift b/StringCatalogEnum/Sources/StringCatalogEnumLibrary/StringEnumHelper.swift new file mode 100644 index 0000000..91a5c8d --- /dev/null +++ b/StringCatalogEnum/Sources/StringCatalogEnumLibrary/StringEnumHelper.swift @@ -0,0 +1,106 @@ + +import Foundation + +/// Model that helps separate the logic used in StringCatalogEnum struct. +public struct StringEnumHelper { + public init() {} + + /// Creates enum cases depending on whether key == name + /// - Parameters: + /// - stringData: A dictionary containing string data. + /// - keyNameMatches: A boolean flag indicating whether the enum cases should match the keys exactly. + /// - keywordEnum: An array of raw values from the Keyword enum in StringCatalogEnum struct + public func createEnumKeys(with stringData: [String: Any], keyNameMatches: Bool, keywordEnum: [String]) -> String { + var partialOutput = "" + var cases = [String]() + var knownCases = [String]() + for (key, _) in stringData { + guard let name = convertToVariableName(key: key) else { + print("SKIPPING: \(key)") + continue + } + + if keyNameMatches { + guard key == name else { + continue + } + } else { + guard key != name else { + continue + } + } + + guard !knownCases.contains(name) else { + cases.append(" // TODO: fix duplicated entry - case \(name)\n") + continue + } + knownCases.append(name) + + // TODO: extract `localizations.en.stringUnit.value` and add in comments as inline documents + + let caseString: String = if keywordEnum.contains(name) { + keyNameMatches + ? " case `\(name)`\n" + : " case `\(name)` = \"\(key.replacingOccurrences(of: "\n", with: ""))\"\n" + } else { + keyNameMatches + ? " case \(name)\n" + : " case \(name) = \"\(key.replacingOccurrences(of: "\n", with: ""))\"\n" + } + + cases.append(caseString) + } + cases.sort() + cases.forEach { string in + partialOutput += string + } + + return partialOutput + } + + /// Convert a Strint Catalog key to a Swift variable name. + public func convertToVariableName(key: String) -> String? { + var result = key + // Check if the entire string is uppercase + if key == key.uppercased() { + result = key.lowercased() + } + + // Uppercase remaining words, e.g. "an example" to "anExample"; + result = result.split(separator: " ").enumerated().map { index, substring in + index == 0 ? String(substring.prefix(1)).lowercased() + substring.dropFirst() : String(substring).capitalized + }.joined() + + // Leave only letters and numeric characters + result = result.components(separatedBy: CharacterSet.letters.union(CharacterSet.alphanumerics).inverted).joined() + + // Remove leading numeric characters + while !result.isEmpty { + let firstLetter = result.prefix(1) + let digitsCharacters = CharacterSet(charactersIn: "0123456789") + if CharacterSet(charactersIn: String(firstLetter)).isSubset(of: digitsCharacters) { + // print("dropping first of: \(result)") + result = String(result.dropFirst()) + } else { + break + } + } + + // Return nil if empty + guard !result.isEmpty else { + return nil + } + + // Return lowercased string if there's only 1 character + guard result.count > 1 else { + return result.lowercased() + } + + // Change the first character to lowercase + let firstLetter = result.prefix(1).lowercased() + let remainingLetters = result.dropFirst() + result = firstLetter + remainingLetters + + return result + } +} diff --git a/StringCatalogEnum/Tests/StringCatalogEnumTests/StringCatalogEnumTests.swift b/StringCatalogEnum/Tests/StringCatalogEnumTests/StringCatalogEnumTests.swift index 7176021..2db024f 100644 --- a/StringCatalogEnum/Tests/StringCatalogEnumTests/StringCatalogEnumTests.swift +++ b/StringCatalogEnum/Tests/StringCatalogEnumTests/StringCatalogEnumTests.swift @@ -1,6 +1,6 @@ -import Foundation import Nimble import Quick +import StringCatalogEnumLibrary final class StringCatalogEnumSpec: QuickSpec { override class func spec() { @@ -12,4 +12,43 @@ final class StringCatalogEnumSpec: QuickSpec { } } } -} \ No newline at end of file +} + +/// Tests the convertToVariableName() function in StringKeyModel +final class StringKeyModelSpec: QuickSpec { + override class func spec() { + let stringEnumHelper = StringEnumHelper() + + context("StringKeyModel") { + describe("Convert To Variable Name Function") { + it("Basic cases should convert properly") { + // Basic test cases + expect(stringEnumHelper.convertToVariableName(key: "SomeKey")).to(equal("someKey")) + expect(stringEnumHelper.convertToVariableName(key: "123Key")).to(equal("key")) + + // Test cases with special characters + expect(stringEnumHelper.convertToVariableName(key: "Special!@#$%^&*()Key")).to(equal("specialKey")) + + // Test cases with mixed case + expect(stringEnumHelper.convertToVariableName(key: "MixedCaseKey")).to(equal("mixedCaseKey")) + + // Empty Key Case + expect(stringEnumHelper.convertToVariableName(key: "")).to(beNil()) + } + it("White Space cases should convert properly") { + // Test cases with spaces + expect(stringEnumHelper.convertToVariableName(key: "Key with Spaces")).to(equal("keyWithSpaces")) + + // Test cases with whitespaces trailing and leaading + expect(stringEnumHelper.convertToVariableName(key: " whitespace ")).to(equal("whitespace")) + } + it("Auto CamelCase for character after white space") { + expect(stringEnumHelper.convertToVariableName(key: "an example")).to(equal("anExample")) + } + it("Should lowercase all capitalized keys") { + expect(stringEnumHelper.convertToVariableName(key: "EXAMPLE")).to(equal("example")) + } + } + } + } +} diff --git a/StringCatalogEnum/Tests/StringCatalogEnumTests/XCTestManifests.swift b/StringCatalogEnum/Tests/StringCatalogEnumTests/XCTestManifests.swift index a740ab9..4beb3db 100644 --- a/StringCatalogEnum/Tests/StringCatalogEnumTests/XCTestManifests.swift +++ b/StringCatalogEnum/Tests/StringCatalogEnumTests/XCTestManifests.swift @@ -2,7 +2,7 @@ import XCTest #if !canImport(ObjectiveC) public func allTests() -> [XCTestCaseEntry] { - return [ + [ testCase(StringCatalogEnumTests.allTests), ] }