213 lines
5.7 KiB
Swift
213 lines
5.7 KiB
Swift
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)
|
|
}
|