feat: Begins using swift argument parser and creating cli client dependency
All checks were successful
CI / Run Tests (push) Successful in 4m27s

This commit is contained in:
2024-11-16 22:32:32 -05:00
parent 3416ce1003
commit 6472d3cd1e
19 changed files with 657 additions and 342 deletions

View File

@@ -1,3 +1,4 @@
import ArgumentParser
import Dependencies
import Foundation
import Logging
@@ -11,106 +12,10 @@ import SensorsService
import ServiceLifecycle
@main
struct Application {
/// The main entry point of the application.
static func main() async throws {
let eventloopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
var logger = Logger(label: "dewpoint-controller")
logger.logLevel = .trace
logger.info("Starting dewpoint-controller!")
let environment = loadEnvVars(logger: logger)
if environment.appEnv == .production {
logger.debug("Updating logging level to info.")
logger.logLevel = .info
}
let mqtt = MQTTClient(
envVars: environment,
eventLoopGroup: eventloopGroup,
logger: logger
)
do {
try await withDependencies {
$0.psychrometricClient = .liveValue
$0.mqtt = .live(client: mqtt, logger: logger)
} operation: {
let mqttConnection = MQTTConnectionService(logger: logger)
let sensors = SensorsService(sensors: .live, logger: logger)
var serviceGroupConfiguration = ServiceGroupConfiguration(
services: [
mqttConnection,
sensors
],
gracefulShutdownSignals: [.sigterm, .sigint],
logger: logger
)
serviceGroupConfiguration.maximumCancellationDuration = .seconds(5)
serviceGroupConfiguration.maximumGracefulShutdownDuration = .seconds(10)
let serviceGroup = ServiceGroup(configuration: serviceGroupConfiguration)
try await serviceGroup.run()
}
try await mqtt.shutdown()
try await eventloopGroup.shutdownGracefully()
} catch {
try await eventloopGroup.shutdownGracefully()
}
}
}
// MARK: - Helpers
private func loadEnvVars(logger: Logger) -> EnvVars {
let defaultEnvVars = EnvVars()
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let defaultEnvDict = (try? encoder.encode(defaultEnvVars))
.flatMap { try? decoder.decode([String: String].self, from: $0) }
?? [:]
let envVarsDict = defaultEnvDict
.merging(ProcessInfo.processInfo.environment, uniquingKeysWith: { $1 })
let envVars = (try? JSONSerialization.data(withJSONObject: envVarsDict))
.flatMap { try? decoder.decode(EnvVars.self, from: $0) }
?? defaultEnvVars
logger.debug("Done loading EnvVars...")
return envVars
}
private extension MQTTNIO.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: .v3_1_1,
disablePing: false,
userName: envVars.userName,
password: envVars.password
)
)
}
}
private extension Array where Element == TemperatureAndHumiditySensor {
static var live: Self {
TemperatureAndHumiditySensor.Location.allCases.map { location in
TemperatureAndHumiditySensor(location: location)
}
}
struct Application: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "dewpoint-controller",
abstract: "Command for running the dewpoint mqtt service.",
subcommands: [Run.self, Debug.self]
)
}

View File

@@ -0,0 +1,74 @@
import ArgumentParser
import CliClient
import CustomDump
import Dependencies
import DotEnv
import Foundation
import Logging
import Models
extension Application {
struct Debug: AsyncParsableCommand {
static let configuration: CommandConfiguration = .init(
commandName: "debug",
abstract: "Debug the environment variables."
)
@OptionGroup
var shared: SharedOptions
@Flag(
name: [.customLong("show-password")],
help: "Don't redact the password from the console."
)
var showPassword: Bool = false
mutating func run() async throws {
@Dependency(\.cliClient) var client
let logger = Logger(label: "debug-command")
print("--------------------------")
print("Running debug command...")
if let envFile = shared.envFile {
print("Reading env file: \(envFile)")
print("--------------------------")
} else {
print("No env file set.")
print("--------------------------")
}
print("Loading EnvVars")
print("--------------------------")
let envVars = try await client.makeEnvVars(shared.envVarsRequest(logger: logger))
printEnvVars(envVars: envVars, showPassword: showPassword)
print("--------------------------")
if let logLevel = shared.logLevel, let level = logLevel() {
print("Log Level option: \(level)")
print("--------------------------")
} else {
print("Log Level option: nil")
print("--------------------------")
}
}
private func printEnvVars(envVars: EnvVars, showPassword: Bool) {
// show the proper password to show depending on if it exists
// and if we should redact it or not.
var passwordString: String?
switch (showPassword, envVars.password) {
case (true, .none), (_, .none):
break
case (true, let .some(password)):
passwordString = password
case (false, .some):
passwordString = "<redacted>"
}
var envVars = envVars
envVars.password = passwordString
customDump(envVars)
}
}
}

View File

@@ -0,0 +1,86 @@
import ArgumentParser
import CliClient
import Dependencies
import Foundation
import Logging
import Models
import MQTTConnectionService
import MQTTManager
import MQTTNIO
import NIO
import PsychrometricClientLive
import SensorsService
import ServiceLifecycle
extension Application {
/// Run the controller.
///
struct Run: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "run",
abstract: "Run the controller."
)
@OptionGroup
var shared: SharedOptions
mutating func run() async throws {
@Dependency(\.cliClient) var cliClient
let eventloopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
var logger = Logger(label: "dewpoint-controller")
let mqtt = try await setup(eventLoopGroup: eventloopGroup, logger: &logger)
do {
try await withDependencies {
$0.psychrometricClient = .liveValue
$0.mqtt = .live(client: mqtt, logger: logger)
} operation: {
let mqttConnection = MQTTConnectionService(logger: logger)
let sensors = SensorsService(sensors: .live, logger: logger)
var serviceGroupConfiguration = ServiceGroupConfiguration(
services: [
mqttConnection,
sensors
],
gracefulShutdownSignals: [.sigterm, .sigint],
logger: logger
)
serviceGroupConfiguration.maximumCancellationDuration = .seconds(5)
serviceGroupConfiguration.maximumGracefulShutdownDuration = .seconds(10)
let serviceGroup = ServiceGroup(configuration: serviceGroupConfiguration)
logger.info("Starting dewpoint-controller!")
try await serviceGroup.run()
}
try await mqtt.shutdown()
try await eventloopGroup.shutdownGracefully()
} catch {
try await eventloopGroup.shutdownGracefully()
}
}
private func setup(
eventLoopGroup: MultiThreadedEventLoopGroup,
logger: inout Logger
) async throws -> MQTTClient {
@Dependency(\.cliClient) var cliClient
let environment = try await cliClient.makeEnvVars(shared.envVarsRequest(logger: logger))
logger.logLevel = cliClient.logLevel(environment)
return try cliClient.makeClient(.init(
envVars: environment,
eventLoopGroup: eventLoopGroup,
logger: logger
))
}
}
}
// MARK: - Helpers

View File

@@ -0,0 +1,50 @@
import ArgumentParser
import CliClient
import Logging
import Models
import MQTTNIO
extension Application {
struct SharedOptions: ParsableArguments {
@Option(
name: [.short, .customLong("env-file")],
help: "A file path to an env file."
)
var envFile: String?
@Option(
name: [.short, .customLong("log-level")],
help: "Set the logging level."
)
var logLevel: LogLevelContainer?
@Option(
name: [.short, .long],
help: "Set the MQTT connecition version."
)
var version: String?
func envVarsRequest(logger: Logger?) -> CliClient.EnvVarsRequest {
.init(envFilePath: envFile, logger: logger, version: version)
}
}
}
/// A container type for making `Logger.Level` into a type
/// that can be parsed as a command line argument. This is
/// to suppress warnings vs. having `Logger.Level` adopt the
/// protocol.
@_spi(Internal)
public struct LogLevelContainer: ExpressibleByArgument {
public let logLevel: Logger.Level?
public init?(argument: String) {
self.logLevel = .init(rawValue: argument.lowercased())
}
public func callAsFunction() -> Logger.Level? {
logLevel
}
}

View File

@@ -0,0 +1,10 @@
import Models
@_spi(Internal)
public extension Array where Element == TemperatureAndHumiditySensor {
static var live: Self {
TemperatureAndHumiditySensor.Location.allCases.map { location in
TemperatureAndHumiditySensor(location: location)
}
}
}