From 58e0f0e4b5a7dfb23d1fd72d91e1b1b8450d0f50 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Fri, 29 Nov 2024 14:30:52 -0500 Subject: [PATCH] feat: Initial commit --- .editorconfig | 7 ++ .gitignore | 8 ++ .swiftformat | 11 +++ .swiftlint.yml | 9 ++ Package.resolved | 105 +++++++++++++++++++++ Package.swift | 39 ++++++++ Sources/CliClient/CliClient.swift | 52 +++++++++++ Sources/CliClient/Configuration.swift | 43 +++++++++ Sources/CliClient/FileClient.swift | 97 +++++++++++++++++++ Sources/CliClient/Helpers.swift | 73 +++++++++++++++ Sources/hpa/Application.swift | 15 +++ Sources/hpa/BuildCommand.swift | 35 +++++++ Sources/hpa/CreateProjectCommand.swift | 15 +++ Sources/hpa/CreateTemplateCommand.swift | 15 +++ Sources/hpa/GlobalOptions.swift | 22 +++++ Sources/hpa/Helpers.swift | 109 ++++++++++++++++++++++ Tests/CliClientTests/CliClientTests.swift | 62 ++++++++++++ justfile | 15 +++ 18 files changed, 732 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .swiftformat create mode 100644 .swiftlint.yml create mode 100644 Package.resolved create mode 100644 Package.swift create mode 100644 Sources/CliClient/CliClient.swift create mode 100644 Sources/CliClient/Configuration.swift create mode 100644 Sources/CliClient/FileClient.swift create mode 100644 Sources/CliClient/Helpers.swift create mode 100644 Sources/hpa/Application.swift create mode 100644 Sources/hpa/BuildCommand.swift create mode 100644 Sources/hpa/CreateProjectCommand.swift create mode 100644 Sources/hpa/CreateTemplateCommand.swift create mode 100644 Sources/hpa/GlobalOptions.swift create mode 100644 Sources/hpa/Helpers.swift create mode 100644 Tests/CliClientTests/CliClientTests.swift create mode 100644 justfile diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7cfbe01 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*.swift] +indent_style = space +indent_size = 2 +tab_width = 2 +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..08f338e --- /dev/null +++ b/.swiftformat @@ -0,0 +1,11 @@ +--self init-only +--indent 2 +--ifdef indent +--trimwhitespace always +--wraparguments before-first +--wrapparameters before-first +--wrapcollections preserve +--wrapconditions after-first +--typeblanklines preserve +--commas inline +--stripunusedargs closure-only diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..8df1f05 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,9 @@ +disabled_rules: + - closing_brace + - fuction_body_length + +included: + - Sources + - Tests + +ignore_multiline_statement_conditions: true diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..ffe2e48 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,105 @@ +{ + "originHash" : "8767e1814bf3d2b706110688b4b4d253de070cc5be3f0f91d5790acb7d0b7ad5", + "pins" : [ + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "9fa31f4403da54855f1e2aeaeff478f4f0e40b13", + "version" : "1.0.2" + } + }, + { + "identity" : "rainbow", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Rainbow", + "state" : { + "revision" : "e0dada9cd44e3fa7ec3b867e49a8ddbf543e3df3", + "version" : "4.0.1" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "41982a3656a71c768319979febd796c6fd111d5c", + "version" : "1.5.0" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "b9b24b69e2adda099a1fa381cda1eeec272d5b53", + "version" : "1.0.5" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "163409ef7dae9d960b87f34b51587b6609a76c1f", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "7d2eb4ad20efb2838269645410d26b64ca48d8aa", + "version" : "1.6.1" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log", + "state" : { + "revision" : "96a2f8a0fa41e9e09af4585e2724c4e825410b91", + "version" : "1.6.2" + } + }, + { + "identity" : "swift-log-format-and-pipe", + "kind" : "remoteSourceControl", + "location" : "https://github.com/adorkable/swift-log-format-and-pipe.git", + "state" : { + "revision" : "bd29badb9e6b18122ec10b84eed534db83cad279", + "version" : "0.1.1" + } + }, + { + "identity" : "swift-shell-client", + "kind" : "remoteSourceControl", + "location" : "https://github.com/m-housh/swift-shell-client.git", + "state" : { + "revision" : "9c0c4757d0a2e313d1a4a28e60418a753d3e4230", + "version" : "0.1.4" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1", + "version" : "1.4.3" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..e0990a0 --- /dev/null +++ b/Package.swift @@ -0,0 +1,39 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "swift-hpa", + platforms: [.macOS(.v14)], + products: [ + .executable(name: "hpa", targets: ["hpa"]), + .library(name: "CliClient", targets: ["CliClient"]) + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"), + .package(url: "https://github.com/apple/swift-log", from: "1.6.0"), + .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.5.2"), + .package(url: "https://github.com/m-housh/swift-shell-client.git", from: "0.1.0") + ], + targets: [ + .executableTarget( + name: "hpa", + dependencies: [ + "CliClient", + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Dependencies", package: "swift-dependencies"), + .product(name: "ShellClient", package: "swift-shell-client") + ] + ), + .target( + name: "CliClient", + dependencies: [ + .product(name: "Dependencies", package: "swift-dependencies"), + .product(name: "DependenciesMacros", package: "swift-dependencies"), + .product(name: "ShellClient", package: "swift-shell-client") + ] + ), + .testTarget(name: "CliClientTests", dependencies: ["CliClient"]) + ] +) diff --git a/Sources/CliClient/CliClient.swift b/Sources/CliClient/CliClient.swift new file mode 100644 index 0000000..e96e07d --- /dev/null +++ b/Sources/CliClient/CliClient.swift @@ -0,0 +1,52 @@ +import Dependencies +import DependenciesMacros +import Foundation + +public extension DependencyValues { + var cliClient: CliClient { + get { self[CliClient.self] } + set { self[CliClient.self] = newValue } + } +} + +@DependencyClient +public struct CliClient: Sendable { + public var decoder: @Sendable () -> JSONDecoder = { .init() } + public var encoder: @Sendable () -> JSONEncoder = { .init() } + public var loadConfiguration: @Sendable () throws -> Configuration +} + +extension CliClient: DependencyKey { + + public static func live( + decoder: JSONDecoder = .init(), + encoder: JSONEncoder = .init(), + env: [String: String] + ) -> Self { + .init { + decoder + } encoder: { + encoder + } loadConfiguration: { + @Dependency(\.logger) var logger + @Dependency(\.fileClient) var fileClient + + let urls = try findConfigurationFiles(env: env) + var env = env + + logger.trace("Loading configuration from: \(urls)") + + for url in urls { + try fileClient.loadFile(url, &env, decoder) + } + + return try .fromEnv(env, encoder: encoder, decoder: decoder) + } + } + + public static var liveValue: CliClient { + .live(env: ProcessInfo.processInfo.environment) + } + + public static let testValue: CliClient = Self() +} diff --git a/Sources/CliClient/Configuration.swift b/Sources/CliClient/Configuration.swift new file mode 100644 index 0000000..b8fcbd5 --- /dev/null +++ b/Sources/CliClient/Configuration.swift @@ -0,0 +1,43 @@ +import Dependencies +import Foundation +import ShellClient + +/// Represents the configuration. +public struct Configuration: Decodable { + + public let playbookDir: String? + public let inventoryPath: String? + public let templateRepo: String? + public let templateRepoVersion: String? + public let templateDir: String? + public let defaultPlaybookArgs: String? + + private enum CodingKeys: String, CodingKey { + case playbookDir = "HPA_PLAYBOOK_DIR" + case inventoryPath = "HPA_DEFAULT_INVENTORY" + case templateRepo = "HPA_TEMPLATE_REPO" + case templateRepoVersion = "HPA_TEMPLATE_VERSION" + case templateDir = "HPA_TEMPLATE_DIR" + case defaultPlaybookArgs = "HPA_DEFAULT_PLAYBOOK_ARGS" + } + + public static func fromEnv( + _ env: [String: String], + encoder: JSONEncoder = .init(), + decoder: JSONDecoder = .init() + ) throws -> Self { + @Dependency(\.logger) var logger + + logger.trace("Creating configuration from env...") + // logger.debug("\(env)") + + let hpaValues = env.reduce(into: [String: String]()) { partial, next in + if next.key.contains("HPA") { + partial[next.key] = next.value + } + } + logger.debug("HPA env vars: \(hpaValues)") + let data = try encoder.encode(env) + return try decoder.decode(Configuration.self, from: data) + } +} diff --git a/Sources/CliClient/FileClient.swift b/Sources/CliClient/FileClient.swift new file mode 100644 index 0000000..c8abf22 --- /dev/null +++ b/Sources/CliClient/FileClient.swift @@ -0,0 +1,97 @@ +import Dependencies +import DependenciesMacros +import Foundation + +@_spi(Internal) +public extension DependencyValues { + var fileClient: FileClient { + get { self[FileClient.self] } + set { self[FileClient.self] = newValue } + } +} + +@_spi(Internal) +@DependencyClient +public struct FileClient: Sendable { + public var contentsOfDirectory: @Sendable (URL) throws -> [URL] + public var loadFile: @Sendable (URL, inout [String: String], JSONDecoder) throws -> Void + public var homeDir: @Sendable () -> URL = { URL(string: "~/")! } + public var isDirectory: @Sendable (URL) -> Bool = { _ in false } + public var isReadable: @Sendable (URL) -> Bool = { _ in false } +} + +@_spi(Internal) +extension FileClient: DependencyKey { + public static let testValue: FileClient = Self() + + public static func live(fileManager: FileManager = .default) -> Self { + let client = LiveFileClient(fileManager: fileManager) + return Self( + contentsOfDirectory: { try client.contentsOfDirectory(url: $0) }, + loadFile: { try client.loadFile(at: $0, into: &$1, decoder: $2) }, + homeDir: { client.homeDir }, + isDirectory: { client.isDirectory(url: $0) }, + isReadable: { client.isReadable(url: $0) } + ) + } + + public static let liveValue = Self.live() +} + +private struct LiveFileClient: @unchecked Sendable { + + private let fileManager: FileManager + + init(fileManager: FileManager) { + self.fileManager = fileManager + } + + var homeDir: URL { fileManager.homeDirectoryForCurrentUser } + + func isDirectory(url: URL) -> Bool { + var isDirectory: ObjCBool = false + fileManager.fileExists(atPath: path(for: url), isDirectory: &isDirectory) + return isDirectory.boolValue + } + + func isReadable(url: URL) -> Bool { + fileManager.isReadableFile(atPath: path(for: url)) + } + + func contentsOfDirectory(url: URL) throws -> [URL] { + try fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: nil) + } + + func loadFile(at url: URL, into env: inout [String: String], decoder: JSONDecoder) throws { + @Dependency(\.logger) var logger + logger.trace("Begin load file for: \(path(for: url))") + +// if url.absoluteString.split(separator: ".json").count > 0 { +// // Handle json file. +// let data = try Data(contentsOf: url) +// let dict = (try? decoder.decode([String: String].self, from: data)) ?? [:] +// env.merge(dict, uniquingKeysWith: { $1 }) +// return +// } + + let string = try String(contentsOfFile: path(for: url), encoding: .utf8) + + logger.trace("Loaded file contents: \(string)") + + let lines = string.split(separator: "\n") + for line in lines { + logger.trace("Line: \(line)") + let strippedLine = line.trimmingCharacters(in: .whitespacesAndNewlines) + let splitLine = strippedLine.split(separator: "=") + logger.trace("Split Line: \(splitLine)") + guard splitLine.count >= 2 else { continue } + + if splitLine.count > 2 { + let rest = splitLine.dropFirst() + env[String(splitLine[0])] = String(rest.joined(separator: "=")) + } else { + env[String(splitLine[0])] = String(splitLine[1]) + } + } + } +} diff --git a/Sources/CliClient/Helpers.swift b/Sources/CliClient/Helpers.swift new file mode 100644 index 0000000..6e05506 --- /dev/null +++ b/Sources/CliClient/Helpers.swift @@ -0,0 +1,73 @@ +import Dependencies +import Foundation +import ShellClient + +@_spi(Internal) +public func findConfigurationFiles( + env: [String: String] = ProcessInfo.processInfo.environment +) throws -> [URL] { + @Dependency(\.logger) var logger + @Dependency(\.fileClient) var fileClient + + logger.debug("Begin find configuration files.") + logger.trace("Env: \(env)") + + let homeDir = fileClient.homeDir() + var url = homeDir.appending(path: ".hparc") + + if fileClient.isReadable(url) { + logger.debug("Found configuration in home directory") + return [url] + } + + if let configHome = env["HPA_CONFIG_HOME"] { + url = .init(filePath: configHome) + + if fileClient.isDirectory(url) { + logger.debug("Found configuration directory from hpa config home env var.") + return try fileClient.contentsOfDirectory(url) + } + if fileClient.isReadable(url) { + logger.debug("Found configuration from hpa config home env var.") + return [url] + } + } + + if let pwd = env["PWD"] { + url = .init(filePath: "\(pwd)").appending(path: ".hparc") + if fileClient.isReadable(url) { + logger.debug("Found configuration in current working directory.") + return [url] + } + } + + if let xdgConfigHome = env["XDG_CONFIG_HOME"] { + logger.debug("XDG Config Home: \(xdgConfigHome)") + + url = .init(filePath: "\(xdgConfigHome)") + .appending(path: "hpa-playbook") + + logger.debug("XDG Config url: \(url.absoluteString)") + + if fileClient.isDirectory(url) { + logger.debug("Found configuration in xdg config home.") + return try fileClient.contentsOfDirectory(url) + } + + if fileClient.isReadable(url) { + logger.debug("Not directory, but readable.") + return [url] + } + } + + // We could not find configuration in any usual places. + throw ConfigurationError.configurationNotFound +} + +func path(for url: URL) -> String { + url.absoluteString.replacing("file://", with: "") +} + +enum ConfigurationError: Error { + case configurationNotFound +} diff --git a/Sources/hpa/Application.swift b/Sources/hpa/Application.swift new file mode 100644 index 0000000..5a5fd28 --- /dev/null +++ b/Sources/hpa/Application.swift @@ -0,0 +1,15 @@ +import ArgumentParser +import Dependencies +import Rainbow +import ShellClient + +@main +struct Application: AsyncParsableCommand { + + static let configuration = CommandConfiguration( + commandName: "hpa", + abstract: "A utility for working with ansible hpa playbook.", + subcommands: [BuildCommand.self, CreateProjectCommand.self, CreateProjectTemplateCommand.self] + ) + +} diff --git a/Sources/hpa/BuildCommand.swift b/Sources/hpa/BuildCommand.swift new file mode 100644 index 0000000..5dceab1 --- /dev/null +++ b/Sources/hpa/BuildCommand.swift @@ -0,0 +1,35 @@ +import ArgumentParser +import CliClient +import Dependencies + +struct BuildCommand: AsyncParsableCommand { + + static let commandName = "build" + + static let configuration = CommandConfiguration.playbookCommandConfiguration( + commandName: commandName, + abstract: "Build a home performance assesment project." + ) + + @OptionGroup var globals: GlobalOptions + + @Argument( + help: "The project directory.", + completion: .directory + ) + var projectDir: String + + @Argument( + help: "Extra arguments passed to the playbook." + ) + var extraArgs: [String] = [] + + mutating func run() async throws { + let args = [ + "--tags", "build-project", + "--extra-vars", "project_dir=\(projectDir)" + ] + extraArgs + + try await runPlaybook(commandName: Self.commandName, globals: globals, args: args) + } +} diff --git a/Sources/hpa/CreateProjectCommand.swift b/Sources/hpa/CreateProjectCommand.swift new file mode 100644 index 0000000..bf96e75 --- /dev/null +++ b/Sources/hpa/CreateProjectCommand.swift @@ -0,0 +1,15 @@ +import ArgumentParser + +struct CreateProjectCommand: AsyncParsableCommand { + + static let configuration = CommandConfiguration( + commandName: "create-project", + abstract: "Create a home performance assesment project." + ) + + @OptionGroup var globals: GlobalOptions + + mutating func run() async throws { + fatalError() + } +} diff --git a/Sources/hpa/CreateTemplateCommand.swift b/Sources/hpa/CreateTemplateCommand.swift new file mode 100644 index 0000000..e5db1c1 --- /dev/null +++ b/Sources/hpa/CreateTemplateCommand.swift @@ -0,0 +1,15 @@ +import ArgumentParser + +struct CreateProjectTemplateCommand: AsyncParsableCommand { + + static let configuration = CommandConfiguration( + commandName: "create-project-template", + abstract: "Create a home performance assesment project template." + ) + + @OptionGroup var globals: GlobalOptions + + mutating func run() async throws { + fatalError() + } +} diff --git a/Sources/hpa/GlobalOptions.swift b/Sources/hpa/GlobalOptions.swift new file mode 100644 index 0000000..81b2ae9 --- /dev/null +++ b/Sources/hpa/GlobalOptions.swift @@ -0,0 +1,22 @@ +import ArgumentParser + +struct GlobalOptions: ParsableArguments { + + @Option( + name: .shortAndLong, + help: "Optional path to the ansible hpa playbook directory." + ) + var playbookDir: String? + + @Option( + name: .shortAndLong, + help: "Optional path to the ansible inventory to use." + ) + var inventoryPath: String? + + @Flag( + name: .long, + help: "Increase logging level." + ) + var verbose: Int +} diff --git a/Sources/hpa/Helpers.swift b/Sources/hpa/Helpers.swift new file mode 100644 index 0000000..1760126 --- /dev/null +++ b/Sources/hpa/Helpers.swift @@ -0,0 +1,109 @@ +import ArgumentParser +import CliClient +import Dependencies +import Foundation +import Logging +import Rainbow +import ShellClient + +extension CommandConfiguration { + static func playbookCommandConfiguration(commandName: String, abstract: String) -> Self { + Self( + commandName: commandName, + abstract: "\(abstract.blue)", + discussion: """ + \("IMPORTANT NOTE:".red) Any extra arguments to pass to the playbook invocation have to + be at the end with `--` before any arguments otherwise there will + be an "Unkown option" error. + + \("Example of passing extra args to the playbook:".yellow) + + $ hpa \(commandName) /my/project -- --vault-id "myId@$SCRIPTS/vault-gopass-client" + + \("See Also:".yellow) + + You can run the following command to see the options that can be passed to the playbook + invocation. + + $ ansible-playbook --help + """ + ) + } +} + +func ensureString( + globals: GlobalOptions, + configuration: Configuration, + globalsKeyPath: KeyPath, + configurationKeyPath: KeyPath +) throws -> String { + if let global = globals[keyPath: globalsKeyPath] { + return global + } + guard let configuration = configuration[keyPath: configurationKeyPath] else { + throw PlaybookNotFound() + } + return configuration +} + +func runPlaybook( + commandName: String, + globals: GlobalOptions, + args: [String] +) async throws { + try await withDependencies { + $0.logger = .init(label: "\("hpa".yellow)") + switch globals.verbose { + case 0: + $0.logger.logLevel = .info + case 1: + $0.logger.logLevel = .debug + case 2: + $0.logger.logLevel = .trace + default: + $0.logger.logLevel = .info + } + $0.logger[metadataKey: "command"] = "\(commandName.blue)" + } operation: { + @Dependency(\.cliClient) var cliClient + @Dependency(\.logger) var logger + @Dependency(\.asyncShellClient) var shellClient + + logger.debug("Begin run playbook: \(globals)") + let configuration = try cliClient.loadConfiguration() + logger.debug("Loaded configuration: \(configuration)") + + let playbookDir = try ensureString( + globals: globals, + configuration: configuration, + globalsKeyPath: \.playbookDir, + configurationKeyPath: \.playbookDir + ) + let playbook = "\(playbookDir)/main.yml" + + let inventory = (try? ensureString( + globals: globals, + configuration: configuration, + globalsKeyPath: \.inventoryPath, + configurationKeyPath: \.inventoryPath + )) ?? "\(playbookDir)/inventory.ini" + + var playbookArgs = [ + "ansible-playbook", playbook, + "--inventory", inventory + ] + args + + if let defaultArgs = configuration.defaultPlaybookArgs { + playbookArgs.append(defaultArgs) + } + + try await shellClient.foreground(.init( + shell: .zsh(useDashC: true), + environment: ProcessInfo.processInfo.environment, + in: nil, + playbookArgs + )) + } +} + +struct PlaybookNotFound: Error {} diff --git a/Tests/CliClientTests/CliClientTests.swift b/Tests/CliClientTests/CliClientTests.swift new file mode 100644 index 0000000..e4d749c --- /dev/null +++ b/Tests/CliClientTests/CliClientTests.swift @@ -0,0 +1,62 @@ +@_spi(Internal) import CliClient +import Dependencies +import ShellClient +import Testing + +@Test +func testFindConfigPaths() throws { + try withTestLogger(key: "testFindConfigPaths") { + $0.fileClient = .liveValue + } operation: { + @Dependency(\.logger) var logger + let urls = try findConfigurationFiles() + logger.debug("urls: \(urls)") + #expect(urls.count == 1) + } +} + +@Test +func loadConfiguration() throws { + try withTestLogger(key: "loadConfiguration", logLevel: .trace) { + $0.cliClient = .liveValue + $0.fileClient = .liveValue + } operation: { + @Dependency(\.cliClient) var client + @Dependency(\.logger) var logger + let config = try client.loadConfiguration() + logger.debug("\(config)") + #expect(config.playbookDir != nil) + } +} + +func withTestLogger( + key: String, + label: String = "CliClientTests", + logLevel: Logger.Level = .debug, + operation: @escaping @Sendable () throws -> Void +) rethrows { + try withDependencies { + $0.logger = .init(label: label) + $0.logger[metadataKey: "instance"] = "\(key)" + $0.logger.logLevel = logLevel + } operation: { + try operation() + } +} + +func withTestLogger( + key: String, + label: String = "CliClientTests", + logLevel: Logger.Level = .debug, + dependencies setupDependencies: @escaping (inout DependencyValues) -> Void, + operation: @escaping @Sendable () throws -> Void +) rethrows { + try withDependencies { + $0.logger = .init(label: label) + $0.logger[metadataKey: "instance"] = "\(key)" + $0.logger.logLevel = logLevel + setupDependencies(&$0) + } operation: { + try operation() + } +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..290d54b --- /dev/null +++ b/justfile @@ -0,0 +1,15 @@ + +build mode="debug": + swift build -c {{mode}} + +alias b := build + +test: + swift test + +alias t := test + +run *ARGS: + swift run hpa {{ARGS}} + +alias r := run