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 { public var coders: @Sendable () -> any Coderable = { JSONCoders() } /// 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 } } } } struct DecodeError: Error {} @_spi(Internal) extension EnvironmentDependency: DependencyKey { public static let testValue: EnvironmentDependency = Self() public static func live( decoder: JSONDecoder = .init(), encoder: JSONEncoder = .init() ) -> Self { Self( coders: { JSONCoders(decoder: decoder, encoder: 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() } /// A type that encode and decode values. /// /// This is really just here to override tests with coders that will throw an error, /// instead of encoding or decoding data. /// @_spi(Internal) public protocol Coderable { func encode(_ instance: T) throws -> Data func decode(_ type: T.Type, from data: Data) throws -> T } struct JSONCoders: Coderable { let decoder: JSONDecoder let encoder: JSONEncoder init( decoder: JSONDecoder = .init(), encoder: JSONEncoder = .init() ) { self.decoder = decoder self.encoder = encoder } func encode(_ instance: T) throws -> Data where T: Encodable { try encoder.encode(instance) } func decode(_ type: T.Type, from data: Data) throws -> T where T: Decodable { try decoder.decode(T.self, from: data) } } private func url(for path: String) -> URL { #if os(Linux) return URL(fileURLWithPath: path) #else return URL(filePath: path) #endif }