Skip to content

Commit

Permalink
refactor template generation to use common use case
Browse files Browse the repository at this point in the history
  • Loading branch information
movch committed Jul 22, 2022
1 parent 6726e45 commit 201f46b
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 74 deletions.
10 changes: 7 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ let package = Package(
name: "FigmaAssetsFetch",
platforms: [.macOS(.v10_15)],
products: [
.executable(name: "figma-assets-fetch", targets: ["FigmaAssetsFetch"])
.executable(name: "figma-assets-fetch", targets: ["FigmaAssetsFetch"]),
],
dependencies: [
.package(url: "https://github.com/stencilproject/Stencil", from: "0.13.1"),
Expand All @@ -18,10 +18,14 @@ let package = Package(
dependencies: [
.product(name: "Stencil", package: "Stencil"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]),
]
),
.testTarget(
name: "FigmaAssetsFetchTests",
dependencies: ["FigmaAssetsFetch"]
dependencies: ["FigmaAssetsFetch"],
resources: [
.process("Resources"),
]
),
]
)
21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ Example template:
case {{ color.name.camelCased }}
{% endfor %}

public var colorValue: UIColor {
switch color {
public var value: UIColor {
switch self {
{% for color in colors %}
case .{{ color.name.camelCased }}:
return UIColor(
Expand All @@ -42,20 +42,21 @@ Example template:
}
}
}

Save it to file with `*.stencil` extension.

### Commands reference
To run the utility you need to pass several parameters, use `figma-assets-fetch help` for detail description of parameters, or refer to the examples below.

#### `colors-code-gen` command
This command is used for template-based code generation of colors obtained from Figma file.

figma-assets-fetch \
colors-code-gen \
--figma-token $FIGMA_TOKEN \ #Figma API token
--colors-node-url "https://www.figma.com/file/1z5n1txr0nz7qMVzcS3Oif/figma-assets-fetch-palette-example?node-id=1%3A3" \
--templates-directory "$TEMPLATES_DIR" \ #Path to directory with Stencil templates
--template-name Colors.swift.stencil \ #File name of the Stencil template to use
--output "$OUT_FILE_PATH" #Where to save generated file

figma-assets-fetch \
colors-code-gen \
--figma-token $FIGMA_TOKEN \ #Figma API token \
--colors-node-url "https://www.figma.com/file/1z5n1txr0nz7qMVzcS3Oif/figma-assets-fetch-palette-example?node-id=1%3A3" \
--template-path "/Users/michael/Documents/ColorsEnum.stencil" \
--output "/Users/michael/Documents/Colors.swift"

#### `colors-xc-assets` command
This command is used to generate `*.xcassets` file with colors from Figma.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,90 +13,48 @@ struct ColorsCodeGen: ParsableCommand {
@OptionGroup
var options: Options

@Option(
help: "Figma frame url that contains a collection of ellipses with colors."
)
var colorsNodeURL: String = ""

@Option(
help: "Template file name to render."
)
var templateName: String = ""
@OptionGroup
var colorsOptions: ColorsOptions

@Option(
help: "Templates directory path."
help: "Path to Stencil template file to render. It should have `*.stencil` extension."
)
var templatesDirectory: String = ""
var templatePath: String = ""

func run() {
let figmaAPI: FigmaAPIType = FigmaAPI(token: options.figmaToken)
let figmaURLParser: FigmaURLParserType = FigmaURLParser()
let source = FigmaColorsSource(
colorsURLPath: colorsOptions.colorsNodeURL,
darkColorsURLPath: colorsOptions.darkColorsNodeURL,
figmaAPI: FigmaAPI(token: options.figmaToken)
)

guard let figmaFileId = try? figmaURLParser.extractFileId(from: colorsNodeURL),
let colorsNodeId = try? figmaURLParser.extractNodeId(from: colorsNodeURL)
else {
print("Incorrect Figma URL")
Darwin.exit(1)
}
let render = TemplateFileSystemRender(templatePath: templatePath)

let useCase = GetRemoteColorsUseCase(
source: source,
render: render,
output: options.output
)

let cancellable = figmaAPI.requestFile(with: figmaFileId, nodeId: colorsNodeId)
.tryMap(render(figmaNodes:))
let cancellable = useCase.run()
.sink(
receiveCompletion: process(completion:),
receiveValue: save(result:)
receiveValue: { _ in }
)

// Infinitely run the main loop to wait for our request.
RunLoop.main.run()
withExtendedLifetime(cancellable) {}
}

private func render(figmaNodes: FileNodesResponse) throws -> String {
let paletteExtractor = FigmaPaletteParser()
let paletteColors = try paletteExtractor.extract(figmaNodes: figmaNodes)
.sorted(by: { first, second in first.name.original < second.name.original })

let context = ["colors": paletteColors]
let environment = Environment(
loader: FileSystemLoader(
paths: [
Path(templatesDirectory),
]
)
)
let rendered = try environment.renderTemplate(
name: templateName,
context: context
)

return rendered
}

private func process(completion: Subscribers.Completion<Error>) {
switch completion {
case let .failure(error):
print(error)
Darwin.exit(1)
case .finished:
print("Done.")
Darwin.exit(0)
}
}

private func save(result: String) {
guard let output = URL(string: "file://\(options.output)") else {
print(result)
Darwin.exit(0)
}

do {
try result.write(
to: output,
atomically: true,
encoding: String.Encoding.utf8
)
} catch {
print(error)
Darwin.exit(1)
}
}
}
2 changes: 2 additions & 0 deletions Sources/FigmaAssetsFetch/Data/FileSystem/FileWriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ public struct FileWriter: FileWriterType {

public class FileWriterMock {
public var paths: [String] = []
public var contents: [String?] = []

public init() {}
}

extension FileWriterMock: FileWriterType {
public func write(content: String?, at url: URL) throws {
paths.append(url.path)
contents.append(content)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Foundation
import PathKit
import Stencil

public enum TemplateFileSystemRenderError: Error {
case invalidTemplatePath
case invalidOutputPath
}

public struct TemplateFileSystemRender {
private let templatePath: String
private let fileWriter: FileWriterType

public init(
templatePath: String,
fileWriter: FileWriterType = FileWriter()
) {
self.templatePath = templatePath
self.fileWriter = fileWriter
}
}

extension TemplateFileSystemRender: ColorsRender {
public func render(colors: [NamedColor], output: String) throws {
guard let templateURL = URL(string: "file://\(templatePath)"),
templateURL.lastPathComponent.contains(".stencil")
else {
throw TemplateFileSystemRenderError.invalidTemplatePath
}

guard let outputURL = URL(string: "file://\(output)") else {
throw TemplateFileSystemRenderError.invalidOutputPath
}

let context = ["colors": colors]

let environment = Environment(
loader: FileSystemLoader(
paths: [
Path(templateURL.deletingLastPathComponent().path),
]
)
)

let rendered = try environment.renderTemplate(
name: templateURL.lastPathComponent,
context: context
)

try fileWriter.write(content: rendered, at: outputURL)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import XCTest

@testable import FigmaAssetsFetch

class TemplateFileSystemRenderTests: XCTestCase {
func testTemplateRendering() throws {
let mockFileWriter = FileWriterMock()
let render = TemplateFileSystemRender(
templatePath: Bundle.module.url(forResource: "TestTemplate", withExtension: "stencil")!.path,
fileWriter: mockFileWriter
)
let namedColor = NamedColor(
name: .init(name: "Bg / Primary"),
value: .init(r: 0.424, g: 0.459, b: 0.49, a: 1)
)

try render.render(colors: [namedColor], output: "/Test.swift")

assert(mockFileWriter.paths == ["/Test.swift"])
assert(mockFileWriter.contents == ["\nbgPrimary 0.424 0.459 0.49 1.0 6C757D\n\n"])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,56 @@ class GetRemoteColorsUseCaseTests: XCTestCase {
"/Test.xcassets/bg_secondary.colorset/Contents.json",
].sorted())
}

func testGetColorsAndRenderToStencilTemplate() throws {
let figmaAPIMock = FigmaAPIMock(mockedJSON: FigmaJSON.examplePalleteWithStyles)
let colorsSource = FigmaColorsSource(
colorsURLPath: "https://www.figma.com/file/1z5n1txr0nz7qMVzcS3Oif/figma-assets-fetch-palette-example?node-id=1%3A3",
figmaAPI: figmaAPIMock
)

let mockFileWriter = FileWriterMock()
let render = TemplateFileSystemRender(
templatePath: Bundle.module.url(forResource: "TestTemplate", withExtension: "stencil")!.path,
fileWriter: mockFileWriter
)

let useCase = GetRemoteColorsUseCase(
source: colorsSource,
render: render,
output: "/Test.swift"
)

var error: Error?
let expectation = self.expectation(description: "Figma colors source mock")

useCase.run()
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
break
case let .failure(encounteredError):
error = encounteredError
}

expectation.fulfill()
},
receiveValue: { _ in }
)
.store(in: &cancellables)

waitForExpectations(timeout: 10)

XCTAssertNil(error)

assert(mockFileWriter.paths == ["/Test.swift"])
assert(
mockFileWriter
.contents ==
[
"\nbgPrimary 0.0 0.48235294222831726 1.0 1.0 007BFF\n\nbgSuccess 0.1568627506494522 0.6549019813537598 0.2705882489681244 1.0 28A745\n\nbgDanger 0.8627451062202454 0.2078431397676468 0.2705882489681244 1.0 DC3545\n\nbgInfo 0.09019608050584793 0.6352941393852234 0.7215686440467834 1.0 17A2B8\n\nbgSecondary 0.42352941632270813 0.4588235318660736 0.4901960790157318 1.0 6C757D\n\n",
]
)
}
}
3 changes: 3 additions & 0 deletions Tests/FigmaAssetsFetchTests/Resources/TestTemplate.stencil
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{% for color in colors %}
{{ color.name.camelCased }} {{ color.value.r }} {{ color.value.g }} {{ color.value.b }} {{ color.value.a }} {{ color.value.hexValue }}
{% endfor %}

0 comments on commit 201f46b

Please sign in to comment.