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 is contained in:
parent
9be8fa4a31
commit
212ca52279
@ -19,19 +19,23 @@ extension Colibri {
|
|||||||
|
|
||||||
mutating func run() async throws {
|
mutating func run() async throws {
|
||||||
let fileService = FileService()
|
let fileService = FileService()
|
||||||
|
let templateService = try await TemplateService(templateFolder: "Files/Templates")
|
||||||
|
let terminalService = TerminalService()
|
||||||
|
|
||||||
let copyFiles = CopyFilesTask(fileService: fileService)
|
let copyFiles = CopyFilesTask(fileService: fileService)
|
||||||
let createFolders = CreateFoldersTask(fileService: fileService)
|
let createFolders = CreateFoldersTask(fileService: fileService)
|
||||||
let createRootFolder = CreateRootFolderTask(fileService: fileService)
|
let createRootFolder = CreateRootFolderTask(fileService: fileService)
|
||||||
let initGitInFolder = InitGitInFolderTask()
|
let initGitInFolder = InitGitInFolderTask(terminalService: terminalService)
|
||||||
|
let renderFiles = RenderFilesTask(fileService: fileService,
|
||||||
|
templateService: templateService)
|
||||||
|
|
||||||
let rootFolder = try await createRootFolder(
|
let rootFolder = try await createRootFolder(name: options.name,
|
||||||
name: options.name,
|
at: options.locationURL)
|
||||||
at: options.locationURL
|
|
||||||
)
|
|
||||||
|
|
||||||
try await createFolders(at: rootFolder)
|
try await createFolders(at: rootFolder)
|
||||||
try await copyFiles(to: rootFolder)
|
try await copyFiles(to: rootFolder)
|
||||||
|
try await renderFiles(at: rootFolder,
|
||||||
|
with: Project(name: options.name))
|
||||||
try await initGitInFolder(at: rootFolder)
|
try await initGitInFolder(at: rootFolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,7 +186,7 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright 2024 Adam Fowler
|
Copyright 2025 Röck+Cöde
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -11,7 +11,7 @@ struct App: AsyncParsableCommand {
|
|||||||
// MARK: Functions
|
// MARK: Functions
|
||||||
|
|
||||||
mutating func run() async throws {
|
mutating func run() async throws {
|
||||||
let builder = AppBuilder(name: "App")
|
let builder = AppBuilder(name: "{{ name }}")
|
||||||
let app = try await builder(options)
|
let app = try await builder(options)
|
||||||
|
|
||||||
try await app.runService()
|
try await app.runService()
|
@ -3,7 +3,7 @@
|
|||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "App",
|
name: "{{ name }}",
|
||||||
platforms: [
|
platforms: [
|
||||||
.macOS(.v14)
|
.macOS(.v14)
|
||||||
],
|
],
|
@ -8,7 +8,7 @@ struct AppTests {
|
|||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
private let arguments = TestArguments()
|
private let arguments = TestArguments()
|
||||||
private let builder = AppBuilder(name: "App")
|
private let builder = AppBuilder(name: "{{ name }}")
|
||||||
|
|
||||||
// MARK: Route tests
|
// MARK: Route tests
|
||||||
|
|
@ -1,16 +1,13 @@
|
|||||||
enum File: String {
|
enum File: String {
|
||||||
case app = "App"
|
|
||||||
case appArguments = "AppArguments"
|
case appArguments = "AppArguments"
|
||||||
case appBuilder = "AppBuilder"
|
case appBuilder = "AppBuilder"
|
||||||
case appOptions = "AppOptions"
|
case appOptions = "AppOptions"
|
||||||
case appTests = "AppTests"
|
|
||||||
case dockerFile = "DockerFile"
|
case dockerFile = "DockerFile"
|
||||||
case dockerIgnore = "DockerIgnore"
|
case dockerIgnore = "DockerIgnore"
|
||||||
case environment = "Environment"
|
case environment = "Environment"
|
||||||
case gitIgnore = "GitIgnore"
|
case gitIgnore = "GitIgnore"
|
||||||
case license = "License"
|
case license = "License"
|
||||||
case loggerLevel = "LoggerLevel"
|
case loggerLevel = "LoggerLevel"
|
||||||
case package = "Package"
|
|
||||||
case readme = "Readme"
|
case readme = "Readme"
|
||||||
case testArguments = "TestArguments"
|
case testArguments = "TestArguments"
|
||||||
|
|
||||||
@ -24,11 +21,9 @@ extension File {
|
|||||||
|
|
||||||
var fileName: String {
|
var fileName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .app: "App.swift"
|
|
||||||
case .appArguments: "AppArguments.swift"
|
case .appArguments: "AppArguments.swift"
|
||||||
case .appBuilder: "AppBuilder.swift"
|
case .appBuilder: "AppBuilder.swift"
|
||||||
case .appOptions: "AppOptions.swift"
|
case .appOptions: "AppOptions.swift"
|
||||||
case .appTests: "AppTests.swift"
|
|
||||||
case .dockerFile: "Dockerfile"
|
case .dockerFile: "Dockerfile"
|
||||||
case .dockerIgnore: ".dockerignore"
|
case .dockerIgnore: ".dockerignore"
|
||||||
case .environment: "Environment+Properties.swift"
|
case .environment: "Environment+Properties.swift"
|
||||||
@ -36,7 +31,6 @@ extension File {
|
|||||||
case .license: "LICENSE"
|
case .license: "LICENSE"
|
||||||
case .loggerLevel: "LoggerLevel+Conformances.swift"
|
case .loggerLevel: "LoggerLevel+Conformances.swift"
|
||||||
case .readme: "README.md"
|
case .readme: "README.md"
|
||||||
case .package: "Package.swift"
|
|
||||||
case .testArguments: "TestArguments.swift"
|
case .testArguments: "TestArguments.swift"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -47,9 +41,8 @@ extension File {
|
|||||||
|
|
||||||
var folder: Folder {
|
var folder: Folder {
|
||||||
switch self {
|
switch self {
|
||||||
case .app, .appOptions: .app
|
case .appOptions: .app
|
||||||
case .appArguments, .appBuilder: .libraryPublic
|
case .appArguments, .appBuilder: .libraryPublic
|
||||||
case .appTests: .testCasesPublic
|
|
||||||
case .environment, .loggerLevel: .libraryInternal
|
case .environment, .loggerLevel: .libraryInternal
|
||||||
case .testArguments: .testHelpers
|
case .testArguments: .testHelpers
|
||||||
default: .root
|
default: .root
|
||||||
@ -60,9 +53,9 @@ extension File {
|
|||||||
let basePath = "Resources/Files/Sources"
|
let basePath = "Resources/Files/Sources"
|
||||||
|
|
||||||
return switch self {
|
return switch self {
|
||||||
case .app, .appOptions: "\(basePath)/App"
|
case .appOptions: "\(basePath)/App"
|
||||||
case .appArguments, .appBuilder, .environment, .loggerLevel: "\(basePath)/Library"
|
case .appArguments, .appBuilder, .environment, .loggerLevel: "\(basePath)/Library"
|
||||||
case .appTests, .testArguments: "\(basePath)/Test"
|
case .testArguments: "\(basePath)/Test"
|
||||||
default: basePath
|
default: basePath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
37
Library/Sources/Internal/Enumerations/Template.swift
Normal file
37
Library/Sources/Internal/Enumerations/Template.swift
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
enum Template: String {
|
||||||
|
case app = "App/App"
|
||||||
|
case appTests = "Test/AppTests"
|
||||||
|
case package = "Package"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Properties
|
||||||
|
|
||||||
|
extension Template {
|
||||||
|
|
||||||
|
// MARK: Computed
|
||||||
|
|
||||||
|
var fileName: String {
|
||||||
|
switch self {
|
||||||
|
case .app: "App.swift"
|
||||||
|
case .appTests: "AppTests.swift"
|
||||||
|
case .package: "Package.swift"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var filePath: String {
|
||||||
|
folder.path + fileName
|
||||||
|
}
|
||||||
|
|
||||||
|
var folder: Folder {
|
||||||
|
switch self {
|
||||||
|
case .app: .app
|
||||||
|
case .appTests: .testCasesPublic
|
||||||
|
default: .root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - CaseIterable
|
||||||
|
|
||||||
|
extension Template: CaseIterable {}
|
@ -1,75 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
struct RunProcessTask {
|
|
||||||
|
|
||||||
// MARK: Type aliases
|
|
||||||
|
|
||||||
typealias Output = String
|
|
||||||
|
|
||||||
// MARK: Properties
|
|
||||||
|
|
||||||
private var process: Processable
|
|
||||||
|
|
||||||
// MARK: Initialisers
|
|
||||||
|
|
||||||
init(process: Processable) {
|
|
||||||
self.process = process
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Functions
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
mutating func callAsFunction(
|
|
||||||
path: String, arguments: [String] = []
|
|
||||||
) async throws (RunProcessError) -> Output {
|
|
||||||
process.executableURL = URL(at: path)
|
|
||||||
process.arguments = arguments
|
|
||||||
|
|
||||||
let pipeError = Pipe()
|
|
||||||
let pipeOutput = Pipe()
|
|
||||||
|
|
||||||
process.standardError = pipeError
|
|
||||||
process.standardOutput = pipeOutput
|
|
||||||
|
|
||||||
async let streamOutput = pipeOutput.availableData.append()
|
|
||||||
async let streamError = pipeError.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 RunProcessError.unexpected
|
|
||||||
}
|
|
||||||
|
|
||||||
throw RunProcessError.output(errorOutput)
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let output = String(data: dataOutput, encoding: .utf8) else {
|
|
||||||
throw RunProcessError.unexpected
|
|
||||||
}
|
|
||||||
|
|
||||||
return await withCheckedContinuation { continuation in
|
|
||||||
process.terminationHandler = { _ in
|
|
||||||
continuation.resume(returning: output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch let error as RunProcessError {
|
|
||||||
throw error
|
|
||||||
} catch {
|
|
||||||
throw RunProcessError.captured(error.localizedDescription)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Errors
|
|
||||||
|
|
||||||
public enum RunProcessError: Error, Equatable {
|
|
||||||
case captured(_ output: String)
|
|
||||||
case output(_ output: String)
|
|
||||||
case unexpected
|
|
||||||
}
|
|
13
Library/Sources/Public/Models/Project.swift
Normal file
13
Library/Sources/Public/Models/Project.swift
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
public struct Project: Equatable, Sendable {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
let name: String
|
||||||
|
|
||||||
|
// MARK: Initialisers
|
||||||
|
|
||||||
|
public init(name: String) {
|
||||||
|
self.name = name
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -2,6 +2,10 @@ import Foundation
|
|||||||
|
|
||||||
public protocol Bundleable {
|
public protocol Bundleable {
|
||||||
|
|
||||||
|
// MARK: Computed
|
||||||
|
|
||||||
|
var resourcePath: String? { get }
|
||||||
|
|
||||||
// MARK: Functions
|
// MARK: Functions
|
||||||
|
|
||||||
func url(forResource name: String?, withExtension ext: String?, subdirectory subpath: String?) -> URL?
|
func url(forResource name: String?, withExtension ext: String?, subdirectory subpath: String?) -> URL?
|
||||||
|
@ -9,6 +9,7 @@ public protocol FileServicing {
|
|||||||
// MARK: Functions
|
// MARK: Functions
|
||||||
|
|
||||||
func copyFile(from source: URL, to destination: URL) async throws (FileServiceError)
|
func copyFile(from source: URL, to destination: URL) async throws (FileServiceError)
|
||||||
|
func createFile(at location: URL, with data: Data) async throws (FileServiceError)
|
||||||
func createFolder(at location: URL) async throws (FileServiceError)
|
func createFolder(at location: URL) async throws (FileServiceError)
|
||||||
func deleteItem(at location: URL) async throws (FileServiceError)
|
func deleteItem(at location: URL) async throws (FileServiceError)
|
||||||
func isItemExists(at location: URL) async throws (FileServiceError) -> Bool
|
func isItemExists(at location: URL) async throws (FileServiceError) -> Bool
|
||||||
@ -18,6 +19,8 @@ public protocol FileServicing {
|
|||||||
// MARK: - Errors
|
// MARK: - Errors
|
||||||
|
|
||||||
public enum FileServiceError: Error, Equatable {
|
public enum FileServiceError: Error, Equatable {
|
||||||
|
case fileDataIsEmpty
|
||||||
|
case fileNotCreated
|
||||||
case folderNotCreated
|
case folderNotCreated
|
||||||
case itemAlreadyExists
|
case itemAlreadyExists
|
||||||
case itemEmptyData
|
case itemEmptyData
|
||||||
|
16
Library/Sources/Public/Protocols/TemplateServicing.swift
Normal file
16
Library/Sources/Public/Protocols/TemplateServicing.swift
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
public protocol TemplateServicing {
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
func render(_ object: Any, on template: String) async throws (TemplateServiceError) -> String
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Errors
|
||||||
|
|
||||||
|
public enum TemplateServiceError: Error {
|
||||||
|
case contentNotRendered
|
||||||
|
case resourcePathNotFound
|
||||||
|
case serviceNotInitialized
|
||||||
|
case templateNotFound
|
||||||
|
}
|
19
Library/Sources/Public/Protocols/TerminalServicing.swift
Normal file
19
Library/Sources/Public/Protocols/TerminalServicing.swift
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
public protocol TerminalServicing {
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func run(_ executableURL: URL, arguments: [String]) async throws (TerminalServiceError) -> String
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Errors
|
||||||
|
|
||||||
|
public enum TerminalServiceError: Error, Equatable {
|
||||||
|
case captured(_ output: String)
|
||||||
|
case output(_ output: String)
|
||||||
|
case unexpected
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct FileService: FileServicing {
|
public struct FileService {
|
||||||
|
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
@ -12,6 +12,12 @@ public struct FileService: FileServicing {
|
|||||||
self.fileManager = fileManager
|
self.fileManager = fileManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - FileServicing
|
||||||
|
|
||||||
|
extension FileService: FileServicing {
|
||||||
|
|
||||||
// MARK: Computed
|
// MARK: Computed
|
||||||
|
|
||||||
public var currentFolder: URL {
|
public var currentFolder: URL {
|
||||||
@ -24,7 +30,7 @@ public struct FileService: FileServicing {
|
|||||||
|
|
||||||
public func copyFile(from source: URL, to destination: URL) async throws (FileServiceError) {
|
public func copyFile(from source: URL, to destination: URL) async throws (FileServiceError) {
|
||||||
guard try await !isItemExists(at: destination) else {
|
guard try await !isItemExists(at: destination) else {
|
||||||
throw FileServiceError.itemAlreadyExists
|
throw .itemAlreadyExists
|
||||||
}
|
}
|
||||||
|
|
||||||
var itemData: Data?
|
var itemData: Data?
|
||||||
@ -32,43 +38,59 @@ public struct FileService: FileServicing {
|
|||||||
do {
|
do {
|
||||||
itemData = try Data(contentsOf: source)
|
itemData = try Data(contentsOf: source)
|
||||||
} catch {
|
} catch {
|
||||||
throw FileServiceError.itemEmptyData
|
throw .itemEmptyData
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try itemData?.write(to: destination, options: .atomic)
|
try itemData?.write(to: destination, options: .atomic)
|
||||||
} catch {
|
} 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) {
|
public func createFolder(at location: URL) async throws (FileServiceError) {
|
||||||
guard try await !isItemExists(at: location) else {
|
guard try await !isItemExists(at: location) else {
|
||||||
throw FileServiceError.itemAlreadyExists
|
throw .itemAlreadyExists
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try fileManager.createDirectory(at: location, withIntermediateDirectories: true)
|
try fileManager.createDirectory(at: location, withIntermediateDirectories: true)
|
||||||
} catch {
|
} catch {
|
||||||
throw FileServiceError.folderNotCreated
|
throw .folderNotCreated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func deleteItem(at location: URL) async throws (FileServiceError) {
|
public func deleteItem(at location: URL) async throws (FileServiceError) {
|
||||||
guard try await isItemExists(at: location) else {
|
guard try await isItemExists(at: location) else {
|
||||||
throw FileServiceError.itemNotExists
|
throw .itemNotExists
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try fileManager.removeItem(at: location)
|
try fileManager.removeItem(at: location)
|
||||||
} catch {
|
} catch {
|
||||||
throw FileServiceError.itemNotDeleted
|
throw .itemNotDeleted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func isItemExists(at location: URL) async throws (FileServiceError) -> Bool {
|
public func isItemExists(at location: URL) async throws (FileServiceError) -> Bool {
|
||||||
guard location.isFileURL else {
|
guard location.isFileURL else {
|
||||||
throw FileServiceError.itemNotFileURL
|
throw .itemNotFileURL
|
||||||
}
|
}
|
||||||
|
|
||||||
let filePath = location.pathString
|
let filePath = location.pathString
|
||||||
|
50
Library/Sources/Public/Services/TemplateService.swift
Normal file
50
Library/Sources/Public/Services/TemplateService.swift
Normal file
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
60
Library/Sources/Public/Services/TerminalService.swift
Normal file
60
Library/Sources/Public/Services/TerminalService.swift
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -2,23 +2,25 @@ import Foundation
|
|||||||
|
|
||||||
public struct InitGitInFolderTask {
|
public struct InitGitInFolderTask {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
private let terminalService: TerminalServicing
|
||||||
|
|
||||||
// MARK: Initialisers
|
// MARK: Initialisers
|
||||||
|
|
||||||
public init() {}
|
public init(terminalService: TerminalServicing) {
|
||||||
|
self.terminalService = terminalService
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Functions
|
// MARK: Functions
|
||||||
|
|
||||||
public func callAsFunction(at rootFolder: URL) async throws (RunProcessError) {
|
public func callAsFunction(at rootFolder: URL) async throws (TerminalServiceError) {
|
||||||
let pathCommand = "/usr/bin/git"
|
let executableURL = URL(at: "/usr/bin/git")
|
||||||
let pathFolder = rootFolder.pathString
|
let pathFolder = rootFolder.pathString
|
||||||
|
|
||||||
var gitInit = RunProcessTask(process: Process())
|
try await terminalService.run(executableURL, arguments: ["init", pathFolder])
|
||||||
var gitAdd = RunProcessTask(process: Process())
|
try await terminalService.run(executableURL, arguments: ["-C", pathFolder, "add", "."])
|
||||||
var gitCommit = RunProcessTask(process: Process())
|
try await terminalService.run(executableURL, arguments: ["-C", pathFolder, "commit", "-m", "Initial commit"])
|
||||||
|
|
||||||
try await gitInit(path: pathCommand, arguments: ["init", pathFolder])
|
|
||||||
try await gitAdd(path: pathCommand, arguments: ["-C", pathFolder, "add", "."])
|
|
||||||
try await gitCommit(path: pathCommand, arguments: ["-C", pathFolder, "commit", "-m", "Initial commit"])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
34
Library/Sources/Public/Tasks/RenderFilesTask.swift
Normal file
34
Library/Sources/Public/Tasks/RenderFilesTask.swift
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct RenderFilesTask {
|
||||||
|
|
||||||
|
// MARK: Computed
|
||||||
|
|
||||||
|
private let fileService: FileServicing
|
||||||
|
private let templateService: TemplateServicing
|
||||||
|
|
||||||
|
// MARK: Initialisers
|
||||||
|
|
||||||
|
public init(
|
||||||
|
fileService: FileServicing,
|
||||||
|
templateService: TemplateServicing
|
||||||
|
) {
|
||||||
|
self.fileService = fileService
|
||||||
|
self.templateService = templateService
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
public func callAsFunction(
|
||||||
|
at rootFolder: URL,
|
||||||
|
with model: Project
|
||||||
|
) async throws {
|
||||||
|
for template in Template.allCases {
|
||||||
|
let content = try await templateService.render(model, on: template.rawValue)
|
||||||
|
let fileURL = rootFolder.appendingPath(template.filePath)
|
||||||
|
|
||||||
|
try await fileService.createFile(at: fileURL, with: Data(content.utf8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -12,7 +12,8 @@ let package = Package(
|
|||||||
.library(name: "ColibriLibrary", targets: ["ColibriLibrary"])
|
.library(name: "ColibriLibrary", targets: ["ColibriLibrary"])
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0")
|
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"),
|
||||||
|
.package(url: "https://github.com/hummingbird-project/swift-mustache", from: "2.0.0")
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
@ -25,7 +26,9 @@ let package = Package(
|
|||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "ColibriLibrary",
|
name: "ColibriLibrary",
|
||||||
dependencies: [],
|
dependencies: [
|
||||||
|
.product(name: "Mustache", package: "swift-mustache")
|
||||||
|
],
|
||||||
path: "Library",
|
path: "Library",
|
||||||
resources: [
|
resources: [
|
||||||
.copy("Resources")
|
.copy("Resources")
|
||||||
|
@ -53,45 +53,37 @@ struct FileTests {
|
|||||||
private extension FileTests {
|
private extension FileTests {
|
||||||
enum Expectation {
|
enum Expectation {
|
||||||
static let fileNames: [String] = [
|
static let fileNames: [String] = [
|
||||||
"App.swift",
|
|
||||||
"AppArguments.swift",
|
"AppArguments.swift",
|
||||||
"AppBuilder.swift",
|
"AppBuilder.swift",
|
||||||
"AppOptions.swift",
|
"AppOptions.swift",
|
||||||
"AppTests.swift",
|
|
||||||
"Dockerfile",
|
"Dockerfile",
|
||||||
".dockerignore",
|
".dockerignore",
|
||||||
"Environment+Properties.swift",
|
"Environment+Properties.swift",
|
||||||
".gitignore",
|
".gitignore",
|
||||||
"LICENSE",
|
"LICENSE",
|
||||||
"LoggerLevel+Conformances.swift",
|
"LoggerLevel+Conformances.swift",
|
||||||
"Package.swift",
|
|
||||||
"README.md",
|
"README.md",
|
||||||
"TestArguments.swift"
|
"TestArguments.swift"
|
||||||
]
|
]
|
||||||
|
|
||||||
static let filePaths: [String] = [
|
static let filePaths: [String] = [
|
||||||
"App/Sources/App.swift",
|
|
||||||
"Library/Sources/Public/AppArguments.swift",
|
"Library/Sources/Public/AppArguments.swift",
|
||||||
"Library/Sources/Public/AppBuilder.swift",
|
"Library/Sources/Public/AppBuilder.swift",
|
||||||
"App/Sources/AppOptions.swift",
|
"App/Sources/AppOptions.swift",
|
||||||
"Test/Sources/Cases/Public/AppTests.swift",
|
|
||||||
"Dockerfile",
|
"Dockerfile",
|
||||||
".dockerignore",
|
".dockerignore",
|
||||||
"Library/Sources/Internal/Environment+Properties.swift",
|
"Library/Sources/Internal/Environment+Properties.swift",
|
||||||
".gitignore",
|
".gitignore",
|
||||||
"LICENSE",
|
"LICENSE",
|
||||||
"Library/Sources/Internal/LoggerLevel+Conformances.swift",
|
"Library/Sources/Internal/LoggerLevel+Conformances.swift",
|
||||||
"Package.swift",
|
|
||||||
"README.md",
|
"README.md",
|
||||||
"Test/Sources/Helpers/TestArguments.swift"
|
"Test/Sources/Helpers/TestArguments.swift"
|
||||||
]
|
]
|
||||||
|
|
||||||
static let folders: [Folder] = [
|
static let folders: [Folder] = [
|
||||||
.app,
|
|
||||||
.libraryPublic,
|
.libraryPublic,
|
||||||
.libraryPublic,
|
.libraryPublic,
|
||||||
.app,
|
.app,
|
||||||
.testCasesPublic,
|
|
||||||
.root,
|
.root,
|
||||||
.root,
|
.root,
|
||||||
.libraryInternal,
|
.libraryInternal,
|
||||||
@ -99,17 +91,13 @@ private extension FileTests {
|
|||||||
.root,
|
.root,
|
||||||
.libraryInternal,
|
.libraryInternal,
|
||||||
.root,
|
.root,
|
||||||
.root,
|
|
||||||
.testHelpers
|
.testHelpers
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
static let resourcePaths: [String] = [
|
static let resourcePaths: [String] = [
|
||||||
"Resources/Files/Sources/App",
|
|
||||||
"Resources/Files/Sources/Library",
|
"Resources/Files/Sources/Library",
|
||||||
"Resources/Files/Sources/Library",
|
"Resources/Files/Sources/Library",
|
||||||
"Resources/Files/Sources/App",
|
"Resources/Files/Sources/App",
|
||||||
"Resources/Files/Sources/Test",
|
|
||||||
"Resources/Files/Sources",
|
"Resources/Files/Sources",
|
||||||
"Resources/Files/Sources",
|
"Resources/Files/Sources",
|
||||||
"Resources/Files/Sources/Library",
|
"Resources/Files/Sources/Library",
|
||||||
@ -117,9 +105,7 @@ private extension FileTests {
|
|||||||
"Resources/Files/Sources",
|
"Resources/Files/Sources",
|
||||||
"Resources/Files/Sources/Library",
|
"Resources/Files/Sources/Library",
|
||||||
"Resources/Files/Sources",
|
"Resources/Files/Sources",
|
||||||
"Resources/Files/Sources",
|
|
||||||
"Resources/Files/Sources/Test"
|
"Resources/Files/Sources/Test"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
63
Test/Sources/Cases/Internal/Enumerations/TemplateTests.swift
Normal file
63
Test/Sources/Cases/Internal/Enumerations/TemplateTests.swift
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import Testing
|
||||||
|
|
||||||
|
@testable import ColibriLibrary
|
||||||
|
|
||||||
|
struct TemplateTests {
|
||||||
|
|
||||||
|
// MARK: Properties tests
|
||||||
|
|
||||||
|
@Test(arguments: zip(Template.allCases, Expectation.fileNames))
|
||||||
|
func fileName(for template: Template, expects fileName: String) async throws {
|
||||||
|
// GIVEN
|
||||||
|
// WHEN
|
||||||
|
let result = template.fileName
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
#expect(result == fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(arguments: zip(Template.allCases, Expectation.filePaths))
|
||||||
|
func filePath(for template: Template, expects filePath: String) async throws {
|
||||||
|
// GIVEN
|
||||||
|
// WHEN
|
||||||
|
let result = template.filePath
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
#expect(result == filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(arguments: zip(Template.allCases, Expectation.folders))
|
||||||
|
func folder(for template: Template, expects folder: Folder) async throws {
|
||||||
|
// GIVEN
|
||||||
|
// WHEN
|
||||||
|
let result = template.folder
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
#expect(result == folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Expectations
|
||||||
|
|
||||||
|
private extension TemplateTests {
|
||||||
|
enum Expectation {
|
||||||
|
static let fileNames: [String] = [
|
||||||
|
"App.swift",
|
||||||
|
"AppTests.swift",
|
||||||
|
"Package.swift",
|
||||||
|
]
|
||||||
|
|
||||||
|
static let filePaths: [String] = [
|
||||||
|
"App/Sources/App.swift",
|
||||||
|
"Test/Sources/Cases/Public/AppTests.swift",
|
||||||
|
"Package.swift",
|
||||||
|
]
|
||||||
|
|
||||||
|
static let folders: [Folder] = [
|
||||||
|
.app,
|
||||||
|
.testCasesPublic,
|
||||||
|
.root,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -1,68 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import Testing
|
|
||||||
|
|
||||||
@testable import ColibriLibrary
|
|
||||||
|
|
||||||
struct RunProcessTaskTests {
|
|
||||||
|
|
||||||
// MARK: Properties
|
|
||||||
|
|
||||||
private var process: Process
|
|
||||||
|
|
||||||
// MARK: Initialisers
|
|
||||||
|
|
||||||
init() {
|
|
||||||
self.process = Process()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Functions tests
|
|
||||||
|
|
||||||
@Test(arguments: [Argument.empty, Argument.listAllInFolder])
|
|
||||||
func run(with arguments: [String]) async throws {
|
|
||||||
// GIVEN
|
|
||||||
var task = RunProcessTask(process: process)
|
|
||||||
|
|
||||||
// WHEN
|
|
||||||
let output = try await task(path: .ls, arguments: arguments)
|
|
||||||
|
|
||||||
// THEN
|
|
||||||
#expect(output.isEmpty == false)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(arguments: zip([Argument.help, Argument.listAllInPWD], Throw.outputs))
|
|
||||||
func runThrows(with arguments: [String], throws error: RunProcessError) async throws {
|
|
||||||
// GIVEN
|
|
||||||
var task = RunProcessTask(process: process)
|
|
||||||
|
|
||||||
// WHEN
|
|
||||||
// THEN
|
|
||||||
await #expect(throws: error) {
|
|
||||||
try await task(path: .ls, arguments: arguments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - String+Constants
|
|
||||||
|
|
||||||
private extension String {
|
|
||||||
static let ls = "/bin/ls"
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Parameters
|
|
||||||
|
|
||||||
private extension RunProcessTaskTests {
|
|
||||||
enum Argument {
|
|
||||||
static let empty: [String] = []
|
|
||||||
static let help: [String] = ["--help"]
|
|
||||||
static let listAllInFolder: [String] = ["-la", "."]
|
|
||||||
static let listAllInPWD: [String] = ["-la", "~"]
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Throw {
|
|
||||||
static let outputs: [RunProcessError] = [
|
|
||||||
.output("ls: unrecognized option `--help\'\nusage: ls [-@ABCFGHILOPRSTUWXabcdefghiklmnopqrstuvwxy1%,] [--color=when] [-D format] [file ...]\n"),
|
|
||||||
.output("ls: ~: No such file or directory\n")
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -35,7 +35,7 @@ struct FileServiceTests {
|
|||||||
// WHEN
|
// WHEN
|
||||||
try await service.copyFile(from: source, to: destination)
|
try await service.copyFile(from: source, to: destination)
|
||||||
|
|
||||||
// THENn
|
// THEN
|
||||||
#expect(spy.actions.count == 1)
|
#expect(spy.actions.count == 1)
|
||||||
|
|
||||||
let action = try #require(spy.actions.last)
|
let action = try #require(spy.actions.last)
|
||||||
@ -57,6 +57,37 @@ struct FileServiceTests {
|
|||||||
#expect(spy.actions.isEmpty == true)
|
#expect(spy.actions.isEmpty == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test(arguments: zip([URL.someNewFile],
|
||||||
|
[Data("some data goes here...".utf8)]))
|
||||||
|
func createFile(with location: URL, and data: Data) async throws {
|
||||||
|
// GIVEN
|
||||||
|
let service = service(action: .createFile(location, data))
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
try await service.createFile(at: location, with: data)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
#expect(spy.actions.count == 1)
|
||||||
|
|
||||||
|
let action = try #require(spy.actions.last)
|
||||||
|
|
||||||
|
#expect(action == .fileCreated(location, data))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(arguments: [FileServiceError.itemAlreadyExists, .fileDataIsEmpty, .fileNotCreated])
|
||||||
|
func createFile(throws error: FileServiceError) async throws {
|
||||||
|
// GIVEN
|
||||||
|
let service = service(action: .error(error))
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
// THEN
|
||||||
|
await #expect(throws: error) {
|
||||||
|
try await service.createFile(at: .someNewFile, with: .init())
|
||||||
|
}
|
||||||
|
|
||||||
|
#expect(spy.actions.isEmpty == true)
|
||||||
|
}
|
||||||
|
|
||||||
@Test(arguments: [URL.someNewFolder, .someNewFile])
|
@Test(arguments: [URL.someNewFolder, .someNewFile])
|
||||||
func createFolder(with location: URL) async throws {
|
func createFolder(with location: URL) async throws {
|
||||||
// GIVEN
|
// GIVEN
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
import ColibriLibrary
|
||||||
|
import Foundation
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
struct TemplateServiceTests {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
private let spy = TemplateServiceSpy()
|
||||||
|
|
||||||
|
// MARK: Functions tests
|
||||||
|
|
||||||
|
@Test(arguments: [String.content])
|
||||||
|
func render(_ content: String) async throws {
|
||||||
|
// GIVEN
|
||||||
|
let service = TemplateServiceMock(action: .render(content), spy: spy)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let result = try await service.render([:], on: .template)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
#expect(result == content)
|
||||||
|
|
||||||
|
#expect(spy.actions.isEmpty == false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(arguments: [TemplateServiceError.serviceNotInitialized, .resourcePathNotFound, .templateNotFound, .contentNotRendered])
|
||||||
|
func render(throws error: TemplateServiceError) async throws {
|
||||||
|
// GIVEN
|
||||||
|
let service = TemplateServiceMock(action: .error(error), spy: spy)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
// THEN
|
||||||
|
await #expect(throws: error) {
|
||||||
|
try await service.render([:], on: .template)
|
||||||
|
}
|
||||||
|
|
||||||
|
#expect(spy.actions.isEmpty == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - String+Constants
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
static let content = ""
|
||||||
|
static let template = ""
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
import ColibriLibrary
|
||||||
|
import Foundation
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
struct TerminalServiceTests {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
private let spy = TerminalServiceSpy()
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
@Test(arguments: [URL.someNewFile], [[], ["--example"], ["--example", "--more", "--etc"]])
|
||||||
|
func run(with executableURL: URL, and arguments: [String]) async throws {
|
||||||
|
// GIVEN
|
||||||
|
let service = TerminalServiceMock(action: .run(executableURL, arguments), spy: spy)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
let result = try await service.run(executableURL, arguments: arguments)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
#expect(result == .content)
|
||||||
|
|
||||||
|
#expect(spy.actions.isEmpty == false)
|
||||||
|
|
||||||
|
let action = try #require(spy.actions.last)
|
||||||
|
|
||||||
|
#expect(action == .ran(executableURL, arguments))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(arguments: [TerminalServiceError.unexpected, .captured("Some captured error"), .output("Some output error")])
|
||||||
|
func run(throws error: TerminalServiceError) async throws {
|
||||||
|
// GIVEN
|
||||||
|
let service = TerminalServiceMock(action: .error(error), spy: spy)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
// THEN
|
||||||
|
await #expect(throws: error) {
|
||||||
|
try await service.run(URL.someNewFile, arguments: [])
|
||||||
|
}
|
||||||
|
|
||||||
|
#expect(spy.actions.isEmpty == true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - String+Constants
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
static let content = ""
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
import Foundation
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
@testable import ColibriLibrary
|
||||||
|
|
||||||
|
struct InitGitInFolderTaskTests {
|
||||||
|
|
||||||
|
// MARK: Functions tests
|
||||||
|
|
||||||
|
@Test(arguments: [URL.someCurrentFolder, .someNewFolder, .someDotFolder, .someTildeFolder])
|
||||||
|
func task(at rootFolder: URL) async throws {
|
||||||
|
// GIVEN
|
||||||
|
let terminalService = TerminalServiceSpy()
|
||||||
|
|
||||||
|
let initGitInFolder = InitGitInFolderTask(terminalService: terminalService)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
try await initGitInFolder(at: rootFolder)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let executableURL = URL(at: "/usr/bin/git")
|
||||||
|
let pathFolder = rootFolder.pathString
|
||||||
|
|
||||||
|
#expect(terminalService.actions.count == 3)
|
||||||
|
#expect(terminalService.actions[0] == .ran(executableURL, ["init", pathFolder]))
|
||||||
|
#expect(terminalService.actions[1] == .ran(executableURL, ["-C", pathFolder, "add", "."]))
|
||||||
|
#expect(terminalService.actions[2] == .ran(executableURL, ["-C", pathFolder, "commit", "-m", "Initial commit"]))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
41
Test/Sources/Cases/Public/Tasks/RenderFilesTaskTests.swift
Normal file
41
Test/Sources/Cases/Public/Tasks/RenderFilesTaskTests.swift
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import Foundation
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
@testable import ColibriLibrary
|
||||||
|
|
||||||
|
struct RenderFilesTaskTests {
|
||||||
|
|
||||||
|
@Test(arguments: [URL.someCurrentFolder], [Project(name: "Some name goes here...")])
|
||||||
|
func task(at rootFolder: URL, with project: Project) async throws {
|
||||||
|
// GIVEN
|
||||||
|
let fileService = FileServiceSpy()
|
||||||
|
let templateService = TemplateServiceSpy()
|
||||||
|
|
||||||
|
let renderFiles = RenderFilesTask(fileService: fileService,
|
||||||
|
templateService: templateService)
|
||||||
|
|
||||||
|
// WHEN
|
||||||
|
try await renderFiles(at: rootFolder, with: project)
|
||||||
|
|
||||||
|
// THEN
|
||||||
|
let fileData = Data()
|
||||||
|
let templates = Template.allCases
|
||||||
|
|
||||||
|
#expect(fileService.actions.count == 3)
|
||||||
|
#expect(templateService.actions.count == 3)
|
||||||
|
|
||||||
|
fileService.actions.enumerated().forEach { index, action in
|
||||||
|
#expect(action == .fileCreated(rootFolder.appendingPath(templates[index].filePath), fileData))
|
||||||
|
}
|
||||||
|
|
||||||
|
templateService.actions.enumerated().forEach { index, action in
|
||||||
|
if case let .rendered(object, template) = action {
|
||||||
|
#expect(object as? Project == project)
|
||||||
|
#expect(template == templates[index].rawValue)
|
||||||
|
} else {
|
||||||
|
Issue.record("Action should have been a case of the `TemplateServiceSpy.Action` enumeration.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -64,6 +64,19 @@ extension FileServiceMock: FileServicing {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createFile(at location: URL, with data: Data) async throws (FileServiceError) {
|
||||||
|
guard let nextAction else { return }
|
||||||
|
|
||||||
|
switch nextAction {
|
||||||
|
case .error(let error):
|
||||||
|
throw error
|
||||||
|
case let .createFile(location, data):
|
||||||
|
try await spy?.createFile(at: location, with: data)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func createFolder(at location: URL) async throws (FileServiceError) {
|
func createFolder(at location: URL) async throws (FileServiceError) {
|
||||||
guard let nextAction else { return }
|
guard let nextAction else { return }
|
||||||
|
|
||||||
@ -127,6 +140,7 @@ private extension FileServiceMock {
|
|||||||
extension FileServiceMock {
|
extension FileServiceMock {
|
||||||
enum Action {
|
enum Action {
|
||||||
case copyFile(URL, URL)
|
case copyFile(URL, URL)
|
||||||
|
case createFile(URL, Data)
|
||||||
case createFolder(URL)
|
case createFolder(URL)
|
||||||
case deleteItem(URL)
|
case deleteItem(URL)
|
||||||
case error(FileServiceError)
|
case error(FileServiceError)
|
||||||
|
75
Test/Sources/Helpers/Mocks/TemplateServiceMock.swift
Normal file
75
Test/Sources/Helpers/Mocks/TemplateServiceMock.swift
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import ColibriLibrary
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class TemplateServiceMock {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
private var actions: [Action] = []
|
||||||
|
|
||||||
|
private weak var spy: TemplateServiceSpy?
|
||||||
|
|
||||||
|
// MARK: Initialisers
|
||||||
|
|
||||||
|
init(
|
||||||
|
action: Action,
|
||||||
|
spy: TemplateServiceSpy? = nil
|
||||||
|
) {
|
||||||
|
self.actions.append(action)
|
||||||
|
self.spy = spy
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TemplateServicing
|
||||||
|
|
||||||
|
extension TemplateServiceMock: TemplateServicing {
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func render(_ object: Any, on template: String) async throws(TemplateServiceError) -> String {
|
||||||
|
guard let nextAction else { return .empty }
|
||||||
|
|
||||||
|
switch nextAction {
|
||||||
|
case .error(let error):
|
||||||
|
throw error
|
||||||
|
case .render(let content):
|
||||||
|
try await spy?.render(object, on: template)
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private extension TemplateServiceMock {
|
||||||
|
|
||||||
|
// MARK: Computed
|
||||||
|
|
||||||
|
var nextAction: Action? {
|
||||||
|
guard !actions.isEmpty else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions.removeFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Actions
|
||||||
|
|
||||||
|
extension TemplateServiceMock {
|
||||||
|
enum Action {
|
||||||
|
case error(TemplateServiceError)
|
||||||
|
case render(String)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - String+Constants
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
static let empty = ""
|
||||||
|
}
|
73
Test/Sources/Helpers/Mocks/TerminalServiceMock.swift
Normal file
73
Test/Sources/Helpers/Mocks/TerminalServiceMock.swift
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import ColibriLibrary
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class TerminalServiceMock {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
private var actions: [Action] = []
|
||||||
|
|
||||||
|
private weak var spy: TerminalServiceSpy?
|
||||||
|
|
||||||
|
// MARK: Initialisers
|
||||||
|
|
||||||
|
init(
|
||||||
|
action: Action,
|
||||||
|
spy: TerminalServiceSpy? = nil
|
||||||
|
) {
|
||||||
|
self.actions.append(action)
|
||||||
|
self.spy = spy
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TerminalServicing
|
||||||
|
|
||||||
|
extension TerminalServiceMock: TerminalServicing {
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
func run(_ executableURL: URL, arguments: [String]) async throws (TerminalServiceError) -> String {
|
||||||
|
guard let nextAction else { return .empty }
|
||||||
|
|
||||||
|
switch nextAction {
|
||||||
|
case .error(let error):
|
||||||
|
throw error
|
||||||
|
case let .run(executableURL, arguments):
|
||||||
|
try await spy?.run(executableURL, arguments: arguments)
|
||||||
|
return .empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private extension TerminalServiceMock {
|
||||||
|
|
||||||
|
// MARK: Computed
|
||||||
|
|
||||||
|
var nextAction: Action? {
|
||||||
|
guard !actions.isEmpty else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions.removeFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Actions
|
||||||
|
|
||||||
|
extension TerminalServiceMock {
|
||||||
|
enum Action {
|
||||||
|
case error(TerminalServiceError)
|
||||||
|
case run(URL, [String])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - String+Constants
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
static let empty = ""
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
|
import ColibriLibrary
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@testable import ColibriLibrary
|
|
||||||
|
|
||||||
final class FileServiceSpy {
|
final class FileServiceSpy {
|
||||||
|
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
@ -22,6 +21,10 @@ extension FileServiceSpy: FileServicing {
|
|||||||
actions.append(.fileCopied(source, destination))
|
actions.append(.fileCopied(source, destination))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createFile(at location: URL, with data: Data) async throws (FileServiceError) {
|
||||||
|
actions.append(.fileCreated(location, data))
|
||||||
|
}
|
||||||
|
|
||||||
func createFolder(at location: URL) async throws (FileServiceError) {
|
func createFolder(at location: URL) async throws (FileServiceError) {
|
||||||
actions.append(.folderCreated(location))
|
actions.append(.folderCreated(location))
|
||||||
}
|
}
|
||||||
@ -43,6 +46,7 @@ extension FileServiceSpy: FileServicing {
|
|||||||
|
|
||||||
extension FileServiceSpy {
|
extension FileServiceSpy {
|
||||||
enum Action: Equatable {
|
enum Action: Equatable {
|
||||||
|
case fileCreated(_ location: URL, _ data: Data)
|
||||||
case fileCopied(_ source: URL, _ destination: URL)
|
case fileCopied(_ source: URL, _ destination: URL)
|
||||||
case folderCreated(_ location: URL)
|
case folderCreated(_ location: URL)
|
||||||
case itemDeleted(_ location: URL)
|
case itemDeleted(_ location: URL)
|
||||||
|
38
Test/Sources/Helpers/Spies/TemplateServiceSpy.swift
Normal file
38
Test/Sources/Helpers/Spies/TemplateServiceSpy.swift
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import ColibriLibrary
|
||||||
|
|
||||||
|
final class TemplateServiceSpy {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
private(set) var actions: [Action] = []
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TemplateServicing
|
||||||
|
|
||||||
|
extension TemplateServiceSpy: TemplateServicing {
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func render(_ object: Any, on template: String) async throws (TemplateServiceError) -> String {
|
||||||
|
actions.append(.rendered(object, template))
|
||||||
|
|
||||||
|
return .content
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Actions
|
||||||
|
|
||||||
|
extension TemplateServiceSpy {
|
||||||
|
enum Action {
|
||||||
|
case rendered(_ object: Any, _ template: String)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - String+Constants
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
static let content = ""
|
||||||
|
}
|
39
Test/Sources/Helpers/Spies/TerminalServiceSpy.swift
Normal file
39
Test/Sources/Helpers/Spies/TerminalServiceSpy.swift
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import ColibriLibrary
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class TerminalServiceSpy {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
private(set) var actions: [Action] = []
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TerminalServicing
|
||||||
|
|
||||||
|
extension TerminalServiceSpy: TerminalServicing {
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func run(_ executableURL: URL, arguments: [String]) async throws(TerminalServiceError) -> String {
|
||||||
|
actions.append(.ran(executableURL, arguments))
|
||||||
|
|
||||||
|
return .content
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Actions
|
||||||
|
|
||||||
|
extension TerminalServiceSpy {
|
||||||
|
enum Action: Equatable {
|
||||||
|
case ran(_ executableURL: URL, _ arguments: [String])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - String+Constants
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
static let content = ""
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user