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 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 } public var fileExists: @Sendable (String) -> Bool = { _ in false } public var findVaultFileInCurrentDirectory: @Sendable () throws -> URL? public var write: @Sendable (String, Data) throws -> Void } @_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( loadFile: { try client.loadFile(at: $0, into: &$1, decoder: $2) }, homeDir: { client.homeDir }, isDirectory: { client.isDirectory(url: $0) }, isReadable: { client.isReadable(url: $0) }, fileExists: { client.fileExists(at: $0) }, findVaultFileInCurrentDirectory: { try client.findVaultFileInCurrentDirectory() }, write: { path, data in try data.write(to: URL(filePath: path)) } ) } 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 fileExists(at path: String) -> Bool { fileManager.fileExists(atPath: path) } func findVaultFileInCurrentDirectory() throws -> URL? { let urls = try fileManager.contentsOfDirectory(at: URL(filePath: "."), includingPropertiesForKeys: nil) if let vault = urls.firstVaultFile { return vault } // check for folders that end with "vars" and search those next. for folder in urls.filter({ $0.absoluteString.hasSuffix("vars/") }) { let files = try fileManager.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil) if let vault = files.firstVaultFile { return vault } } // Fallback to check all sub-folders for folder in urls.filter({ self.isDirectory(url: $0) && !$0.absoluteString.hasSuffix("vars/") }) { let files = try fileManager.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil) if let vault = files.firstVaultFile { return vault } } return 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.hasSuffix(".json") { // 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: "=").map { $0.replacingOccurrences(of: "\"", with: "") } 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]) } } } } private extension Array where Element == URL { var firstVaultFile: URL? { first { $0.absoluteString.hasSuffix("vault.yml") } } }