Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: hummingbird-project/swift-mustache-cli
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 0.1.0
Choose a base ref
...
head repository: hummingbird-project/swift-mustache-cli
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
  • 6 commits
  • 5 files changed
  • 1 contributor

Commits on Sep 18, 2024

  1. Copy the full SHA
    4d37e20 View commit details
  2. Update readme

    adam-fowler committed Sep 18, 2024
    Copy the full SHA
    003f3e1 View commit details
  3. Update CI

    adam-fowler committed Sep 18, 2024
    Copy the full SHA
    1b50f0f View commit details
  4. And again

    adam-fowler committed Sep 18, 2024
    Copy the full SHA
    1f83b89 View commit details
  5. Copy the full SHA
    5d53878 View commit details
  6. Allow building in swift 5.10

    Just without tests
    adam-fowler committed Sep 18, 2024
    Copy the full SHA
    d571948 View commit details
Showing with 272 additions and 102 deletions.
  1. +2 −2 .github/workflows/ci.yml
  2. +7 −3 Package.swift
  3. +134 −0 Sources/MustacheCLI/app.swift
  4. +0 −97 Sources/swift-mustache-cli/app.swift
  5. +129 −0 Tests/MustacheCLITests/MustacheCLITests.swift
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ jobs:
timeout-minutes: 15
strategy:
matrix:
image: ["swift:5.10", "swiftlang/swift:nightly-6.0-jammy"]
image: ["swift:5.10"]

container:
image: ${{ matrix.image }}
@@ -25,4 +25,4 @@ jobs:
uses: actions/checkout@v4
- name: Build
run: |
swift build
swift test
10 changes: 7 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.9
// swift-tools-version:5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
@@ -7,7 +7,7 @@ let package = Package(
name: "swift-mustache-cli",
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)],
products: [
.executable(name: "mustache", targets: ["swift-mustache-cli"]),
.executable(name: "mustache", targets: ["MustacheCLI"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.4.0"),
@@ -16,12 +16,16 @@ let package = Package(
],
targets: [
.executableTarget(
name: "swift-mustache-cli",
name: "MustacheCLI",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Mustache", package: "swift-mustache"),
.product(name: "Yams", package: "yams"),
]
),
.testTarget(
name: "MustacheCLITests",
dependencies: ["MustacheCLI"]
)
]
)
134 changes: 134 additions & 0 deletions Sources/MustacheCLI/app.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import ArgumentParser
import Foundation
import Mustache
import Yams

struct MustacheAppError: Error, CustomStringConvertible {
let description: String

init(_ description: String) {
self.description = description
}
}

@main
struct MustacheApp: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "mustache",
abstract: """
Mustache is a logic-less templating system for rendering
text files.
""",
usage: """
mustache <context-filename> <template-filename>
mustache - <template-filename>
""",
discussion: """
The mustache command processes a Mustache template with a context
defined in YAML/JSON. While the template is always loaded from a file
the context can be supplied to the process either from a file or from
stdin.
Examples:
mustache context.yml template.mustache
mustache default.yml,context.yml template.mustache
cat context.yml | mustache - template.mustache
"""
)

@Argument(help: "Comma separated list of context files")
var contextFile: String

@Argument(help: "Mustache template file")
var templateFile: String

func run() throws {
let renderer = Renderer()
guard let templateString = loadString(filename: self.templateFile) else {
throw MustacheAppError("Failed to load template file \(self.templateFile)")
}
let contextStrings = try loadContextFiles(filename: self.contextFile)
let rendered = try renderer.render(template: templateString, contexts: contextStrings)
if rendered.last?.isNewline == true {
print(rendered, terminator: "")
} else {
print(rendered)
}
}

/// Load file into string
func loadString(filename: some StringProtocol) -> String? {
guard let data = FileManager.default.contents(atPath: String(filename)) else { return nil }
return String(decoding: data, as: Unicode.UTF8.self)
}

/// Pass stdin into a string
func loadStdin() -> String {
let input = AnyIterator { readLine(strippingNewline: false) }.joined(separator: "")
return input
}

/// Load context strings
func loadContextFiles(filename: String) throws -> [String] {
var contexts: [String] = []
let files = filename.split(separator: ",")

for file in files {
if file == "-" {
contexts.append(self.loadStdin())
} else {
guard let string = loadString(filename: file) else {
throw MustacheAppError("Failed to load context file \(filename)")
}
contexts.append(string)
}
}
return contexts
}

struct Renderer {
func render(template: String, contexts: [String]) throws -> String {
let compiledTemplate = try MustacheTemplate(string: template)
let context = try loadCombinedYaml(contexts: contexts)
return compiledTemplate.render(context)
}

func loadCombinedYaml(contexts: [String]) throws -> Any {
func merge(_ object: Any, into base: Any?) -> Any {
if let objectDictionary = object as? [String: Any], var baseDictionary = base as? [String: Any] {
for (key, value) in objectDictionary {
baseDictionary[key] = merge(value, into: baseDictionary[key])
}
return baseDictionary
} else {
return object
}
}

guard var object = try contexts.first.map({ try loadYaml(string: String($0)) }) else {
throw MustacheAppError("No contexts provided")
}
let restOfContexts = contexts.dropFirst()
for context in restOfContexts {
let additionalObject = try loadYaml(string: context)
object = merge(additionalObject, into: object)
}
return object
}

func loadYaml(string: String) throws -> Any {
func convertObject(_ object: Any) -> Any {
guard var dictionary = object as? [String: Any] else { return object }
for (key, value) in dictionary {
dictionary[key] = convertObject(value)
}
return dictionary
}

guard let yaml = try Yams.load(yaml: string) else {
throw MustacheAppError("YAML context file is empty")
}
return convertObject(yaml)
}
}
}
97 changes: 0 additions & 97 deletions Sources/swift-mustache-cli/app.swift

This file was deleted.

Loading