diff --git a/Executable/Sources/Colibri.swift b/Executable/Sources/Colibri.swift index d39fac1..a32b262 100644 --- a/Executable/Sources/Colibri.swift +++ b/Executable/Sources/Colibri.swift @@ -11,6 +11,7 @@ struct Colibri: AsyncParsableCommand { Build.self, Clean.self, Create.self, + Open.self, Outdated.self, Update.self ], diff --git a/Executable/Sources/Commands/OpenCommand.swift b/Executable/Sources/Commands/OpenCommand.swift new file mode 100644 index 0000000..688659c --- /dev/null +++ b/Executable/Sources/Commands/OpenCommand.swift @@ -0,0 +1,29 @@ +import ArgumentParser +import ColibriLibrary + +extension Colibri { + struct Open: AsyncParsableCommand { + + // MARK: Properties + + static let configuration = CommandConfiguration( + commandName: "open-project", + abstract: "Open a Hummingbird app", + helpNames: .shortAndLong, + aliases: ["open"] + ) + + @OptionGroup var options: Options + + // MARK: Functions + + mutating func run() async throws { + let terminalService = TerminalService() + + let openProject = OpenProjectTask(terminalService: terminalService) + + try await openProject(with: options.ide, at: options.locationURL) + } + + } +} diff --git a/Executable/Sources/Extensions/IDE+Conformances.swift b/Executable/Sources/Extensions/IDE+Conformances.swift new file mode 100644 index 0000000..413ea49 --- /dev/null +++ b/Executable/Sources/Extensions/IDE+Conformances.swift @@ -0,0 +1,6 @@ +import ArgumentParser +import ColibriLibrary + +// MARK: - ExpressibleByArgument + +extension IDE: ExpressibleByArgument {} diff --git a/Executable/Sources/Options/OpenOptions.swift b/Executable/Sources/Options/OpenOptions.swift new file mode 100644 index 0000000..4f94ec7 --- /dev/null +++ b/Executable/Sources/Options/OpenOptions.swift @@ -0,0 +1,16 @@ +import ArgumentParser +import ColibriLibrary + +extension Colibri.Open { + struct Options: ParsableArguments, Locationable { + + // MARK: Properties + + @Option(name: .shortAndLong) + var ide: IDE = .xcode + + @Option(name: .shortAndLong) + var location: String? + + } +} diff --git a/Library/Sources/Public/Enumerations/IDE.swift b/Library/Sources/Public/Enumerations/IDE.swift new file mode 100644 index 0000000..2c530ac --- /dev/null +++ b/Library/Sources/Public/Enumerations/IDE.swift @@ -0,0 +1,20 @@ +public enum IDE: String { + case vscode + case xcode +} + +// MARK: - Extension + +extension IDE { + + // MARK: Functions + + static func random() -> IDE { + .allCases.randomElement() ?? .xcode + } + +} + +// MARK: - CaseIterable + +extension IDE: CaseIterable {} diff --git a/Library/Sources/Public/Tasks/OpenProjectTask.swift b/Library/Sources/Public/Tasks/OpenProjectTask.swift new file mode 100644 index 0000000..d609ebb --- /dev/null +++ b/Library/Sources/Public/Tasks/OpenProjectTask.swift @@ -0,0 +1,44 @@ +import Foundation + +public struct OpenProjectTask { + + // MARK: Properties + + private let terminalService: TerminalServicing + + // MARK: Initialisers + + public init(terminalService: TerminalServicing) { + self.terminalService = terminalService + } + + // MARK: Functions + + public func callAsFunction(with ide: IDE, at location: URL? = nil) async throws (TerminalServiceError) { + let executableURL: URL = switch ide { + case .vscode: .init(at: "/usr/local/bin/code") + case .xcode: .init(at: "/usr/bin/open") + } + + let locationPath = switch ide { + case .vscode: location?.pathString ?? "." + case .xcode: location?.appendingPath(.Path.package).pathString ?? .Path.package + } + + let arguments: [String] = switch ide { + case .vscode: [locationPath] + case .xcode: ["-a", "Xcode", locationPath] + } + + try await terminalService.run(executableURL, arguments: arguments) + } + +} + +// MARK: - String+Constants + +private extension String { + enum Path { + static let package = "Package.swift" + } +} diff --git a/Test/Sources/Cases/Public/Tasks/OpenProjectTaskTests.swift b/Test/Sources/Cases/Public/Tasks/OpenProjectTaskTests.swift new file mode 100644 index 0000000..7e4f748 --- /dev/null +++ b/Test/Sources/Cases/Public/Tasks/OpenProjectTaskTests.swift @@ -0,0 +1,56 @@ +import Foundation +import Testing + +@testable import ColibriLibrary + +struct OpenProjectTaskTests { + + @Test(arguments: [nil, URL.someCurrentFolder]) + func taskWithVSCodeIDE(at location: URL?) async throws { + // GIVEN + let terminalService = TerminalServiceSpy() + let task = OpenProjectTask(terminalService: terminalService) + + // WHEN + try await task(with: .vscode, at: location) + + // THEN + let executableURL = URL(at: "/usr/local/bin/code") + let arguments = [location?.pathString ?? "."] + + #expect(terminalService.actions.count == 1) + #expect(terminalService.actions[0] == .ran(executableURL, arguments)) + } + + @Test(arguments: [nil, URL.someCurrentFolder]) + func taskWithXcodeIDE(at location: URL?) async throws { + // GIVEN + let terminalService = TerminalServiceSpy() + let task = OpenProjectTask(terminalService: terminalService) + + // WHEN + try await task(with: .xcode, at: location) + + // THEN + let locationPath = location?.appendingPath("Package.swift").pathString ?? "Package.swift" + let executableURL = URL(at: "/usr/bin/open") + let arguments = ["-a", "Xcode", locationPath] + + #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 = OpenProjectTask(terminalService: terminalService) + + // WHEN + // THEN + await #expect(throws: error) { + try await task(with: .random(), at: location) + } + } + +}