import ConfigurationClient import Dependencies import DependenciesMacros import Foundation import ShellClient public extension DependencyValues { var cliClient: CliClient { get { self[CliClient.self] } set { self[CliClient.self] = newValue } } } @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 { @DependencyClient struct RunPlaybook: Sendable { public var buildProject: @Sendable (RunOptions, BuildOptions) async throws -> Void public var createProject: @Sendable (RunOptions, CreateOptions) async throws -> String public struct RunOptions: Equatable, Sendable { public let loggingOptions: CliClient.LoggingOptions public let quiet: Bool public let shell: String? } public struct BuildOptions: Equatable, Sendable { public let extraOptions: [String]? public let inventoryFilePath: String? public let projectDirectory: String? public init( extraOptions: [String]?, inventoryFilePath: String?, projectDirectory: String ) { self.extraOptions = extraOptions self.inventoryFilePath = inventoryFilePath self.projectDirectory = projectDirectory } } public struct CreateOptions: Equatable, Sendable { public let extraOptions: [String]? public let inventoryFilePath: String? public let projectDirectory: String public let template: Configuration.Template public let useLocalTemplateDirectory: Bool public init( extraOptions: [String]?, inventoryFilePath: String?, projectDirectory: String, template: Configuration.Template, useLocalTemplateDirectory: Bool ) { self.extraOptions = extraOptions self.inventoryFilePath = inventoryFilePath self.projectDirectory = projectDirectory self.template = template self.useLocalTemplateDirectory = useLocalTemplateDirectory } } } } public extension CliClient { struct PandocOptions: Equatable, Sendable { let buildDirectory: String? let files: [String]? let includeInHeader: [String]? let outputDirectory: String? let outputFileName: String? let outputFileType: FileType let projectDirectory: String? let quiet: Bool let shell: String? let shouldBuild: Bool public init( buildDirectory: String? = nil, files: [String]? = nil, includeInHeader: [String]? = nil, outputDirectory: String? = nil, outputFileName: String? = nil, outputFileType: FileType, projectDirectory: String?, quiet: Bool, shell: String? = nil, shouldBuild: Bool ) { self.buildDirectory = buildDirectory self.files = files self.includeInHeader = includeInHeader self.outputDirectory = outputDirectory self.outputFileName = outputFileName self.outputFileType = outputFileType self.projectDirectory = projectDirectory self.quiet = quiet self.shell = shell self.shouldBuild = shouldBuild } // swiftlint:disable nesting public enum FileType: Equatable, Sendable { case html case latex case pdf(engine: String?) } // swiftlint:enable nesting } 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? let inventoryFilePath: String? let playbookDirectory: String? let quiet: Bool let shell: String? public init( arguments: [String], configuration: Configuration? = nil, inventoryFilePath: String? = nil, playbookDirectory: String? = nil, quiet: Bool, shell: String? = nil ) { self.arguments = arguments self.configuration = configuration self.inventoryFilePath = inventoryFilePath self.playbookDirectory = playbookDirectory self.quiet = quiet self.shell = shell } public enum Route { case build(BuildOption) case create(CreateOption) public struct BuildOption: Equatable, Sendable { public let extraOptions: [String]? public let inventoryFilePath: String? public let projectDirectory: String? public init( extraOptions: [String]?, inventoryFilePath: String?, projectDirectory: String ) { self.extraOptions = extraOptions self.inventoryFilePath = inventoryFilePath self.projectDirectory = projectDirectory } } public struct CreateOption: Equatable, Sendable { public let extraOptions: [String]? public let inventoryFilePath: String? public let projectDirectory: String public let template: Configuration.Template public let useLocalTemplateDirectory: Bool public init( extraOptions: [String]?, inventoryFilePath: String?, projectDirectory: String, template: Configuration.Template, useLocalTemplateDirectory: Bool ) { self.extraOptions = extraOptions self.inventoryFilePath = inventoryFilePath self.projectDirectory = projectDirectory self.template = template self.useLocalTemplateDirectory = useLocalTemplateDirectory } } } } struct RunCommandOptions: Sendable, Equatable { public let arguments: [String] public let quiet: Bool public let shell: ShellCommand.Shell public init( arguments: [String], quiet: Bool, shell: ShellCommand.Shell ) { self.arguments = arguments self.quiet = quiet self.shell = shell } } struct VaultOptions: Equatable, Sendable { let arguments: [String] let configuration: Configuration? let quiet: Bool let shell: String? let vaultFilePath: String? public init( arguments: [String], configuration: Configuration? = nil, quiet: Bool, shell: String?, vaultFilePath: String? = nil ) { self.arguments = arguments self.configuration = configuration self.quiet = quiet self.shell = shell self.vaultFilePath = vaultFilePath } } } extension CliClient: DependencyKey { public static func live( env: [String: String] ) -> Self { @Dependency(\.logger) var logger return .init { options in @Dependency(\.asyncShellClient) var shellClient if !options.quiet { try await shellClient.foreground(.init( shell: options.shell, environment: ProcessInfo.processInfo.environment, in: nil, options.arguments )) } else { try await shellClient.background(.init( shell: options.shell, environment: ProcessInfo.processInfo.environment, in: nil, 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 } } public static var liveValue: CliClient { .live(env: ProcessInfo.processInfo.environment) } public static let testValue: CliClient = Self() public static func capturing(_ client: CapturingClient) -> Self { .init { options in await client.set(options) } generateJSON: { try await Self().generateJSON($0, $1, $2) } } public actor CapturingClient: Sendable { public private(set) var quiet: Bool? public private(set) var shell: ShellCommand.Shell? public private(set) var arguments: [String]? public init() {} public func set( _ options: RunCommandOptions ) { quiet = options.quiet shell = options.shell arguments = options.arguments } } }