From 7b30b78b67319e7b5016e098a29a14fcd0a423f6 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Thu, 12 Dec 2024 11:16:22 -0500 Subject: [PATCH] feat: Moves logging setup and generate-json for the create command to cli-client module. --- Sources/CliClient/CliClient+Commands.swift | 139 ++++++++-------- Sources/CliClient/CliClientError.swift | 9 ++ Sources/CliClient/GenerateJson.swift | 80 +++++++++ Sources/CliClient/Interface.swift | 51 +++++- Sources/CliClient/LoggingExtensions.swift | 28 ++++ Sources/hpa/BuildCommand.swift | 23 ++- Sources/hpa/CreateCommand.swift | 126 +++------------ Sources/hpa/Internal/GlobalOptions.swift | 19 +++ Sources/hpa/Internal/LoggingExtensions.swift | 33 ---- .../UtilsCommands/GenerateConfigCommand.swift | 5 +- .../hpa/VaultCommands/DecryptCommand.swift | 3 +- .../hpa/VaultCommands/EncryptCommand.swift | 3 +- Sources/hpa/VaultCommands/VaultOptions.swift | 7 + Tests/CliClientTests/CliClientTests.swift | 153 +++++++++++++++++- 14 files changed, 449 insertions(+), 230 deletions(-) create mode 100644 Sources/CliClient/CliClientError.swift create mode 100644 Sources/CliClient/GenerateJson.swift create mode 100644 Sources/CliClient/LoggingExtensions.swift diff --git a/Sources/CliClient/CliClient+Commands.swift b/Sources/CliClient/CliClient+Commands.swift index 115ae43..3abbd77 100644 --- a/Sources/CliClient/CliClient+Commands.swift +++ b/Sources/CliClient/CliClient+Commands.swift @@ -23,81 +23,91 @@ public extension CliClient { try await runCommand(quiet: quiet, shell: shell, args) } - func runPlaybookCommand(_ options: PlaybookOptions) async throws { - @Dependency(\.configurationClient) var configurationClient - @Dependency(\.logger) var logger + func runPlaybookCommand( + _ options: PlaybookOptions, + logging loggingOptions: LoggingOptions + ) async throws { + try await withLogger(loggingOptions) { + @Dependency(\.configurationClient) var configurationClient + @Dependency(\.logger) var logger - let configuration = try await configurationClient.ensuredConfiguration(options.configuration) - logger.trace("Configuration: \(configuration)") + let configuration = try await configurationClient.ensuredConfiguration(options.configuration) + logger.trace("Configuration: \(configuration)") - let playbookDirectory = try configuration.ensuredPlaybookDirectory(options.playbookDirectory) - let playbookPath = "\(playbookDirectory)/\(Constants.playbookFileName)" - logger.trace("Playbook path: \(playbookPath)") + let playbookDirectory = try configuration.ensuredPlaybookDirectory(options.playbookDirectory) + let playbookPath = "\(playbookDirectory)/\(Constants.playbookFileName)" + logger.trace("Playbook path: \(playbookPath)") - let inventoryPath = ensuredInventoryPath( - options.inventoryFilePath, - configuration: configuration, - playbookDirectory: playbookDirectory - ) - logger.trace("Inventory path: \(inventoryPath)") + let inventoryPath = ensuredInventoryPath( + options.inventoryFilePath, + configuration: configuration, + playbookDirectory: playbookDirectory + ) + logger.trace("Inventory path: \(inventoryPath)") - var arguments = [ - Constants.playbookCommand, playbookPath, - "--inventory", inventoryPath - ] + options.arguments + var arguments = [ + Constants.playbookCommand, playbookPath, + "--inventory", inventoryPath + ] + options.arguments - if let defaultArgs = configuration.args { - arguments.append(contentsOf: defaultArgs) + if let defaultArgs = configuration.args { + arguments.append(contentsOf: defaultArgs) + } + + if configuration.useVaultArgs, let vaultArgs = configuration.vault.args { + arguments.append(contentsOf: vaultArgs) + } + + logger.trace("Running playbook command with arguments: \(arguments)") + + try await runCommand( + quiet: options.quiet, + shell: options.shell.orDefault, + arguments + ) } - - if configuration.useVaultArgs, let vaultArgs = configuration.vault.args { - arguments.append(contentsOf: vaultArgs) - } - - logger.trace("Running playbook command with arguments: \(arguments)") - - try await runCommand( - quiet: options.quiet, - shell: options.shell.orDefault, - arguments - ) } - func runVaultCommand(_ options: VaultOptions) async throws { - @Dependency(\.configurationClient) var configurationClient - @Dependency(\.fileClient) var fileClient - @Dependency(\.logger) var logger + func runVaultCommand( + _ options: VaultOptions, + logging loggingOptions: LoggingOptions + ) async throws { + try await withLogger(loggingOptions) { + @Dependency(\.configurationClient) var configurationClient + @Dependency(\.fileClient) var fileClient + @Dependency(\.logger) var logger - let configuration = try await configurationClient.ensuredConfiguration(options.configuration) - logger.trace("Configuration: \(configuration)") + let configuration = try await configurationClient.ensuredConfiguration(options.configuration) + logger.trace("Configuration: \(configuration)") - let vaultFilePath = try await fileClient.ensuredVaultFilePath(options.vaultFilePath) - logger.trace("Vault file: \(vaultFilePath)") + let vaultFilePath = try await fileClient.ensuredVaultFilePath(options.vaultFilePath) + logger.trace("Vault file: \(vaultFilePath)") - var arguments = [ - Constants.vaultCommand - ] + options.arguments + var arguments = [ + Constants.vaultCommand + ] + options.arguments - if let defaultArgs = configuration.vault.args { - arguments.append(contentsOf: defaultArgs) + if let defaultArgs = configuration.vault.args { + arguments.append(contentsOf: defaultArgs) + } + + if arguments.contains("encrypt"), + !arguments.contains("--encrypt-vault-id"), + let id = configuration.vault.encryptId + { + arguments.append(contentsOf: ["--encrypt-vault-id", id]) + } + + arguments.append(vaultFilePath) + + logger.trace("Running vault command with arguments: \(arguments)") + + try await runCommand( + quiet: options.quiet, + shell: options.shell.orDefault, + arguments + ) } - - if arguments.contains("encrypt"), - !arguments.contains("--encrypt-vault-id"), - let id = configuration.vault.encryptId - { - arguments.append(contentsOf: ["--encrypt-vault-id", id]) - } - - arguments.append(vaultFilePath) - - logger.trace("Running vault command with arguments: \(arguments)") - - try await runCommand( - quiet: options.quiet, - shell: options.shell.orDefault, - arguments - ) } } @@ -162,9 +172,4 @@ public extension FileClient { } } -enum CliClientError: Error { - case playbookDirectoryNotFound - case vaultFileNotFound -} - extension ShellCommand.Shell: @retroactive @unchecked Sendable {} diff --git a/Sources/CliClient/CliClientError.swift b/Sources/CliClient/CliClientError.swift new file mode 100644 index 0000000..592ee7e --- /dev/null +++ b/Sources/CliClient/CliClientError.swift @@ -0,0 +1,9 @@ +import Foundation + +public enum CliClientError: Error { + case encodingError + case playbookDirectoryNotFound + case templateDirectoryNotFound + case templateDirectoryOrRepoNotSpecified + case vaultFileNotFound +} diff --git a/Sources/CliClient/GenerateJson.swift b/Sources/CliClient/GenerateJson.swift new file mode 100644 index 0000000..7e4b534 --- /dev/null +++ b/Sources/CliClient/GenerateJson.swift @@ -0,0 +1,80 @@ +import ConfigurationClient +import Dependencies +import Foundation + +// NOTE: We're not using the `Coders` client because we generally do not +// want the output to be `prettyPrinted` or anything, unless we're running +// tests, so we use a supplied json encoder. + +func createJSONData( + _ options: CliClient.GenerateJsonOptions, + logging loggingOptions: CliClient.LoggingOptions, + encoder: JSONEncoder = .init() +) async throws -> Data { + try await CliClient.withLogger(loggingOptions) { + @Dependency(\.logger) var logger + @Dependency(\.configurationClient) var configurationClient + + let configuration = try await configurationClient.findAndLoad() + + let templateDir = options.templateDirectory ?? configuration.template.directory + let templateRepo = options.templateRepo ?? configuration.template.url + let version = options.version ?? configuration.template.version + + logger.debug(""" + (\(options.useLocalTemplateDirectory), \(String(describing: templateDir)), \(String(describing: templateRepo))) + """) + + switch (options.useLocalTemplateDirectory, templateDir, templateRepo) { + case (true, .none, _): + // User supplied they wanted to use a local template directory, but we could not find + // the path set from command line or in configuration. + throw CliClientError.templateDirectoryNotFound + case let (false, _, .some(repo)): + // User did not supply they wanted to use a local template directory, and we found a repo url that was + // either set by the command line or found in the configuration. + logger.debug("Using repo.") + return try encoder.encode(TemplateRepo(repo: repo, version: version)) + case let (true, .some(templateDir), _): + // User supplied they wanted to use a local template directory, and we found the template directory + // either set by the command line or in the configuration. + logger.debug("Using template directory.") + return try encoder.encode(TemplateDirJson(path: templateDir)) + case let (false, .some(templateDir), _): + // User supplied they did not wanted to use a local template directory, and we found the template directory + // either set by the command line or in the configuration, and no repo was found / handled previously. + logger.debug("Using template directory.") + return try encoder.encode(TemplateDirJson(path: templateDir)) + case (_, .none, .none): + // We could not find a repo or template directory. + throw CliClientError.templateDirectoryOrRepoNotSpecified + } + } +} + +private struct TemplateDirJson: Encodable { + + let template: Template + + init(path: String) { + self.template = .init(path: path) + } + + struct Template: Encodable { + let path: String + } +} + +private struct TemplateRepo: Encodable { + + let template: Template + + init(repo: String, version: String?) { + self.template = .init(repo: repo, version: version ?? "main") + } + + struct Template: Encodable { + let repo: String + let version: String + } +} diff --git a/Sources/CliClient/Interface.swift b/Sources/CliClient/Interface.swift index 19efbba..ec3f228 100644 --- a/Sources/CliClient/Interface.swift +++ b/Sources/CliClient/Interface.swift @@ -4,9 +4,6 @@ import DependenciesMacros import Foundation import ShellClient -// TODO: Add logging options and setup in this module and remove from the -// executable `hpa` module. - public extension DependencyValues { var cliClient: CliClient { get { self[CliClient.self] } @@ -16,11 +13,49 @@ public extension DependencyValues { @DependencyClient public struct CliClient: Sendable { - public var runCommand: @Sendable (RunCommandOptions) async throws -> Void + public var generateJSON: @Sendable (GenerateJsonOptions, LoggingOptions, JSONEncoder) async throws -> String + + public func generateJSON( + _ options: GenerateJsonOptions, + logging loggingOptions: LoggingOptions, + encoder jsonEncoder: JSONEncoder = .init() + ) async throws -> String { + try await generateJSON(options, loggingOptions, jsonEncoder) + } } public extension CliClient { + + struct GenerateJsonOptions: Equatable, Sendable { + let templateDirectory: String? + let templateRepo: String? + let version: String? + let useLocalTemplateDirectory: Bool + + public init( + templateDirectory: String?, + templateRepo: String?, + version: String?, + useLocalTemplateDirectory: Bool + ) { + self.templateDirectory = templateDirectory + self.templateRepo = templateRepo + self.version = version + self.useLocalTemplateDirectory = useLocalTemplateDirectory + } + } + + struct LoggingOptions: Equatable, Sendable { + let commandName: String + let logLevel: Logger.Level + + public init(commandName: String, logLevel: Logger.Level) { + self.commandName = commandName + self.logLevel = logLevel + } + } + struct PlaybookOptions: Sendable, Equatable { let arguments: [String] let configuration: Configuration? @@ -109,6 +144,12 @@ extension CliClient: DependencyKey { options.arguments )) } + } generateJSON: { options, loggingOptions, encoder in + let data = try await createJSONData(options, logging: loggingOptions, encoder: encoder) + guard let string = String(data: data, encoding: .utf8) else { + throw CliClientError.encodingError + } + return string } } @@ -121,6 +162,8 @@ extension CliClient: DependencyKey { public static func capturing(_ client: CapturingClient) -> Self { .init { options in await client.set(options) + } generateJSON: { + try await Self().generateJSON($0, $1, $2) } } diff --git a/Sources/CliClient/LoggingExtensions.swift b/Sources/CliClient/LoggingExtensions.swift new file mode 100644 index 0000000..5be5973 --- /dev/null +++ b/Sources/CliClient/LoggingExtensions.swift @@ -0,0 +1,28 @@ +import Dependencies +import Logging +import ShellClient + +public extension CliClient { + + @discardableResult + func withLogger( + _ options: LoggingOptions, + operation: @Sendable @escaping () async throws -> T + ) async rethrows -> T { + try await Self.withLogger(options, operation: operation) + } + + @discardableResult + static func withLogger( + _ options: LoggingOptions, + operation: @Sendable @escaping () async throws -> T + ) async rethrows -> T { + try await withDependencies { + $0.logger = .init(label: "\(Constants.executableName)") + $0.logger.logLevel = options.logLevel + $0.logger[metadataKey: "command"] = "\(options.commandName.blue)" + } operation: { + try await operation() + } + } +} diff --git a/Sources/hpa/BuildCommand.swift b/Sources/hpa/BuildCommand.swift index 2252e1f..726b4a9 100644 --- a/Sources/hpa/BuildCommand.swift +++ b/Sources/hpa/BuildCommand.swift @@ -38,18 +38,17 @@ struct BuildCommand: AsyncParsableCommand { } private func _run() async throws { - try await withSetupLogger(commandName: Self.commandName, globals: globals) { - @Dependency(\.cliClient) var cliClient + @Dependency(\.cliClient) var cliClient - try await cliClient.runPlaybookCommand( - globals.playbookOptions( - arguments: [ - "--tags", "build-project", - "--extra-vars", "project_dir=\(self.projectDir)" - ], - configuration: nil - ) - ) - } + try await cliClient.runPlaybookCommand( + globals.playbookOptions( + arguments: [ + "--tags", "build-project", + "--extra-vars", "project_dir=\(projectDir)" + ], + configuration: nil + ), + logging: globals.loggingOptions(commandName: Self.commandName) + ) } } diff --git a/Sources/hpa/CreateCommand.swift b/Sources/hpa/CreateCommand.swift index 13c5bdc..b611a24 100644 --- a/Sources/hpa/CreateCommand.swift +++ b/Sources/hpa/CreateCommand.swift @@ -61,126 +61,46 @@ struct CreateCommand: AsyncParsableCommand { } private func _run() async throws { - try await withSetupLogger(commandName: Self.commandName, globals: globals) { - @Dependency(\.coders) var coders - @Dependency(\.cliClient) var cliClient - @Dependency(\.configurationClient) var configurationClient + @Dependency(\.coders) var coders + @Dependency(\.cliClient) var cliClient + @Dependency(\.configurationClient) var configurationClient + + let loggingOptions = globals.loggingOptions(commandName: Self.commandName) + + try await cliClient.withLogger(loggingOptions) { @Dependency(\.logger) var logger - let encoder = coders.jsonEncoder() - - let configuration = try await configurationClient.findAndLoad() - - logger.debug("Configuration: \(configuration)") - - let jsonData = try parseOptions( - command: self, - configuration: configuration, - logger: logger, - encoder: encoder + let json = try await cliClient.generateJSON( + generateJsonOptions, + logging: loggingOptions ) - guard let jsonString = String(data: jsonData, encoding: .utf8) else { - throw CreateError.encodingError - } - - logger.debug("JSON string: \(jsonString)") + logger.debug("JSON string: \(json)") let arguments = [ "--tags", "setup-project", "--extra-vars", "project_dir=\(self.projectDir)", - "--extra-vars", "'\(jsonString)'" + "--extra-vars", "'\(json)'" ] + extraArgs try await cliClient.runPlaybookCommand( globals.playbookOptions( arguments: arguments, - configuration: configuration - ) + configuration: nil + ), + logging: loggingOptions ) - -// try await runPlaybook( -// commandName: Self.commandName, -// globals: self.globals, -// configuration: configuration, -// extraArgs: extraArgs, -// "--tags", "setup-project", -// "--extra-vars", "project_dir=\(self.projectDir)", -// "--extra-vars", "'\(jsonString)'" -// ) } } } -private func parseOptions( - command: CreateCommand, - configuration: Configuration, - logger: Logger, - encoder: JSONEncoder -) throws -> Data { - let templateDir = command.templateDir ?? configuration.template.directory - let templateRepo = command.repo ?? configuration.template.url - let version = (command.branch ?? configuration.template.version) ?? "main" - - logger.debug(""" - (\(command.localTemplateDir), \(String(describing: templateDir)), \(String(describing: templateRepo))) - """) - - switch (command.localTemplateDir, templateDir, templateRepo) { - case (true, .none, _): - // User supplied they wanted to use a local template directory, but we could not find - // the path set from command line or in configuration. - throw CreateError.templateDirNotFound - case let (false, _, .some(repo)): - // User did not supply they wanted to use a local template directory, and we found a repo url that was - // either set by the command line or found in the configuration. - logger.debug("Using repo.") - return try encoder.encode(TemplateRepo(repo: repo, version: version)) - case let (true, .some(templateDir), _): - // User supplied they wanted to use a local template directory, and we found the template directory - // either set by the command line or in the configuration. - logger.debug("Using template directory.") - return try encoder.encode(TemplateDirJson(path: templateDir)) - case let (false, .some(templateDir), _): - // User supplied they did not wanted to use a local template directory, and we found the template directory - // either set by the command line or in the configuration, and no repo was found / handled previously. - logger.debug("Using template directory.") - return try encoder.encode(TemplateDirJson(path: templateDir)) - case (_, .none, .none): - // We could not find a repo or template directory. - throw CreateError.templateDirOrRepoNotSpecified +private extension CreateCommand { + var generateJsonOptions: CliClient.GenerateJsonOptions { + .init( + templateDirectory: templateDir, + templateRepo: repo, + version: branch, + useLocalTemplateDirectory: localTemplateDir + ) } } - -private struct TemplateDirJson: Encodable { - - let template: Template - - init(path: String) { - self.template = .init(path: path) - } - - struct Template: Encodable { - let path: String - } -} - -private struct TemplateRepo: Encodable { - - let template: Template - - init(repo: String, version: String?) { - self.template = .init(repo: repo, version: version ?? "main") - } - - struct Template: Encodable { - let repo: String - let version: String - } -} - -enum CreateError: Error { - case encodingError - case templateDirNotFound - case templateDirOrRepoNotSpecified -} diff --git a/Sources/hpa/Internal/GlobalOptions.swift b/Sources/hpa/Internal/GlobalOptions.swift index c33a253..77fa51e 100644 --- a/Sources/hpa/Internal/GlobalOptions.swift +++ b/Sources/hpa/Internal/GlobalOptions.swift @@ -1,4 +1,5 @@ import ArgumentParser +import CliClient struct BasicGlobalOptions: ParsableArguments { @Flag( @@ -55,3 +56,21 @@ struct GlobalOptions: ParsableArguments { } } + +extension GlobalOptions { + func loggingOptions(commandName: String) -> CliClient.LoggingOptions { + .init( + commandName: commandName, + logLevel: .init(globals: basic, quietOnlyPlaybook: quietOnlyPlaybook) + ) + } +} + +extension BasicGlobalOptions { + func loggingOptions(commandName: String) -> CliClient.LoggingOptions { + .init( + commandName: commandName, + logLevel: .init(globals: self, quietOnlyPlaybook: false) + ) + } +} diff --git a/Sources/hpa/Internal/LoggingExtensions.swift b/Sources/hpa/Internal/LoggingExtensions.swift index 1835682..6c37677 100644 --- a/Sources/hpa/Internal/LoggingExtensions.swift +++ b/Sources/hpa/Internal/LoggingExtensions.swift @@ -1,7 +1,5 @@ -import Dependencies import Logging -// TODO: Move some of this to the cli-client module. extension Logger.Level { /// Set the log level based on the user's options supplied. @@ -22,34 +20,3 @@ extension Logger.Level { self = .info } } - -func withSetupLogger( - commandName: String, - globals: BasicGlobalOptions, - quietOnlyPlaybook: Bool = false, - dependencies setupDependencies: (inout DependencyValues) -> Void = { _ in }, - operation: @escaping () async throws -> Void -) async rethrows { - try await withDependencies { - $0.logger = .init(label: "\("hpa".yellow)") - $0.logger.logLevel = .init(globals: globals, quietOnlyPlaybook: quietOnlyPlaybook) - $0.logger[metadataKey: "command"] = "\(commandName.blue)" - } operation: { - try await operation() - } -} - -func withSetupLogger( - commandName: String, - globals: GlobalOptions, - dependencies setupDependencies: (inout DependencyValues) -> Void = { _ in }, - operation: @escaping () async throws -> Void -) async rethrows { - try await withSetupLogger( - commandName: commandName, - globals: globals.basic, - quietOnlyPlaybook: globals.quietOnlyPlaybook, - dependencies: setupDependencies, - operation: operation - ) -} diff --git a/Sources/hpa/UtilsCommands/GenerateConfigCommand.swift b/Sources/hpa/UtilsCommands/GenerateConfigCommand.swift index 81e2eba..e993f8c 100644 --- a/Sources/hpa/UtilsCommands/GenerateConfigCommand.swift +++ b/Sources/hpa/UtilsCommands/GenerateConfigCommand.swift @@ -50,9 +50,8 @@ struct GenerateConfigurationCommand: AsyncParsableCommand { // FIX: private func _run() async throws { - try await withSetupLogger(commandName: Self.commandName, globals: globals) { - @Dependency(\.cliClient) var cliClient - + @Dependency(\.cliClient) var cliClient + try await cliClient.withLogger(globals.loggingOptions(commandName: Self.commandName)) { let actualPath: String // if let path { diff --git a/Sources/hpa/VaultCommands/DecryptCommand.swift b/Sources/hpa/VaultCommands/DecryptCommand.swift index 2cc9aea..8b63b0f 100644 --- a/Sources/hpa/VaultCommands/DecryptCommand.swift +++ b/Sources/hpa/VaultCommands/DecryptCommand.swift @@ -29,7 +29,8 @@ struct DecryptCommand: AsyncParsableCommand { } try await cliClient.runVaultCommand( - options.vaultOptions(arguments: args, configuration: nil) + options.vaultOptions(arguments: args, configuration: nil), + logging: options.loggingOptions(commandName: Self.commandName) ) // try await runVault( diff --git a/Sources/hpa/VaultCommands/EncryptCommand.swift b/Sources/hpa/VaultCommands/EncryptCommand.swift index f8a325d..f554de8 100644 --- a/Sources/hpa/VaultCommands/EncryptCommand.swift +++ b/Sources/hpa/VaultCommands/EncryptCommand.swift @@ -28,7 +28,8 @@ struct EncryptCommand: AsyncParsableCommand { args.append(contentsOf: ["--output", output]) } try await cliClient.runVaultCommand( - options.vaultOptions(arguments: args, configuration: nil) + options.vaultOptions(arguments: args, configuration: nil), + logging: options.loggingOptions(commandName: Self.commandName) ) // try await runVault( diff --git a/Sources/hpa/VaultCommands/VaultOptions.swift b/Sources/hpa/VaultCommands/VaultOptions.swift index bce4c9d..47ff701 100644 --- a/Sources/hpa/VaultCommands/VaultOptions.swift +++ b/Sources/hpa/VaultCommands/VaultOptions.swift @@ -1,4 +1,5 @@ import ArgumentParser +import CliClient // Holds the common options for vault commands, as they all share the // same structure. @@ -29,3 +30,9 @@ struct VaultOptions: ParsableArguments { } } + +extension VaultOptions { + func loggingOptions(commandName: String) -> CliClient.LoggingOptions { + globals.loggingOptions(commandName: commandName) + } +} diff --git a/Tests/CliClientTests/CliClientTests.swift b/Tests/CliClientTests/CliClientTests.swift index e3f75e0..8a6bbd4 100644 --- a/Tests/CliClientTests/CliClientTests.swift +++ b/Tests/CliClientTests/CliClientTests.swift @@ -10,6 +10,12 @@ import TestSupport @Suite("CliClientTests") struct CliClientTests: TestCase { + static let loggingOptions: CliClient.LoggingOptions = { + let levelString = ProcessInfo.processInfo.environment["LOGGING_LEVEL"] ?? "debug" + let logLevel = Logger.Level(rawValue: levelString) ?? .debug + return .init(commandName: "CliClientTests", logLevel: logLevel) + }() + @Test func capturingClient() async throws { let captured = CliClient.CapturingClient() @@ -35,7 +41,10 @@ struct CliClientTests: TestCase { @Dependency(\.cliClient) var cliClient let configuration = Configuration.mock - try await cliClient.runVaultCommand(.init(arguments: [argument], quiet: false, shell: nil)) + try await cliClient.runVaultCommand( + .init(arguments: [argument], quiet: false, shell: nil), + logging: Self.loggingOptions + ) let shell = await captured.shell #expect(shell == .zsh(useDashC: true)) @@ -74,11 +83,14 @@ struct CliClientTests: TestCase { try await withMockConfiguration(captured, configuration: configuration, key: "runPlaybook") { @Dependency(\.cliClient) var cliClient - try await cliClient.runPlaybookCommand(.init( - arguments: [], - quiet: false, - shell: nil - )) + try await cliClient.runPlaybookCommand( + .init( + arguments: [], + quiet: false, + shell: nil + ), + logging: Self.loggingOptions + ) let expectedArguments = [ "ansible-playbook", "playbook/main.yml", @@ -156,6 +168,118 @@ struct CliClientTests: TestCase { } } + @Test( + arguments: [ + GenerateJsonTestOption( + options: .init( + templateDirectory: nil, + templateRepo: nil, + version: nil, + useLocalTemplateDirectory: true + ), + configuration: nil, + expectation: .failing + ), + GenerateJsonTestOption( + options: .init( + templateDirectory: nil, + templateRepo: nil, + version: nil, + useLocalTemplateDirectory: false + ), + configuration: nil, + expectation: .failing + ), + GenerateJsonTestOption( + options: .init( + templateDirectory: "template", + templateRepo: nil, + version: nil, + useLocalTemplateDirectory: true + ), + configuration: nil, + expectation: .success(""" + { + "template" : { + "path" : "template" + } + } + """) + ), + GenerateJsonTestOption( + options: .init( + templateDirectory: nil, + templateRepo: nil, + version: nil, + useLocalTemplateDirectory: false + ), + configuration: .init(template: .init(directory: "template")), + expectation: .success(""" + { + "template" : { + "path" : "template" + } + } + """) + ), + GenerateJsonTestOption( + options: .init( + templateDirectory: nil, + templateRepo: "https://git.example.com/template.git", + version: nil, + useLocalTemplateDirectory: false + ), + configuration: nil, + expectation: .success(""" + { + "template" : { + "repo" : "https://git.example.com/template.git", + "version" : "main" + } + } + """) + ), + GenerateJsonTestOption( + options: .init( + templateDirectory: nil, + templateRepo: nil, + version: nil, + useLocalTemplateDirectory: false + ), + configuration: .init(template: .init(url: "https://git.example.com/template.git", version: "v0.1.0")), + expectation: .success(""" + { + "template" : { + "repo" : "https://git.example.com/template.git", + "version" : "v0.1.0" + } + } + """) + ) + ] + ) + func generateJson(input: GenerateJsonTestOption) async { + await withTestLogger(key: "generateJson") { + $0.configurationClient = .mock(input.configuration ?? .init()) + $0.cliClient = .liveValue + } operation: { + @Dependency(\.cliClient) var cliClient + + let json = try? await cliClient.generateJSON( + input.options, + logging: Self.loggingOptions, + encoder: jsonEncoder + ) + + switch input.expectation { + case let .success(expected): + #expect(json == expected) + case .failing: + #expect(json == nil) + } + } + } + func withMockConfiguration( _ capturing: CliClient.CapturingClient, configuration: Configuration = .mock, @@ -174,6 +298,17 @@ struct CliClientTests: TestCase { } } +struct GenerateJsonTestOption: Sendable { + let options: CliClient.GenerateJsonOptions + let configuration: Configuration? + let expectation: GenerateJsonExpectation +} + +enum GenerateJsonExpectation: Sendable { + case failing + case success(String) +} + extension ConfigurationClient { static func mock(_ configuration: Configuration) -> Self { var mock = Self.testValue @@ -184,3 +319,9 @@ extension ConfigurationClient { } struct TestError: Error {} + +let jsonEncoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes, .sortedKeys] + return encoder +}()