feat: Begins using swift argument parser and creating cli client dependency
All checks were successful
CI / Run Tests (push) Successful in 4m27s
All checks were successful
CI / Run Tests (push) Successful in 4m27s
This commit is contained in:
@@ -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]
|
||||
)
|
||||
}
|
||||
|
||||
74
Sources/DewPointController/DebugCommand.swift
Normal file
74
Sources/DewPointController/DebugCommand.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
86
Sources/DewPointController/RunCommand.swift
Normal file
86
Sources/DewPointController/RunCommand.swift
Normal 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
|
||||
50
Sources/DewPointController/SharedOptions.swift
Normal file
50
Sources/DewPointController/SharedOptions.swift
Normal 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
|
||||
}
|
||||
}
|
||||
10
Sources/DewPointController/Utils.swift
Normal file
10
Sources/DewPointController/Utils.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user