diff --git a/README.md b/README.md index df7bdf4..ea83670 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,23 @@ # Chroma -A command line tool to auto generate .swift extensions or structs files from .xcassets on your iOS, macOS & SwiftUI projects. +A command line tool to generate .swift extensions or structs files from .xcassets on your UIKit, AppKIt or SwiftUI projects. ### Usage ``` $ Chroma --help -USAGE: chroma --asset --path [--type ] [--platform ] +USAGE: chroma --asset --path [--type ] [--framework ] OPTIONS: - -a, --asset The path of .xcasset file. - -p, --path The path of the generated .swift file. - -t, --type Specifies generated file type. - Supported values: "extension","struct". (default: + -a, --asset The path of .xcasset file. + -p, --path The path of the generated .swift file. + -t, --type The output type of generated .swift file. + Supported values: extension, struct. (default: extension) - --platform Specifies the platform compatibility of the exported - file. - iOS, macOS, swiftUI (default: iOS) + --framework The framework compatibility of generated .swift file. + Supported values: AppKit, SwiftUI, UIKit. (default: + SwiftUI) -h, --help Show help information. ``` @@ -53,6 +53,6 @@ Select your project target on Xcode > go to `Build Phases` tab > Press on `+` > Copy & paste below command on your new script phase and update paths & platform parameters according to your needs. ``` -chroma --asset MyProject/Assets.xcassets --path MyProject/Extensions/Colors.swift --platform swiftUI +chroma --asset MyProject/Assets.xcassets --path MyProject/Extensions/Colors.swift --framework SwiftUI ``` Optionally you can rename your new `Run Script` to `Chroma`. diff --git a/Sources/Chroma/App/Chroma.swift b/Sources/Chroma/App/Chroma.swift index 0fac624..83f7ed3 100644 --- a/Sources/Chroma/App/Chroma.swift +++ b/Sources/Chroma/App/Chroma.swift @@ -7,7 +7,6 @@ // import ArgumentParser -import Files import Foundation public struct Chroma: ParsableCommand { @@ -20,48 +19,25 @@ public struct Chroma: ParsableCommand { @Option(name: .shortAndLong, help: OutputType.help) private var type: OutputType = .extension - @Option(name: .long, help: "Specifies the platform compatibility of the exported file.\niOS, macOS, swiftUI") - private var platform: Platform = .iOS + @Option(name: .long, help: Framework.help) + private var framework: Framework = .SwiftUI public init() {} public func run() throws { - let outputFile = try createOutputFile() - let content = try getContentFromAssetsFile(outputFile: outputFile) - try outputFile.write(content) + let generator = FileGenerator( + asset: asset, + path: path, + type: type, + framework: framework + ) + let file = try generator.generate() print( """ - \(outputFile.name) was generated successfully. - Can be found at \(outputFile.path) + \(file.name) was generated successfully. + Can be found at \(file.path) """ ) } } - -extension Chroma { - private func createOutputFile() throws -> File { - // Check if path param is a valid swift file path - guard let pathURL = URL(string: path), !pathURL.hasDirectoryPath, pathURL.pathExtension == "swift" else { - throw ChromaError.invalidPath(path: path) - } - - let folder = try Folder(path: pathURL.deletingLastPathComponent().path) - return try File(named: pathURL.lastPathComponent, at: folder) - } - - private func getContentFromAssetsFile(outputFile: File) throws -> String { - let assetFolder = try Folder(path: asset) - let body = platform.fileBody(asset: assetFolder).joined(separator: "\n") - return platform.fileContent(header: header(file: outputFile), body: body) - } - - private func header(file: File) -> String { - switch type { - case .extension: - return "\(type.rawValue) \(platform.variableType)" - case .struct: - return "\(type.rawValue) \(file.nameExcludingExtension)" - } - } -} diff --git a/Sources/Chroma/App/Platform.swift b/Sources/Chroma/App/FileGenerator.swift similarity index 53% rename from Sources/Chroma/App/Platform.swift rename to Sources/Chroma/App/FileGenerator.swift index 80d9600..62af3f5 100644 --- a/Sources/Chroma/App/Platform.swift +++ b/Sources/Chroma/App/FileGenerator.swift @@ -1,65 +1,44 @@ // -// Platform.swift +// FileGenerator.swift // Chroma // -// Created by Oscar De Moya on 7/06/20. -// Copyright © 2020 Jota Uribe. All rights reserved. +// Created by Jota Uribe on 16/10/23. // -import Foundation -import ArgumentParser import Files +import Foundation -enum Platform: String, ExpressibleByArgument { - case iOS - case macOS - case swiftUI -} - -extension Platform { +struct FileGenerator { private static let colorAssetExtension = "colorset" - var framework: String { - switch self { - case .iOS: return "UIKit" - case .macOS: return "AppKit" - case .swiftUI: return "SwiftUI" - } - } + let asset: String + let path: String + let type: OutputType + let framework: Framework - var defaultValue: String { - switch self { - case .iOS, .macOS: - return "?? .clear " - case .swiftUI: - return "" - } + func generate() throws -> File { + let outputFile = try createOutputFile() + let content = try getContentFromAssetsFile(outputFile: outputFile) + try outputFile.write(content) + return outputFile } - var parameterName: String { - switch self { - case .iOS, .macOS: - return "named: " - case .swiftUI: - return "" - } - } + // MARK: Helper Methods - var variableType: String { - switch self { - case .iOS: return "UIColor" - case .macOS: return "NSColor" - case .swiftUI: return "Color" + private func createOutputFile() throws -> File { + // Check if path param is a valid swift file path + guard let pathURL = URL(string: path), !pathURL.hasDirectoryPath, pathURL.pathExtension == "swift" else { + throw ChromaError.invalidPath(path: path) } + + let folder = try Folder(path: pathURL.deletingLastPathComponent().path) + return try File(named: pathURL.lastPathComponent, at: folder) } - var systemReservedVariableNames: [String] { - switch self { - case .iOS, .macOS: - return [] - case .swiftUI: - return ["accentColor"] - } + private func getContentFromAssetsFile(outputFile: File) throws -> String { + let assetFolder = try Folder(path: asset) + let body = fileBody(asset: assetFolder).joined(separator: "\n") + return fileContent(header: header(fileName: outputFile.nameExcludingExtension), body: body) } func fileContent(header: String, body: String) -> String { @@ -70,8 +49,8 @@ extension Platform { // // This file was auto generated please do not modify it directly. // - - import \(framework) + + import \(framework.rawValue) \(header) { @@ -81,6 +60,15 @@ extension Platform { """ } + func header(fileName: String) -> String { + switch type { + case .extension: + return "\(type.rawValue) \(framework.variableType)" + case .struct: + return "\(type.rawValue) \(fileName.capitalized)" + } + } + func fileBody(asset: Folder) -> Array { let assetKey = asset.nameExcludingExtension // Get subfolders with valid extension @@ -107,13 +95,7 @@ extension Platform { private func colorVariableNames(folders: [Folder]) -> [String] { // We filter out duplicated variable names Set(folders.compactMap { colorFolder in - return colorVariable(name: colorFolder.nameExcludingExtension) + return framework.colorVariable(name: colorFolder.nameExcludingExtension) }).sorted() } - - func colorVariable(name: String) -> String? { - let formattedName = name.camelCased().removing(.punctuationCharacters.union(.symbols)) - guard !systemReservedVariableNames.contains(formattedName) else { return nil } - return " static var \(formattedName): \(variableType) { return \(variableType)(\(parameterName)\"\(name)\") \(defaultValue)}" - } } diff --git a/Sources/Chroma/App/Framework.swift b/Sources/Chroma/App/Framework.swift new file mode 100644 index 0000000..7369a97 --- /dev/null +++ b/Sources/Chroma/App/Framework.swift @@ -0,0 +1,67 @@ +// +// Framework.swift +// Chroma +// +// Created by Jota Uribe on 16/10/23. +// + +import Foundation +import ArgumentParser + +enum Framework: String, CaseIterable, ExpressibleByArgument { + case AppKit + case SwiftUI + case UIKit +} + +extension Framework { + static var help: ArgumentHelp { + """ + The framework compatibility of generated .swift file. + Supported values: \(formattedValues). + """ + } +} + +extension Framework { + var defaultValue: String { + switch self { + case .UIKit, .AppKit: + return "?? .clear " + case .SwiftUI: + return "" + } + } + + var parameterName: String { + switch self { + case .UIKit, .AppKit: + return "named: " + case .SwiftUI: + return "" + } + } + + var variableType: String { + switch self { + case .UIKit: return "UIColor" + case .AppKit: return "NSColor" + case .SwiftUI: return "Color" + } + } + + var systemReservedVariableNames: [String] { + switch self { + case .UIKit, .AppKit: + return [] + case .SwiftUI: + return ["accentColor"] + } + } + + func colorVariable(name: String) -> String? { + let formattedName = name.camelCased().removing(.punctuationCharacters.union(.symbols)) + guard !systemReservedVariableNames.contains(formattedName) else { return nil } + return " static var \(formattedName): \(variableType) { return \(variableType)(\(parameterName)\"\(name)\") \(defaultValue)}" + } +} diff --git a/Sources/Chroma/App/OutputType.swift b/Sources/Chroma/App/OutputType.swift index b925857..1a14f17 100644 --- a/Sources/Chroma/App/OutputType.swift +++ b/Sources/Chroma/App/OutputType.swift @@ -15,14 +15,9 @@ enum OutputType: String, CaseIterable, ExpressibleByArgument { } extension OutputType { - - private static var formattedValues: String { - return OutputType.allCases.map { "\"\($0.rawValue)\"" }.joined(separator: ",") - } - static var help: ArgumentHelp { """ - Specifies generated file type. + The output type of generated .swift file. Supported values: \(formattedValues). """ } diff --git a/Sources/Chroma/Extensions/CaseIterable+Formatters.swift b/Sources/Chroma/Extensions/CaseIterable+Formatters.swift new file mode 100644 index 0000000..7ec7d88 --- /dev/null +++ b/Sources/Chroma/Extensions/CaseIterable+Formatters.swift @@ -0,0 +1,14 @@ +// +// CaseIterable+Formatters.swift +// Chroma +// +// Created by Jota Uribe on 17/10/23. +// + +import Foundation + +extension CaseIterable where Self: RawRepresentable { + static var formattedValues: String { + return Self.allCases.map { "\($0.rawValue)" }.joined(separator: ", ") + } +} diff --git a/Tests/ChromaTests/FileGeneratorTests.swift b/Tests/ChromaTests/FileGeneratorTests.swift new file mode 100644 index 0000000..0c779ae --- /dev/null +++ b/Tests/ChromaTests/FileGeneratorTests.swift @@ -0,0 +1,181 @@ +// +// FileGeneratorTests.swift +// Chroma +// +// Created by Jota Uribe on 16/10/23. +// + +import XCTest +import Files +@testable import Chroma + +final class FileGeneratorTests: XCTestCase { + func test_headerFormat_withExtensionOutputFile() throws { + var sut = FileGenerator( + asset: "asset.xcasset", + path: "file.swift", + type: .extension, + framework: .AppKit + ) + XCTAssertEqual(sut.header(fileName: "Asset"), "extension NSColor") + + + sut = FileGenerator( + asset: "asset.xcasset", + path: "file.swift", + type: .extension, + framework: .SwiftUI + ) + XCTAssertEqual(sut.header(fileName: "Asset"), "extension Color") + + + sut = FileGenerator( + asset: "asset.xcasset", + path: "file.swift", + type: .extension, + framework: .UIKit + ) + XCTAssertEqual(sut.header(fileName: "Asset"), "extension UIColor") + } + + func test_headerFormat_withStructOutputFile() throws { + var sut = FileGenerator( + asset: "asset.xcasset", + path: "file.swift", + type: .struct, + framework: .AppKit + ) + XCTAssertEqual(sut.header(fileName: "asset"), "struct Asset") + + + sut = FileGenerator( + asset: "asset.xcasset", + path: "file.swift", + type: .struct, + framework: .SwiftUI + ) + XCTAssertEqual(sut.header(fileName: "asset"), "struct Asset") + + + sut = FileGenerator( + asset: "asset.xcasset", + path: "file.swift", + type: .struct, + framework: .UIKit + ) + XCTAssertEqual(sut.header(fileName: "asset"), "struct Asset") + } + + func test_fileBodyFormat_withRegularAsset() throws { + let path = try XCTUnwrap(resourceFilePath(fileName: "Assets.xcassets")) + let assetPath = try Folder(path: path) + var sut = FileGenerator( + asset: path, + path: "file.swift", + type: .extension, + framework: .AppKit + ) + var fileBody = sut.fileBody(asset: assetPath) + XCTAssertEqual(fileBody, expectedResult(.AppKit)) + + sut = FileGenerator( + asset: path, + path: "file.swift", + type: .extension, + framework: .SwiftUI + ) + fileBody = sut.fileBody(asset: assetPath) + XCTAssertEqual(fileBody, expectedResult(.SwiftUI)) + + sut = FileGenerator( + asset: path, + path: "file.swift", + type: .extension, + framework: .UIKit + ) + fileBody = sut.fileBody(asset: assetPath) + XCTAssertEqual(fileBody, expectedResult(.UIKit)) + } + + func test_fileBodyFormat_withAssetWithFolders() throws { + let path = try XCTUnwrap(resourceFilePath(fileName: "FolderAssets.xcassets")) + let assetPath = try Folder(path: path) + var sut = FileGenerator( + asset: path, + path: "file.swift", + type: .extension, + framework: .AppKit + ) + var fileBody = sut.fileBody(asset: assetPath) + XCTAssertEqual(fileBody, expectedResult(.AppKit, assetType: .withFolders)) + + sut = FileGenerator( + asset: path, + path: "file.swift", + type: .extension, + framework: .SwiftUI + ) + fileBody = sut.fileBody(asset: assetPath) + XCTAssertEqual(fileBody, expectedResult(.SwiftUI, assetType: .withFolders)) + + sut = FileGenerator( + asset: path, + path: "file.swift", + type: .extension, + framework: .UIKit + ) + fileBody = sut.fileBody(asset: assetPath) + XCTAssertEqual(fileBody, expectedResult(.UIKit, assetType: .withFolders)) + } + + static var allTests = [ + ("test_headerFormat_withExtensionOutputFile", test_headerFormat_withExtensionOutputFile), + ("test_headerFormat_withStructOutputFile", test_headerFormat_withStructOutputFile), + ("test_fileBodyFormat_withRegularAsset", test_fileBodyFormat_withRegularAsset), + ("test_fileBodyFormat_withAssetWithFolders", test_fileBodyFormat_withAssetWithFolders) + ] +} + +// MARK: - Helper Methods + +extension FileGeneratorTests { + private enum AssetType { + case regular + case withFolders + } + + private func resourceFilePath(fileName: String) throws -> String { + let resourcePath = try XCTUnwrap(Bundle.module.resourcePath) + do { + _ = try Folder(path: resourcePath.appending("/Resources")) + return resourcePath.appending("/Resources/\(fileName)") + } catch { + return resourcePath.appending("/\(fileName)") + } + } + + private func expectedResult(_ framework: Framework, assetType: AssetType = .regular) -> [String] { + let variableType = framework.variableType + let defaultValue = framework.defaultValue + switch assetType { + case .regular: + return [ + " static var exampleColor1: \(variableType) { return \(variableType)(\(framework.parameterName)\"Example Color 1\") \(defaultValue)}", + " static var exampleColor2: \(variableType) { return \(variableType)(\(framework.parameterName)\"exampleColor2\") \(defaultValue)}", + " static var exampleColor3: \(variableType) { return \(variableType)(\(framework.parameterName)\"ExampleColor3\") \(defaultValue)}", + " static var exampleColor4: \(variableType) { return \(variableType)(\(framework.parameterName)\"ExampleColor4-\") \(defaultValue)}" + ] + case .withFolders: + return [ + " static var rootExampleColor: \(variableType) { return \(variableType)(\(framework.parameterName)\"Root Example Color\") \(defaultValue)}", + " // MARK: - Example 1", + " static var exampleColor1: \(variableType) { return \(variableType)(\(framework.parameterName)\"Example Color 1\") \(defaultValue)}", + " static var exampleColor2: \(variableType) { return \(variableType)(\(framework.parameterName)\"exampleColor2\") \(defaultValue)}", + " static var exampleColor3: \(variableType) { return \(variableType)(\(framework.parameterName)\"ExampleColor3\") \(defaultValue)}", + " static var exampleColor4: \(variableType) { return \(variableType)(\(framework.parameterName)\"ExampleColor4-\") \(defaultValue)}", + " // MARK: - SubFolder", + " static var subFolderExampleColor: \(variableType) { return \(variableType)(\(framework.parameterName)\"SubFolder Example Color\") \(defaultValue)}" + ] + } + } +} diff --git a/Tests/ChromaTests/FrameworkTests.swift b/Tests/ChromaTests/FrameworkTests.swift new file mode 100644 index 0000000..ef831cb --- /dev/null +++ b/Tests/ChromaTests/FrameworkTests.swift @@ -0,0 +1,40 @@ +// +// FrameworkTests.swift +// Chroma +// +// Created by Jota Uribe on 9/06/22. +// + +import XCTest +import Files +@testable import Chroma + +final class FrameworkTests: XCTestCase { + func test_framework_outputValues() { + XCTAssertEqual(Framework.AppKit.rawValue, "AppKit") + XCTAssertEqual(Framework.UIKit.rawValue, "UIKit") + XCTAssertEqual(Framework.SwiftUI.rawValue, "SwiftUI") + } + + func test_variableType_outputValues() { + XCTAssertEqual(Framework.AppKit.variableType, "NSColor") + XCTAssertEqual(Framework.SwiftUI.variableType, "Color") + XCTAssertEqual(Framework.UIKit.variableType, "UIColor") + } + + func test_colorVariable_outputValues() throws { + let variableName = "ExampleColor1" + var platform: Framework = .AppKit + XCTAssertEqual(platform.colorVariable(name: variableName), " static var exampleColor1: NSColor { return NSColor(named: \"ExampleColor1\") ?? .clear }") + platform = .SwiftUI + XCTAssertEqual(platform.colorVariable(name: variableName), " static var exampleColor1: Color { return Color(\"ExampleColor1\") }") + platform = .UIKit + XCTAssertEqual(platform.colorVariable(name: variableName), " static var exampleColor1: UIColor { return UIColor(named: \"ExampleColor1\") ?? .clear }") + } + + static var allTests = [ + ("test_framework_outputValues", test_framework_outputValues), + ("test_variableType_outputValues", test_variableType_outputValues), + ("test_colorVariable_outputValues", test_colorVariable_outputValues) + ] +} diff --git a/Tests/ChromaTests/PlatformTests.swift b/Tests/ChromaTests/PlatformTests.swift deleted file mode 100644 index 3784a7c..0000000 --- a/Tests/ChromaTests/PlatformTests.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// FileTests.swift -// Chroma -// -// Created by Jota Uribe on 9/06/22. -// - -import XCTest -import Files -@testable import Chroma - -private enum AssetType { - case regular - case withFolders -} - -final class PlatformTests: XCTestCase { - func test_framework_outputValues() { - XCTAssertEqual(Platform.iOS.framework, "UIKit") - XCTAssertEqual(Platform.macOS.framework, "AppKit") - XCTAssertEqual(Platform.swiftUI.framework, "SwiftUI") - } - - func test_variableType_outputValues() { - XCTAssertEqual(Platform.iOS.variableType, "UIColor") - XCTAssertEqual(Platform.macOS.variableType, "NSColor") - XCTAssertEqual(Platform.swiftUI.variableType, "Color") - } - - func test_colorVariable_outputValues() throws { - let variableName = "ExampleColor1" - var platform: Platform = .iOS - XCTAssertEqual(platform.colorVariable(name: variableName), " static var exampleColor1: UIColor { return UIColor(named: \"ExampleColor1\") ?? .clear }") - platform = .macOS - XCTAssertEqual(platform.colorVariable(name: variableName), " static var exampleColor1: NSColor { return NSColor(named: \"ExampleColor1\") ?? .clear }") - platform = .swiftUI - XCTAssertEqual(platform.colorVariable(name: variableName), " static var exampleColor1: Color { return Color(\"ExampleColor1\") }") - } - - func test_fileBodyFormat_withRegularAsset() throws { - let path = try XCTUnwrap(resourceFilePath(fileName: "Assets.xcassets")) - let assetPath = try Folder(path: path) - var fileBody = Platform.iOS.fileBody(asset: assetPath) - XCTAssertEqual(fileBody, expectedResult(.iOS)) - - fileBody = Platform.macOS.fileBody(asset: assetPath) - XCTAssertEqual(fileBody, expectedResult(.macOS)) - - fileBody = Platform.swiftUI.fileBody(asset: assetPath) - XCTAssertEqual(fileBody, expectedResult(.swiftUI)) - } - - func test_fileBodyFormat_withAssetWithFolders() throws { - let path = try XCTUnwrap(resourceFilePath(fileName: "FolderAssets.xcassets")) - let assetPath = try Folder(path: path) - var fileBody = Platform.iOS.fileBody(asset: assetPath) - XCTAssertEqual(fileBody, expectedResult(.iOS, assetType: .withFolders)) - - fileBody = Platform.macOS.fileBody(asset: assetPath) - XCTAssertEqual(fileBody, expectedResult(.macOS, assetType: .withFolders)) - - fileBody = Platform.swiftUI.fileBody(asset: assetPath) - XCTAssertEqual(fileBody, expectedResult(.swiftUI, assetType: .withFolders)) - } - - static var allTests = [ - ("test_framework_outputValues", test_framework_outputValues), - ("test_variableType_outputValues", test_variableType_outputValues), - ("test_colorVariable_outputValues", test_colorVariable_outputValues), - ("test_fileBodyFormat_withRegularAsset", test_fileBodyFormat_withRegularAsset), - ("test_fileBodyFormat_withAssetWithFolders", test_fileBodyFormat_withAssetWithFolders) - ] -} - -// MARK: - Helper Methods - -extension PlatformTests { - private func resourceFilePath(fileName: String) throws -> String { - let resourcePath = try XCTUnwrap(Bundle.module.resourcePath) - do { - _ = try Folder(path: resourcePath.appending("/Resources")) - return resourcePath.appending("/Resources/\(fileName)") - } catch { - return resourcePath.appending("/\(fileName)") - } - } - - private func expectedResult(_ platform: Platform, assetType: AssetType = .regular) -> [String] { - let variableType = platform.variableType - let defaultValue = platform.defaultValue - switch assetType { - case .regular: - return [ - " static var exampleColor1: \(variableType) { return \(variableType)(\(platform.parameterName)\"Example Color 1\") \(defaultValue)}", - " static var exampleColor2: \(variableType) { return \(variableType)(\(platform.parameterName)\"exampleColor2\") \(defaultValue)}", - " static var exampleColor3: \(variableType) { return \(variableType)(\(platform.parameterName)\"ExampleColor3\") \(defaultValue)}", - " static var exampleColor4: \(variableType) { return \(variableType)(\(platform.parameterName)\"ExampleColor4-\") \(defaultValue)}" - ] - case .withFolders: - return [ - " static var rootExampleColor: \(variableType) { return \(variableType)(\(platform.parameterName)\"Root Example Color\") \(defaultValue)}", - " // MARK: - Example 1", - " static var exampleColor1: \(variableType) { return \(variableType)(\(platform.parameterName)\"Example Color 1\") \(defaultValue)}", - " static var exampleColor2: \(variableType) { return \(variableType)(\(platform.parameterName)\"exampleColor2\") \(defaultValue)}", - " static var exampleColor3: \(variableType) { return \(variableType)(\(platform.parameterName)\"ExampleColor3\") \(defaultValue)}", - " static var exampleColor4: \(variableType) { return \(variableType)(\(platform.parameterName)\"ExampleColor4-\") \(defaultValue)}", - " // MARK: - SubFolder", - " static var subFolderExampleColor: \(variableType) { return \(variableType)(\(platform.parameterName)\"SubFolder Example Color\") \(defaultValue)}" - ] - } - } -} diff --git a/Tests/ChromaTests/XCTestManifests.swift b/Tests/ChromaTests/XCTestManifests.swift index f32af74..60654c0 100644 --- a/Tests/ChromaTests/XCTestManifests.swift +++ b/Tests/ChromaTests/XCTestManifests.swift @@ -5,7 +5,7 @@ public func allTests() -> [XCTestCaseEntry] { return [ testCase(ChromaErrorTests.allTests), testCase(FileTests.allTests), - testCase(PlatformTests.allTests), + testCase(FrameworkTests.allTests), testCase(StringFormatterTests.allTests) ] }