135 lines
3.5 KiB
Swift
135 lines
3.5 KiB
Swift
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<T: Encodable>(_ instance: T) throws -> Data
|
|
func decode<T: Decodable>(_ 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<T>(_ instance: T) throws -> Data where T: Encodable {
|
|
try encoder.encode(instance)
|
|
}
|
|
|
|
func decode<T>(_ 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
|
|
}
|