Template support for input parameters (#4)

This PR contains the work done to support input parameters for the `create` command of the executable target, and to render content dynamically for the newly-generated project.

Reviewed-on: #4
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #4.
This commit is contained in:
2025-02-17 22:11:05 +00:00
committed by Javier Cicchelli
parent 9be8fa4a31
commit 212ca52279
33 changed files with 812 additions and 202 deletions
@@ -1,7 +1,7 @@
import Foundation
public struct FileService: FileServicing {
public struct FileService {
// MARK: Properties
private let fileManager: FileManager
@@ -12,6 +12,12 @@ public struct FileService: FileServicing {
self.fileManager = fileManager
}
}
// MARK: - FileServicing
extension FileService: FileServicing {
// MARK: Computed
public var currentFolder: URL {
@@ -24,7 +30,7 @@ public struct FileService: FileServicing {
public func copyFile(from source: URL, to destination: URL) async throws (FileServiceError) {
guard try await !isItemExists(at: destination) else {
throw FileServiceError.itemAlreadyExists
throw .itemAlreadyExists
}
var itemData: Data?
@@ -32,43 +38,59 @@ public struct FileService: FileServicing {
do {
itemData = try Data(contentsOf: source)
} catch {
throw FileServiceError.itemEmptyData
throw .itemEmptyData
}
do {
try itemData?.write(to: destination, options: .atomic)
} catch {
throw FileServiceError.itemNotCopied
throw .itemNotCopied
}
}
public func createFile(at location: URL, with data: Data) async throws (FileServiceError) {
guard try await !isItemExists(at: location) else {
throw .itemAlreadyExists
}
guard !data.isEmpty else {
throw .fileDataIsEmpty
}
do {
try data.write(to: location, options: .atomic)
} catch {
throw .fileNotCreated
}
}
public func createFolder(at location: URL) async throws (FileServiceError) {
guard try await !isItemExists(at: location) else {
throw FileServiceError.itemAlreadyExists
throw .itemAlreadyExists
}
do {
try fileManager.createDirectory(at: location, withIntermediateDirectories: true)
} catch {
throw FileServiceError.folderNotCreated
throw .folderNotCreated
}
}
public func deleteItem(at location: URL) async throws (FileServiceError) {
guard try await isItemExists(at: location) else {
throw FileServiceError.itemNotExists
throw .itemNotExists
}
do {
try fileManager.removeItem(at: location)
} catch {
throw FileServiceError.itemNotDeleted
throw .itemNotDeleted
}
}
public func isItemExists(at location: URL) async throws (FileServiceError) -> Bool {
guard location.isFileURL else {
throw FileServiceError.itemNotFileURL
throw .itemNotFileURL
}
let filePath = location.pathString
@@ -0,0 +1,50 @@
import Foundation
import Mustache
public struct TemplateService {
// MARK: Properties
private let mustacheRenderer: MustacheLibrary
// MARK: Initialisers
public init(
bundle: Bundleable? = nil,
templateFolder: String
) async throws (TemplateServiceError) {
guard let pathResources = (bundle ?? Bundle.module).resourcePath else {
throw .resourcePathNotFound
}
let pathTemplates = pathResources + "/" + templateFolder
do {
self.mustacheRenderer = try await MustacheLibrary(directory: pathTemplates)
} catch {
throw .serviceNotInitialized
}
}
}
// MARK: - TemplateServicing
extension TemplateService: TemplateServicing {
// MARK: Functions
public func render(_ object: Any, on template: String) async throws (TemplateServiceError) -> String {
guard mustacheRenderer.getTemplate(named: template) != nil else {
throw .templateNotFound
}
guard let content = mustacheRenderer.render(object, withTemplate: template) else {
throw .contentNotRendered
}
return content
}
}
@@ -0,0 +1,60 @@
import Foundation
public struct TerminalService {
// MARK: Initialisers
public init() {}
}
// MARK: - TerminalServicing
extension TerminalService: TerminalServicing {
// MARK: Functions
public func run(_ executableURL: URL, arguments: [String]) async throws (TerminalServiceError) -> String {
let process = Process()
let standardError = Pipe()
let standardOutput = Pipe()
process.executableURL = executableURL
process.arguments = arguments
process.standardError = standardError
process.standardOutput = standardOutput
async let streamOutput = standardOutput.availableData.append()
async let streamError = standardError.availableData.append()
do {
try process.run()
let dataOutput = await streamOutput
let dataError = await streamError
guard dataError.isEmpty else {
guard let errorOutput = String(data: dataError, encoding: .utf8) else {
throw TerminalServiceError.unexpected
}
throw TerminalServiceError.output(errorOutput)
}
guard let output = String(data: dataOutput, encoding: .utf8) else {
throw TerminalServiceError.unexpected
}
return await withCheckedContinuation { continuation in
process.terminationHandler = { _ in
continuation.resume(returning: output)
}
}
} catch let error as TerminalServiceError {
throw error
} catch {
throw .captured(error.localizedDescription)
}
}
}