Files
swift-mqtt-dewpoint/Sources/CliClient/EnvironmentDependency.swift
Michael Housh 99f39b91af
All checks were successful
CI / Run Tests (push) Successful in 4m57s
feat: More cli client tests and documentation.
2024-11-18 22:55:54 -05:00

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
}