feat: Initial commit

This commit is contained in:
2024-11-29 14:30:52 -05:00
commit 58e0f0e4b5
18 changed files with 732 additions and 0 deletions

View File

@@ -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()
}

View File

@@ -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)
}
}

View File

@@ -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])
}
}
}
}

View File

@@ -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
}