From 6a3b9b5141e092b4c093815b7fb9ad91e7d79d98 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 18 Feb 2025 23:03:51 +0000 Subject: [PATCH] Implemented the Create subcommand (#5) This PR contains the work done to add the *Build* subcommand that build a *Hummingbird* project from the command line to the `Colibri` command. Reviewed-on: https://repo.rock-n-code.com/rock-n-code/colibri/pulls/5 Co-authored-by: Javier Cicchelli Co-committed-by: Javier Cicchelli --- Executable/Sources/Colibri.swift | 6 ++- .../Sources/Commands/BuildCommand.swift | 29 +++++++++++++ Executable/Sources/Options/BuildOptions.swift | 13 ++++++ .../Sources/Options/CreateOptions.swift | 12 ++---- .../Public/Protocols/Locationable.swift | 21 ++++++++++ .../Public/Tasks/BuildProjectTask.swift | 29 +++++++++++++ .../Public/Tasks/BuildProjectTaskTests.swift | 42 +++++++++++++++++++ .../Helpers/Mocks/TemplateServiceMock.swift | 8 ++++ .../Helpers/Mocks/TerminalServiceMock.swift | 8 ++++ 9 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 Executable/Sources/Commands/BuildCommand.swift create mode 100644 Executable/Sources/Options/BuildOptions.swift create mode 100644 Library/Sources/Public/Protocols/Locationable.swift create mode 100644 Library/Sources/Public/Tasks/BuildProjectTask.swift create mode 100644 Test/Sources/Cases/Public/Tasks/BuildProjectTaskTests.swift diff --git a/Executable/Sources/Colibri.swift b/Executable/Sources/Colibri.swift index 194d287..4359f28 100644 --- a/Executable/Sources/Colibri.swift +++ b/Executable/Sources/Colibri.swift @@ -7,7 +7,11 @@ struct Colibri: AsyncParsableCommand { static let configuration = CommandConfiguration( abstract: "The utility to manage your Hummingbird apps", - subcommands: [Create.self] + subcommands: [ + Build.self, + Create.self + ], + defaultSubcommand: Create.self ) } diff --git a/Executable/Sources/Commands/BuildCommand.swift b/Executable/Sources/Commands/BuildCommand.swift new file mode 100644 index 0000000..be1fc08 --- /dev/null +++ b/Executable/Sources/Commands/BuildCommand.swift @@ -0,0 +1,29 @@ +import ArgumentParser +import ColibriLibrary + +extension Colibri { + struct Build: AsyncParsableCommand { + + // MARK: Properties + + static let configuration = CommandConfiguration( + commandName: "build-project", + abstract: "Build a Hummingbird app", + helpNames: .shortAndLong, + aliases: ["build"] + ) + + @OptionGroup var options: Options + + // MARK: Functions + + mutating func run() async throws { + let terminalService = TerminalService() + + let buildProject = BuildProjectTask(terminalService: terminalService) + + try await buildProject(at: options.locationURL) + } + + } +} diff --git a/Executable/Sources/Options/BuildOptions.swift b/Executable/Sources/Options/BuildOptions.swift new file mode 100644 index 0000000..9396639 --- /dev/null +++ b/Executable/Sources/Options/BuildOptions.swift @@ -0,0 +1,13 @@ +import ArgumentParser +import ColibriLibrary + +extension Colibri.Build { + struct Options: ParsableArguments, Locationable { + + // MARK: Properties + + @Option(name: .shortAndLong) + var location: String? + + } +} diff --git a/Executable/Sources/Options/CreateOptions.swift b/Executable/Sources/Options/CreateOptions.swift index 8050ea8..40d66c1 100644 --- a/Executable/Sources/Options/CreateOptions.swift +++ b/Executable/Sources/Options/CreateOptions.swift @@ -1,8 +1,8 @@ import ArgumentParser -import Foundation +import ColibriLibrary extension Colibri.Create { - struct Options: ParsableArguments { + struct Options: ParsableArguments, Locationable { // MARK: Properties @@ -11,12 +11,6 @@ extension Colibri.Create { @Option(name: .shortAndLong) var location: String? - - // MARK: Computed - - var locationURL: URL? { - location.flatMap { URL(fileURLWithPath: $0) } - } - + } } diff --git a/Library/Sources/Public/Protocols/Locationable.swift b/Library/Sources/Public/Protocols/Locationable.swift new file mode 100644 index 0000000..cb14d55 --- /dev/null +++ b/Library/Sources/Public/Protocols/Locationable.swift @@ -0,0 +1,21 @@ +import Foundation + +public protocol Locationable { + + // MARK: Properties + + var location: String? { get set } + +} + +// MARK: - Locationable+Properties + +public extension Locationable { + + // MARK: Properties + + var locationURL: URL? { + location.flatMap { URL(fileURLWithPath: $0) } + } + +} diff --git a/Library/Sources/Public/Tasks/BuildProjectTask.swift b/Library/Sources/Public/Tasks/BuildProjectTask.swift new file mode 100644 index 0000000..282c7cb --- /dev/null +++ b/Library/Sources/Public/Tasks/BuildProjectTask.swift @@ -0,0 +1,29 @@ +import Foundation + +public struct BuildProjectTask { + + // MARK: Properties + + private let terminalService: TerminalServicing + + // MARK: Initialisers + + public init(terminalService: TerminalServicing) { + self.terminalService = terminalService + } + + // MARK: Functions + + public func callAsFunction(at location: URL? = nil) async throws (TerminalServiceError) { + let executableURL = URL(at: "/usr/bin/swift") + + var arguments: [String] = ["build"] + + if let location { + arguments.append(contentsOf: ["--package-path", location.pathString]) + } + + try await terminalService.run(executableURL, arguments: arguments) + } + +} diff --git a/Test/Sources/Cases/Public/Tasks/BuildProjectTaskTests.swift b/Test/Sources/Cases/Public/Tasks/BuildProjectTaskTests.swift new file mode 100644 index 0000000..0c8049f --- /dev/null +++ b/Test/Sources/Cases/Public/Tasks/BuildProjectTaskTests.swift @@ -0,0 +1,42 @@ +import Foundation +import Testing + +@testable import ColibriLibrary + +struct BuildProjectTaskTests { + + @Test(arguments: [nil, URL.someCurrentFolder]) + func task(at location: URL?) async throws { + // GIVEN + let terminalService = TerminalServiceSpy() + let task = BuildProjectTask(terminalService: terminalService) + + // WHEN + try await task(at: location) + + // THEN + let executableURL = URL(at: "/usr/bin/swift") + let arguments = if let location { + ["build", "--package-path", location.pathString] + } else { + ["build"] + } + + #expect(terminalService.actions.count == 1) + #expect(terminalService.actions[0] == .ran(executableURL, arguments)) + } + + @Test(arguments: [nil, URL.someCurrentFolder], [TerminalServiceError.unexpected, .output(""), .captured("")]) + func task(at location: URL?, throws error: TerminalServiceError) async throws { + // GIVEN + let terminalService = TerminalServiceMock(action: .error(error)) + let task = BuildProjectTask(terminalService: terminalService) + + // WHEN + // THEN + await #expect(throws: error) { + try await task(at: location) + } + } + +} diff --git a/Test/Sources/Helpers/Mocks/TemplateServiceMock.swift b/Test/Sources/Helpers/Mocks/TemplateServiceMock.swift index 3b03345..f44a94b 100644 --- a/Test/Sources/Helpers/Mocks/TemplateServiceMock.swift +++ b/Test/Sources/Helpers/Mocks/TemplateServiceMock.swift @@ -19,6 +19,14 @@ final class TemplateServiceMock { self.spy = spy } + init( + actions: [Action], + spy: TemplateServiceSpy? = nil + ) { + self.actions = actions + self.spy = spy + } + } // MARK: - TemplateServicing diff --git a/Test/Sources/Helpers/Mocks/TerminalServiceMock.swift b/Test/Sources/Helpers/Mocks/TerminalServiceMock.swift index 3a1344d..c211aca 100644 --- a/Test/Sources/Helpers/Mocks/TerminalServiceMock.swift +++ b/Test/Sources/Helpers/Mocks/TerminalServiceMock.swift @@ -19,6 +19,14 @@ final class TerminalServiceMock { self.spy = spy } + init( + actions: [Action], + spy: TerminalServiceSpy? = nil + ) { + self.actions = actions + self.spy = spy + } + } // MARK: - TerminalServicing