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 } } } /// Represents the interface needed for the command line application. /// /// @DependencyClient public struct CliClient { /// Parse a log level from the given `EnvVars`. public var logLevel: @Sendable (EnvVars) -> Logger.Level = { _ in .debug } /// Generate the `EnvVars` with the given parameters. public var makeEnvVars: @Sendable (EnvVarsRequest) async throws -> EnvVars /// Generate the `MQTTClient` with the given parameters. public var makeClient: @Sendable (ClientRequest) throws -> MQTTClient /// Attempt to parse a string to an `MQTTClient.Version`. public var parseMqttClientVersion: @Sendable (String) -> MQTTClient.Version? /// Represents the parameters needed to create an `MQTTClient`. /// public struct ClientRequest: Sendable { /// The environment variables used to create the client. public let environment: EnvVars /// The event loop group for the client. public let eventLoopGroup: MultiThreadedEventLoopGroup /// A logger to use with the client. public let logger: Logger? /// Create a new client request. /// /// - Parameters: /// - environment: The environment variables to use. /// - eventLoopGroup: The event loop group to use on the client. /// - logger: An optional logger to use on the client. public init( environment: EnvVars, eventLoopGroup: MultiThreadedEventLoopGroup, logger: Logger? ) { self.environment = environment 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 await EnvVars.load( dotEnvFile: $0.envFilePath, logger: $0.logger, version: $0.mqttClientVersion ) }, makeClient: { MQTTClient( environment: $0.environment, eventLoopGroup: $0.eventLoopGroup, logger: $0.logger ) }, parseMqttClientVersion: { .init(string: $0) } ) } } // MARK: - Helpers extension EnvironmentDependency { func dotEnvDict(path: String?) async throws -> [String: String] { guard let path, let file = FileType(path: path) else { return [:] } return try await load(file) } } extension EnvVars { /// Load the `EnvVars` from the environment. /// /// - Paramaters: /// - dotEnvFile: An optional environment file to load. /// - 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? ) async throws -> EnvVars { @Dependency(\.environment) var environment let defaultEnvVars = EnvVars() let coders = environment.coders() let defaultEnvDict = (try? coders.encode(defaultEnvVars)) .flatMap { try? coders.decode([String: String].self, from: $0) } ?? [:] let dotEnvDict = try await environment.dotEnvDict(path: dotEnvFile) let envVarsDict = defaultEnvDict .merging(environment.processInfo(), uniquingKeysWith: { $1 }) .merging(dotEnvDict, uniquingKeysWith: { $1 }) var envVars = (try? JSONSerialization.data(withJSONObject: envVarsDict)) .flatMap { try? coders.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( environment 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: .parseOrDefault(string: envVars.version), disablePing: false, userName: envVars.userName, password: envVars.password ) ) } } @_spi(Internal) public extension MQTTClient.Version { static let `default` = Self.v3_1_1 static func parseOrDefault(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 } } } 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 } } }