use std::path::PathBuf;
use anyhow::{Context, Result};
use bincode::{Decode, Encode};
#[cfg(test)]
use bon::Builder;
use config::{Config, Environment, File, FileFormat, Source};
use dirs2::config_dir;
use getset::{CopyGetters, Getters, Setters};
use serde::{Deserialize, Serialize};
use tracing::Level;
use tracing_subscriber_init::{TracingConfig, get_effective_level};
#[cfg(test)]
use crate::utils::Mock;
use crate::{TlsConfig, TracingConfigExt, error::Error, utils::to_path_buf};
pub trait PathDefaults {
fn env_prefix(&self) -> String;
fn config_absolute_path(&self) -> Option<String>;
fn default_file_path(&self) -> String;
fn default_file_name(&self) -> String;
fn tracing_absolute_path(&self) -> Option<String>;
fn default_tracing_path(&self) -> String;
fn default_tracing_file_name(&self) -> String;
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Getters, PartialEq, Serialize)]
pub struct Tracing {
#[getset(get = "pub")]
stdout: Layer,
#[getset(get = "pub")]
file: FileLayer,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Clone, CopyGetters, Debug, Default, Deserialize, Eq, PartialEq, Serialize, Setters)]
pub struct FileLayer {
quiet: u8,
verbose: u8,
layer: Layer,
}
impl TracingConfig for FileLayer {
fn quiet(&self) -> u8 {
self.quiet
}
fn verbose(&self) -> u8 {
self.verbose
}
fn with_ansi(&self) -> bool {
false
}
fn with_target(&self) -> bool {
self.layer.with_target
}
fn with_thread_ids(&self) -> bool {
self.layer.with_thread_ids
}
fn with_thread_names(&self) -> bool {
self.layer.with_thread_names
}
fn with_line_number(&self) -> bool {
self.layer.with_line_number
}
fn with_level(&self) -> bool {
self.layer.with_level
}
}
impl TracingConfigExt for FileLayer {
fn enable_stdout(&self) -> bool {
false
}
fn directives(&self) -> Option<&String> {
self.layer.directives.as_ref()
}
fn level(&self) -> Level {
get_effective_level(self.quiet(), self.verbose())
}
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Clone, CopyGetters, Debug, Default, Deserialize, Eq, Getters, PartialEq, Serialize)]
pub struct Layer {
#[getset(get_copy = "pub")]
with_target: bool,
#[getset(get_copy = "pub")]
with_thread_ids: bool,
#[getset(get_copy = "pub")]
with_thread_names: bool,
#[getset(get_copy = "pub")]
with_line_number: bool,
#[getset(get_copy = "pub")]
with_level: bool,
#[getset(get = "pub")]
directives: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Getters, PartialEq, Serialize)]
#[getset(get = "pub")]
pub struct Command {
cmd: String,
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Getters, PartialEq, Serialize)]
#[getset(get = "pub")]
pub struct Actix {
workers: u8,
ip: String,
port: u16,
tls: Option<Tls>,
}
#[derive(Clone, CopyGetters, Debug, Default, Deserialize, Eq, Getters, PartialEq, Serialize)]
pub struct Tls {
#[getset(get = "pub")]
ip: String,
#[getset(get_copy = "pub")]
port: u16,
#[getset(get = "pub")]
cert_file_path: String,
#[getset(get = "pub")]
key_file_path: String,
}
impl TlsConfig for Tls {
fn cert_file_path(&self) -> &str {
&self.cert_file_path
}
fn key_file_path(&self) -> &str {
&self.key_file_path
}
}
#[derive(Clone, CopyGetters, Debug, Default, Deserialize, Eq, Getters, PartialEq, Serialize)]
pub struct Bartos {
#[getset(get = "pub")]
prefix: String,
#[getset(get = "pub")]
host: String,
#[getset(get_copy = "pub")]
port: u16,
}
#[derive(Clone, CopyGetters, Debug, Default, Deserialize, Eq, Getters, PartialEq, Serialize)]
pub struct Mariadb {
host: String,
port: Option<u16>,
username: String,
password: String,
database: String,
options: Option<String>,
#[doc(hidden)]
#[getset(get_copy = "pub")]
#[serde(default = "OutputTableName::default")]
output_table: OutputTableName,
#[doc(hidden)]
#[getset(get_copy = "pub")]
#[serde(default = "StatusTableName::default")]
status_table: StatusTableName,
}
impl Mariadb {
#[must_use]
pub fn connection_string(&self) -> String {
let mut url = format!(
"mariadb://{}:{}@{}:{}/{}",
self.username,
self.password,
self.host,
self.port.unwrap_or(3306),
self.database
);
if let Some(options) = self.options.as_ref() {
url.push('?');
url.push_str(options);
}
url
}
#[must_use]
pub fn disp_connection_string(&self) -> String {
let mut url = format!(
"mariadb://{}:****@{}:{}/{}",
self.username,
self.host,
self.port.unwrap_or(3306),
self.database
);
if let Some(options) = self.options.as_ref() {
url.push('?');
url.push_str(options);
}
url
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub enum OutputTableName {
#[default]
Output,
OutputTest,
}
impl From<OutputTableName> for &'static str {
fn from(value: OutputTableName) -> Self {
match value {
OutputTableName::Output => "output",
OutputTableName::OutputTest => "output_test",
}
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub enum StatusTableName {
#[default]
Status,
StatusTest,
}
impl From<StatusTableName> for &'static str {
fn from(value: StatusTableName) -> Self {
match value {
StatusTableName::Status => "status",
StatusTableName::StatusTest => "status_test",
}
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub enum MissedTick {
#[default]
Burst,
Delay,
Skip,
}
#[derive(Clone, Debug, Decode, Deserialize, Encode, Eq, Getters, PartialEq, Serialize)]
#[cfg_attr(test, derive(Builder))]
#[getset(get = "pub")]
pub struct Schedules {
schedules: Vec<Schedule>,
}
#[cfg(test)]
impl Mock for Schedules {
fn mock() -> Self {
Self::builder()
.schedules(vec![Schedule::mock(), Schedule::mock()])
.build()
}
}
#[derive(Clone, Debug, Decode, Default, Deserialize, Encode, Eq, Getters, PartialEq, Serialize)]
#[cfg_attr(test, derive(Builder))]
#[getset(get = "pub")]
pub struct Schedule {
name: String,
on_calendar: String,
cmds: Vec<String>,
}
#[cfg(test)]
impl Mock for Schedule {
fn mock() -> Self {
Self::builder()
.name("mock_schedule".to_string())
.on_calendar("* * * * *".to_string())
.cmds(vec!["echo 'Hello, World!'".to_string()])
.build()
}
}
pub fn load<'a, S, T, D>(cli: &S, defaults: &D) -> Result<T>
where
T: Deserialize<'a>,
S: Source + Clone + Send + Sync + 'static,
D: PathDefaults,
{
let config_file_path = config_file_path(defaults)?;
let config = Config::builder()
.add_source(
Environment::with_prefix(&defaults.env_prefix())
.separator("_")
.try_parsing(true),
)
.add_source(cli.clone())
.add_source(File::from(config_file_path).format(FileFormat::Toml))
.build()
.with_context(|| Error::ConfigBuild)?;
config
.try_deserialize::<T>()
.with_context(|| Error::ConfigDeserialize)
}
fn config_file_path<D>(defaults: &D) -> Result<PathBuf>
where
D: PathDefaults,
{
let default_fn = || -> Result<PathBuf> { default_config_file_path(defaults) };
defaults
.config_absolute_path()
.as_ref()
.map_or_else(default_fn, to_path_buf)
}
fn default_config_file_path<D>(defaults: &D) -> Result<PathBuf>
where
D: PathDefaults,
{
let mut config_file_path = config_dir().ok_or(Error::ConfigDir)?;
config_file_path.push(defaults.default_file_path());
config_file_path.push(defaults.default_file_name());
let _ = config_file_path.set_extension("toml");
Ok(config_file_path)
}