From 1429c51821641c13e34465ae9a315d90074c6752 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Mon, 16 Dec 2024 12:28:38 -0500 Subject: [PATCH] feat: Adds output values to some of the commands to allow them to be piped into other commands --- Sources/CommandClient/CommandClient.swift | 39 ++++++++-- .../PlaybookClient+RunPlaybook.swift | 15 ++-- Sources/PlaybookClient/PlaybookClient.swift | 2 +- Sources/VaultClient/VaultClient.swift | 74 +++++++++---------- Sources/hpa/CreateCommand.swift | 1 + .../UtilsCommands/CreateTemplateCommand.swift | 4 +- .../hpa/VaultCommands/DecryptCommand.swift | 5 +- .../hpa/VaultCommands/EncryptCommand.swift | 5 +- Sources/hpa/VaultCommands/VaultOptions.swift | 2 - .../PlaybookClientTests.swift | 4 +- Tests/VaultClientTests/VaultClientTests.swift | 25 +++++-- 11 files changed, 112 insertions(+), 64 deletions(-) diff --git a/Sources/CommandClient/CommandClient.swift b/Sources/CommandClient/CommandClient.swift index 4207536..7a8c075 100644 --- a/Sources/CommandClient/CommandClient.swift +++ b/Sources/CommandClient/CommandClient.swift @@ -19,6 +19,30 @@ public struct CommandClient: Sendable { /// Runs a shell command. public var runCommand: @Sendable (RunCommandOptions) async throws -> Void + /// Runs a shell command, sets up logging, and returns an output value. + /// + /// This is useful when you need to give some output value to the caller, + /// generally for the ability of that output to be piped into other commands. + /// + @discardableResult + public func run( + logging logginOptions: LoggingOptions, + quiet: Bool = false, + shell: String? = nil, + in workingDirectory: String? = nil, + makeArguments: @Sendable @escaping () async throws -> ([String], T) + ) async throws -> T { + try await logginOptions.withLogger { + let (arguments, returnValue) = try await makeArguments() + try await self.run( + quiet: quiet, + shell: shell, + arguments + ) + return returnValue + } + } + /// Runs a shell command and sets up logging. public func run( logging logginOptions: LoggingOptions, @@ -27,14 +51,13 @@ public struct CommandClient: Sendable { in workingDirectory: String? = nil, makeArguments: @Sendable @escaping () async throws -> [String] ) async throws { - try await logginOptions.withLogger { - let arguments = try await makeArguments() - try await self.run( - quiet: quiet, - shell: shell, - arguments - ) - } + try await run( + logging: logginOptions, + quiet: quiet, + shell: shell, + in: workingDirectory, + makeArguments: { try await (makeArguments(), ()) } + ) } /// Runs a shell command. diff --git a/Sources/PlaybookClient/PlaybookClient+RunPlaybook.swift b/Sources/PlaybookClient/PlaybookClient+RunPlaybook.swift index a604f72..6bdd7d9 100644 --- a/Sources/PlaybookClient/PlaybookClient+RunPlaybook.swift +++ b/Sources/PlaybookClient/PlaybookClient+RunPlaybook.swift @@ -53,7 +53,7 @@ extension PlaybookClient.RunPlaybook.CreateOptions { } extension PlaybookClient.RunPlaybook.GenerateTemplateOptions { - func run() async throws { + func run() async throws -> String { try await shared.run { arguments, _ in arguments.append(contentsOf: [ "--tags", "repo-template", @@ -67,16 +67,21 @@ extension PlaybookClient.RunPlaybook.GenerateTemplateOptions { if !useVault { arguments.append(contentsOf: ["--extra-vars", "use_vault=false"]) } + + return templateDirectory } } } extension PlaybookClient.RunPlaybook.SharedRunOptions { - func run(_ apply: @Sendable @escaping (inout [String], Configuration) throws -> Void) async throws { + @discardableResult + func run( + _ apply: @Sendable @escaping (inout [String], Configuration) throws -> T + ) async throws -> T { @Dependency(\.commandClient) var commandClient - try await commandClient.run( + return try await commandClient.run( logging: loggingOptions, quiet: quiet, shell: shell @@ -91,9 +96,9 @@ extension PlaybookClient.RunPlaybook.SharedRunOptions { inventoryFilePath: inventoryFilePath ) - try apply(&arguments, configuration) + let output = try apply(&arguments, configuration) - return arguments + return (arguments, output) } } } diff --git a/Sources/PlaybookClient/PlaybookClient.swift b/Sources/PlaybookClient/PlaybookClient.swift index 9b5b713..9bf84f6 100644 --- a/Sources/PlaybookClient/PlaybookClient.swift +++ b/Sources/PlaybookClient/PlaybookClient.swift @@ -44,7 +44,7 @@ public extension PlaybookClient { struct RunPlaybook: Sendable { public var buildProject: @Sendable (BuildOptions) async throws -> Void public var createProject: @Sendable (CreateOptions, JSONEncoder?) async throws -> Void - public var generateTemplate: @Sendable (GenerateTemplateOptions) async throws -> Void + public var generateTemplate: @Sendable (GenerateTemplateOptions) async throws -> String public func createProject(_ options: CreateOptions) async throws { try await createProject(options, nil) diff --git a/Sources/VaultClient/VaultClient.swift b/Sources/VaultClient/VaultClient.swift index b70a301..9795314 100644 --- a/Sources/VaultClient/VaultClient.swift +++ b/Sources/VaultClient/VaultClient.swift @@ -15,7 +15,13 @@ public extension DependencyValues { @DependencyClient public struct VaultClient: Sendable { - public var run: @Sendable (RunOptions) async throws -> Void + public var run: Run + + @DependencyClient + public struct Run: Sendable { + public var decrypt: @Sendable (RunOptions) async throws -> String + public var encrypt: @Sendable (RunOptions) async throws -> String + } public struct RunOptions: Equatable, Sendable { @@ -23,12 +29,10 @@ public struct VaultClient: Sendable { public let loggingOptions: LoggingOptions public let outputFilePath: String? public let quiet: Bool - public let route: Route public let shell: String? public let vaultFilePath: String? public init( - _ route: Route, extraOptions: [String]? = nil, loggingOptions: LoggingOptions, outputFilePath: String? = nil, @@ -40,28 +44,34 @@ public struct VaultClient: Sendable { self.loggingOptions = loggingOptions self.outputFilePath = outputFilePath self.quiet = quiet - self.route = route self.shell = shell self.vaultFilePath = vaultFilePath } + } - public enum Route: String, Equatable, Sendable { - case encrypt - case decrypt + @_spi(Internal) + public enum Route: String, Equatable, Sendable { + case encrypt + case decrypt - @_spi(Internal) - public var verb: String { rawValue } - } + public var verb: String { rawValue } } } extension VaultClient: DependencyKey { - public static let testValue: VaultClient = Self() + public static let testValue: VaultClient = Self(run: Run()) public static var liveValue: VaultClient { .init( - run: { try await $0.run() } + run: .init( + decrypt: { + try await $0.run(route: .decrypt) + }, + encrypt: { + try await $0.run(route: .encrypt) + } + ) ) } } @@ -75,10 +85,18 @@ public extension VaultClient { extension VaultClient.RunOptions { - func run() async throws { + // Sets up the default arguments and runs the `ansible-vault` command, + // returning the output file path, which is either supplied by the caller + // or found via the `fileClient.findVaultFileInCurrentDirectory`. + // + // This allows the output to be piped into other commands. + // + // + @discardableResult + func run(route: VaultClient.Route) async throws -> String { @Dependency(\.commandClient) var commandClient - try await commandClient.run( + return try await commandClient.run( logging: loggingOptions, quiet: quiet, shell: shell @@ -87,6 +105,8 @@ extension VaultClient.RunOptions { @Dependency(\.fileClient) var fileClient @Dependency(\.logger) var logger + var output: String? + let configuration = try await configurationClient.findAndLoad() logger.trace("Configuration: \(configuration)") @@ -101,6 +121,7 @@ extension VaultClient.RunOptions { guard let vaultFilePath else { throw VaultClientError.vaultFileNotFound } + output = vaultFilePath logger.trace("Vault file: \(vaultFilePath)") @@ -111,6 +132,7 @@ extension VaultClient.RunOptions { if let outputFilePath { arguments.append(contentsOf: ["--output", outputFilePath]) + output = outputFilePath } if let extraOptions { @@ -132,33 +154,11 @@ extension VaultClient.RunOptions { logger.trace("Arguments: \(arguments)") - return arguments + return (arguments, output ?? "") } } } -// extension VaultClient.RunOptions.Route { -// -// var arguments: [String] { -// let outputFile: String? -// var arguments: [String] -// -// switch self { -// case let .decrypt(outputFile: output): -// outputFile = output -// arguments = ["decrypt"] -// case let .encrypt(outputFile: output): -// outputFile = output -// arguments = ["encrypt"] -// } -// -// if let outputFile { -// arguments.append(contentsOf: ["--output", outputFile]) -// } -// return arguments -// } -// } - enum VaultClientError: Error { case vaultFileNotFound } diff --git a/Sources/hpa/CreateCommand.swift b/Sources/hpa/CreateCommand.swift index 8be86a4..9e23c4e 100644 --- a/Sources/hpa/CreateCommand.swift +++ b/Sources/hpa/CreateCommand.swift @@ -68,6 +68,7 @@ struct CreateCommand: AsyncParsableCommand { template: .init(directory: templateDir), useLocalTemplateDirectory: localTemplateDir )) + print(projectDir) } } diff --git a/Sources/hpa/UtilsCommands/CreateTemplateCommand.swift b/Sources/hpa/UtilsCommands/CreateTemplateCommand.swift index 2b24cce..aaef502 100644 --- a/Sources/hpa/UtilsCommands/CreateTemplateCommand.swift +++ b/Sources/hpa/UtilsCommands/CreateTemplateCommand.swift @@ -42,7 +42,7 @@ struct GenerateProjectTemplateCommand: AsyncParsableCommand { mutating func run() async throws { @Dependency(\.playbookClient) var playbookClient - try await playbookClient.run.generateTemplate(.init( + let output = try await playbookClient.run.generateTemplate(.init( shared: globals.sharedPlaybookRunOptions( commandName: Self.commandName, extraOptions: extraOptions @@ -51,5 +51,7 @@ struct GenerateProjectTemplateCommand: AsyncParsableCommand { templateVarsDirectory: templateVars, useVault: !noVault )) + + print(output) } } diff --git a/Sources/hpa/VaultCommands/DecryptCommand.swift b/Sources/hpa/VaultCommands/DecryptCommand.swift index c8ad6b0..939f7e8 100644 --- a/Sources/hpa/VaultCommands/DecryptCommand.swift +++ b/Sources/hpa/VaultCommands/DecryptCommand.swift @@ -23,10 +23,11 @@ struct DecryptCommand: AsyncParsableCommand { mutating func run() async throws { @Dependency(\.vaultClient) var vaultClient - try await vaultClient.run(options.runOptions( + let output = try await vaultClient.run.decrypt(options.runOptions( commandName: Self.commandName, - route: .decrypt, outputFilePath: output )) + + print(output) } } diff --git a/Sources/hpa/VaultCommands/EncryptCommand.swift b/Sources/hpa/VaultCommands/EncryptCommand.swift index 5fb9542..8910862 100644 --- a/Sources/hpa/VaultCommands/EncryptCommand.swift +++ b/Sources/hpa/VaultCommands/EncryptCommand.swift @@ -23,10 +23,11 @@ struct EncryptCommand: AsyncParsableCommand { mutating func run() async throws { @Dependency(\.vaultClient) var vaultClient - try await vaultClient.run(options.runOptions( + let output = try await vaultClient.run.encrypt(options.runOptions( commandName: Self.commandName, - route: .encrypt, outputFilePath: output )) + + print(output) } } diff --git a/Sources/hpa/VaultCommands/VaultOptions.swift b/Sources/hpa/VaultCommands/VaultOptions.swift index 34d6c50..58f28d8 100644 --- a/Sources/hpa/VaultCommands/VaultOptions.swift +++ b/Sources/hpa/VaultCommands/VaultOptions.swift @@ -42,11 +42,9 @@ extension VaultOptions { func runOptions( commandName: String, - route: VaultClient.RunOptions.Route, outputFilePath: String? = nil ) -> VaultClient.RunOptions { .init( - route, extraOptions: extraOptions, loggingOptions: .init( commandName: commandName, diff --git a/Tests/PlaybookClientTests/PlaybookClientTests.swift b/Tests/PlaybookClientTests/PlaybookClientTests.swift index a9bde0e..c869578 100644 --- a/Tests/PlaybookClientTests/PlaybookClientTests.swift +++ b/Tests/PlaybookClientTests/PlaybookClientTests.swift @@ -151,11 +151,13 @@ struct PlaybookClientTests: TestCase { } run: { @Dependency(\.playbookClient) var playbookClient - try await playbookClient.run.generateTemplate(.init( + let output = try await playbookClient.run.generateTemplate(.init( shared: Self.sharedRunOptions, templateDirectory: "/template" )) + #expect(output == "/template") + } assert: { output in let expected = [ diff --git a/Tests/VaultClientTests/VaultClientTests.swift b/Tests/VaultClientTests/VaultClientTests.swift index e40d3d2..62cde73 100644 --- a/Tests/VaultClientTests/VaultClientTests.swift +++ b/Tests/VaultClientTests/VaultClientTests.swift @@ -19,13 +19,20 @@ struct VaultClientTests: TestCase { } run: { @Dependency(\.vaultClient) var vaultClient - try await vaultClient.run(.init( - .decrypt, + let output = try await vaultClient.run.decrypt(.init( extraOptions: input.extraOptions, loggingOptions: Self.loggingOptions, outputFilePath: input.outputFilePath, vaultFilePath: input.vaultFilePath )) + + if let outputFilePath = input.outputFilePath { + #expect(output == outputFilePath) + } else if let vaultFilePath = input.vaultFilePath { + #expect(output == vaultFilePath) + } else { + #expect(output == "/vault.yml") + } } assert: { options in #expect(options.arguments == input.expected(.decrypt)) @@ -43,13 +50,21 @@ struct VaultClientTests: TestCase { } run: { @Dependency(\.vaultClient) var vaultClient - try await vaultClient.run(.init( - .encrypt, + let output = try await vaultClient.run.encrypt(.init( extraOptions: input.extraOptions, loggingOptions: Self.loggingOptions, outputFilePath: input.outputFilePath, vaultFilePath: input.vaultFilePath )) + + if let outputFilePath = input.outputFilePath { + #expect(output == outputFilePath) + } else if let vaultFilePath = input.vaultFilePath { + #expect(output == vaultFilePath) + } else { + #expect(output == "/vault.yml") + } + } assert: { options in #expect(options.arguments == input.expected(.encrypt)) } @@ -75,7 +90,7 @@ struct TestOptions: Sendable { self.vaultFilePath = vaultFilePath } - func expected(_ route: VaultClient.RunOptions.Route) -> [String] { + func expected(_ route: VaultClient.Route) -> [String] { var expected = [ "ansible-vault", "\(route.verb)" ]