import Dependencies import DependenciesMacros import DotEnv import Foundation import Logging import Models import MQTTNIO import NIO public extension DependencyValues { var cliClient: CliClient { get { self[CliClient.self] } set { self[CliClient.self] = newValue } } } @DependencyClient public struct CliClient { public var logLevel: @Sendable (EnvVars) -> Logger.Level = { _ in .debug } public var makeEnvVars: @Sendable (EnvVarsRequest) async throws -> EnvVars public var makeClient: @Sendable (ClientRequest) throws -> MQTTClient public var parseMqttClientVersion: @Sendable (String) -> MQTTClient.Version? public struct ClientRequest: Sendable { public let envVars: EnvVars public let eventLoopGroup: MultiThreadedEventLoopGroup public let logger: Logger? public init( envVars: EnvVars, eventLoopGroup: MultiThreadedEventLoopGroup, logger: Logger? ) { self.envVars = envVars self.eventLoopGroup = eventLoopGroup self.logger = logger } } public struct EnvVarsRequest: Sendable { public let envFilePath: String? public let logger: Logger? public let mqttClientVersion: String? public init( envFilePath: String? = nil, logger: Logger? = nil, version mqttClientVersion: String? = nil ) { self.envFilePath = envFilePath self.logger = logger self.mqttClientVersion = mqttClientVersion } } } extension CliClient: DependencyKey { public static let testValue: CliClient = Self() public static var liveValue: CliClient { Self( logLevel: { Logger.Level.from(environment: $0) }, makeEnvVars: { try EnvVars.load( dotEnvFile: $0.envFilePath, logger: $0.logger, version: $0.mqttClientVersion ) }, makeClient: { MQTTClient( envVars: $0.envVars, eventLoopGroup: $0.eventLoopGroup, logger: $0.logger ) }, parseMqttClientVersion: { .init(string: $0) } ) } } extension EnvVars { /// Load the `EnvVars` from the environment. /// /// - Paramaters: /// - logger: An optional logger to use for debugging. /// - version: A version that is specified from command line, ignoring any environment variable. static func load( dotEnvFile: String?, logger: Logger?, version: String? ) throws -> EnvVars { let defaultEnvVars = EnvVars() let encoder = JSONEncoder() let decoder = JSONDecoder() if let dotEnvFile { try DotEnv.load(path: dotEnvFile) } let defaultEnvDict = (try? encoder.encode(defaultEnvVars)) .flatMap { try? decoder.decode([String: String].self, from: $0) } ?? [:] let envVarsDict = defaultEnvDict .merging(ProcessInfo.processInfo.environment, uniquingKeysWith: { $1 }) var envVars = (try? JSONSerialization.data(withJSONObject: envVarsDict)) .flatMap { try? decoder.decode(EnvVars.self, from: $0) } ?? defaultEnvVars if let version { envVars.version = version } logger?.debug("Done loading EnvVars...") return envVars } } @_spi(Internal) public extension MQTTClient { convenience init( envVars: EnvVars, eventLoopGroup: EventLoopGroup, logger: Logger? ) { self.init( host: envVars.host, port: envVars.port != nil ? Int(envVars.port!) : nil, identifier: envVars.identifier, eventLoopGroupProvider: .shared(eventLoopGroup), logger: logger, configuration: .init( version: .parseOrDefualt(string: envVars.version), disablePing: false, userName: envVars.userName, password: envVars.password ) ) } } extension MQTTClient.Version { static let `default` = Self.v3_1_1 static func parseOrDefualt(string: String?) -> Self { guard let string, let value = Self(string: string) else { return .default } return value } init?(string: String) { if string.contains("5") { self = .v5_0 } else if string.contains("3") { self = .v3_1_1 } else { return nil } } } @_spi(Internal) public extension Logger.Level { /// Parse a `Logger.Level` from the loaded `EnvVars`. static func from(environment envVars: EnvVars) -> Self { // If the log level was set via an environment variable. if let logLevel = envVars.logLevel { return logLevel } // Parse the appEnv to derive an log level. switch envVars.appEnv { case .staging, .development: return .debug case .production: return .info case .testing: return .trace } } }