feat: Breaking out more dependencies.
This commit is contained in:
212
Sources/ConfigurationClient/ConfigurationClient.swift
Normal file
212
Sources/ConfigurationClient/ConfigurationClient.swift
Normal file
@@ -0,0 +1,212 @@
|
||||
import CodersClient
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import FileClient
|
||||
import Foundation
|
||||
import ShellClient
|
||||
|
||||
public extension DependencyValues {
|
||||
var configuration: ConfigurationClient {
|
||||
get { self[ConfigurationClient.self] }
|
||||
set { self[ConfigurationClient.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
@DependencyClient
|
||||
public struct ConfigurationClient: Sendable {
|
||||
public var find: @Sendable () async throws -> File
|
||||
var generate: @Sendable (File, Bool) async throws -> Void
|
||||
public var load: @Sendable (File?) async throws -> Configuration
|
||||
var write: @Sendable (File, Configuration, Bool) async throws -> Void
|
||||
|
||||
public func findAndLoad() async throws -> Configuration {
|
||||
let file = try? await find()
|
||||
return try await load(file)
|
||||
}
|
||||
|
||||
public func generate(
|
||||
at file: File,
|
||||
force: Bool = false
|
||||
) async throws {
|
||||
try await generate(file, force)
|
||||
}
|
||||
|
||||
public func write(
|
||||
_ configuration: Configuration,
|
||||
to file: File,
|
||||
force: Bool = false
|
||||
) async throws {
|
||||
try await write(file, configuration, force)
|
||||
}
|
||||
}
|
||||
|
||||
extension ConfigurationClient: DependencyKey {
|
||||
public static var testValue: Self { Self() }
|
||||
|
||||
public static func live(environment: [String: String]) -> Self {
|
||||
let liveClient = LiveConfigurationClient(environment: environment)
|
||||
return .init {
|
||||
try await liveClient.find()
|
||||
} generate: { file, force in
|
||||
try await liveClient.generate(at: file, force: force)
|
||||
} load: { file in
|
||||
try await liveClient.load(file: file)
|
||||
} write: { file, configuration, force in
|
||||
try await liveClient.write(configuration, to: file, force: force)
|
||||
}
|
||||
}
|
||||
|
||||
public static var liveValue: Self {
|
||||
.live(environment: ProcessInfo.processInfo.environment)
|
||||
}
|
||||
}
|
||||
|
||||
struct LiveConfigurationClient {
|
||||
|
||||
private let environment: [String: String]
|
||||
|
||||
@Dependency(\.coders) var coders
|
||||
@Dependency(\.fileClient) var fileManager
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
let validFileNames = [
|
||||
"config.json", "config.toml",
|
||||
".hparc.json", ".hparc.toml"
|
||||
]
|
||||
|
||||
init(environment: [String: String]) {
|
||||
self.environment = environment
|
||||
}
|
||||
|
||||
func find() async throws -> File {
|
||||
logger.debug("Begin find configuration.")
|
||||
|
||||
if let pwd = environment["PWD"],
|
||||
let file = await findInDirectory(pwd)
|
||||
{
|
||||
logger.debug("Found configuration in pwd: \(file.path)")
|
||||
return file
|
||||
}
|
||||
|
||||
if let configHome = environment.hpaConfigHome,
|
||||
let file = await findInDirectory(configHome)
|
||||
{
|
||||
logger.debug("Found configuration in config home env var: \(file.path)")
|
||||
return file
|
||||
}
|
||||
|
||||
if let configFile = environment.hpaConfigFile,
|
||||
isReadable(configFile),
|
||||
let file = File(configFile)
|
||||
{
|
||||
logger.debug("Found configuration in config file env var: \(file.path)")
|
||||
return file
|
||||
}
|
||||
|
||||
if let file = await findInDirectory(fileManager.homeDirectory()) {
|
||||
logger.debug("Found configuration in home directory: \(file.path)")
|
||||
return file
|
||||
}
|
||||
|
||||
if let file = await findInDirectory("\(environment.xdgConfigHome)/\(HPAKey.configDirName)") {
|
||||
logger.debug("Found configuration in xdg config directory: \(file.path)")
|
||||
return file
|
||||
}
|
||||
|
||||
throw ConfigurationError.configurationNotFound
|
||||
}
|
||||
|
||||
func generate(at file: File, force: Bool) async throws {
|
||||
if !force {
|
||||
guard !fileManager.fileExists(file.url) else {
|
||||
throw ConfigurationError.fileExists(path: file.path)
|
||||
}
|
||||
}
|
||||
|
||||
if case .toml = file {
|
||||
// In the case of toml, we copy the internal resource that includes
|
||||
// usage comments in the file.
|
||||
guard let resourceFile = Bundle.module.url(
|
||||
forResource: HPAKey.resourceFileName,
|
||||
withExtension: HPAKey.resourceFileExtension
|
||||
) else {
|
||||
throw ConfigurationError.resourceNotFound
|
||||
}
|
||||
|
||||
try await fileManager.copy(resourceFile, file.url)
|
||||
} else {
|
||||
// Json does not allow comments, so we write the mock configuration
|
||||
// to the file path.
|
||||
try await write(.mock, to: file, force: force)
|
||||
}
|
||||
}
|
||||
|
||||
func load(file: File?) async throws -> Configuration {
|
||||
guard let file else { return .init() }
|
||||
let data = try await fileManager.load(file.url)
|
||||
|
||||
switch file {
|
||||
case .json:
|
||||
return try coders.jsonDecoder().decode(Configuration.self, from: data)
|
||||
case .toml:
|
||||
guard let string = String(data: data, encoding: .utf8) else {
|
||||
throw ConfigurationError.decodingError
|
||||
}
|
||||
return try coders.tomlDecoder().decode(Configuration.self, from: string)
|
||||
}
|
||||
}
|
||||
|
||||
func write(
|
||||
_ configuration: Configuration,
|
||||
to file: File,
|
||||
force: Bool
|
||||
) async throws {
|
||||
if !force {
|
||||
guard !fileManager.fileExists(file.url) else {
|
||||
throw ConfigurationError.fileExists(path: file.path)
|
||||
}
|
||||
}
|
||||
|
||||
let data: Data
|
||||
|
||||
switch file {
|
||||
case .json:
|
||||
data = try coders.jsonEncoder().encode(configuration)
|
||||
case .toml:
|
||||
let string = try coders.tomlEncoder().encode(configuration)
|
||||
data = Data(string.utf8)
|
||||
}
|
||||
|
||||
try await fileManager.write(data, file.url)
|
||||
}
|
||||
|
||||
private func findInDirectory(_ directory: URL) async -> File? {
|
||||
for file in validFileNames {
|
||||
let url = directory.appending(path: file)
|
||||
if isReadable(url), let file = File(url) {
|
||||
return file
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func findInDirectory(_ directory: String) async -> File? {
|
||||
await findInDirectory(URL(filePath: directory))
|
||||
}
|
||||
|
||||
private func isReadable(_ file: URL) -> Bool {
|
||||
FileManager.default.isReadableFile(atPath: file.cleanFilePath)
|
||||
}
|
||||
|
||||
private func isReadable(_ file: String) -> Bool {
|
||||
isReadable(URL(filePath: file))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum ConfigurationError: Error {
|
||||
case configurationNotFound
|
||||
case resourceNotFound
|
||||
case decodingError
|
||||
case fileExists(path: String)
|
||||
}
|
||||
Reference in New Issue
Block a user