import Dependencies import DependenciesMacros import DotEnv import Foundation import Models @_spi(Internal) public extension DependencyValues { /// A dependecy responsible for loding environment variables. /// /// This is just used internally of this module, but is useful to /// override for testing purposes, so import using `@_spi(Internal)`. var environment: EnvironmentDependency { get { self[EnvironmentDependency.self] } set { self[EnvironmentDependency.self] = newValue } } } /// Responsible for loading environment variables and files. /// /// @_spi(Internal) @DependencyClient public struct EnvironmentDependency: Sendable { /// A json decoder to use to decode files and environment variables. public var jsonDecoder: @Sendable () -> JSONDecoder = { JSONDecoder() } /// A json encoder to use to encode files and environment variables. public var jsonEncoder: @Sendable () -> JSONEncoder = { JSONEncoder() } /// Load the variables based on the request. public var load: @Sendable (FileType) async throws -> [String: String] = { _ in [:] } /// Load the environment variables based on the current process environment. /// /// You can override this to use an empty environment, which is useful for testing purposes. public var processInfo: @Sendable () -> [String: String] = { [:] } /// Represents the types of files that can be loaded and decoded into /// the environment. public enum FileType: Equatable { case dotEnv(path: String) case json(path: String) public init?(path: String) { let strings = path.split(separator: ".") guard let ext = strings.last else { return nil } switch ext { case "env": self = .dotEnv(path: path) case "json": self = .json(path: path) default: return nil } } } } @_spi(Internal) extension EnvironmentDependency: DependencyKey { public static let testValue: EnvironmentDependency = Self() public static func live( decoder: JSONDecoder = .init(), encoder: JSONEncoder = .init() ) -> Self { Self( jsonDecoder: { decoder }, jsonEncoder: { encoder }, load: { file in switch file { case let .dotEnv(path: path): let file = try DotEnv.read(path: path) return file.lines.reduce(into: [String: String]()) { partialResult, line in partialResult[line.key] = line.value } case let .json(path: path): let url = url(for: path) return try decoder.decode( [String: String].self, from: Data(contentsOf: url) ) } }, processInfo: { ProcessInfo.processInfo.environment } ) } public static let liveValue: EnvironmentDependency = .live() } private func url(for path: String) -> URL { #if os(Linux) return URL(fileURLWithPath: path) #else return URL(filePath: path) #endif }