From ddcc2f2fc521376910faf7ba07ae79006b555231 Mon Sep 17 00:00:00 2001 From: pvshvp-oss Date: Thu, 11 Apr 2024 11:24:51 -0500 Subject: [PATCH 1/8] feat: :construction: Integrating configuration from various sources --- paxy/src/app/config.rs | 30 +++++++++++++++++++++--------- paxy/src/ui/mod.rs | 16 ++++++++++++++-- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/paxy/src/app/config.rs b/paxy/src/app/config.rs index 0a59e0c..7c47720 100644 --- a/paxy/src/app/config.rs +++ b/paxy/src/app/config.rs @@ -3,7 +3,11 @@ /// local paths. overridden by environment variables starting with `PAXY_`, /// overridden by the configuration file specified by the commandline. /// Values from only files with supported file extensions would be merged. -pub fn init_config(config_filepath: Option<&Path>) -> Result<(Config, Vec), Error> { +pub fn init_config(cli_modifier: C) -> Result<(Config, Vec), Error> +where + C: clap::Parser + ui::CliModifier + fmt::Debug, + ::L: LogLevel, +{ let mut candidate_config_filepath_stubs: Vec = Vec::new(); // Global directories @@ -41,10 +45,11 @@ pub fn init_config(config_filepath: Option<&Path>) -> Result<(Config, Vec) -> Result<(Config, Vec &Option; - fn is_json(&self) -> bool; + fn is_json(&self) -> &Option; fn is_plain(&self) -> bool; @@ -237,6 +248,7 @@ use owo_colors::OwoColorize; use snafu::{ResultExt, Snafu}; use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::filter::LevelFilter; +use figment; use crate::app::{self, config, logging}; From 5b373336a1283964f56b05baaeeb2c06d2ebb5a8 Mon Sep 17 00:00:00 2001 From: pvshvp-oss Date: Thu, 11 Apr 2024 22:30:39 -0500 Subject: [PATCH 2/8] Initial attempt --- Cargo.toml | 2 +- paxy/src/app/config.rs | 34 +++++------ paxy/src/app/logging.rs | 17 +++++- paxy/src/ui/mod.rs | 132 ++++++++++++++++++---------------------- 4 files changed, 92 insertions(+), 93 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0688588..fe7a04c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ paxy = { path = "paxy" } # Logging tracing = "0.1" tracing-appender = "0.2" -tracing-subscriber = "0.3" +tracing-subscriber = { version = "0.3", features = ["serde", "tracing-serde"] } # Configuration figment = "0.10" diff --git a/paxy/src/app/config.rs b/paxy/src/app/config.rs index 7c47720..69cb553 100644 --- a/paxy/src/app/config.rs +++ b/paxy/src/app/config.rs @@ -3,10 +3,10 @@ /// local paths. overridden by environment variables starting with `PAXY_`, /// overridden by the configuration file specified by the commandline. /// Values from only files with supported file extensions would be merged. -pub fn init_config(cli_modifier: C) -> Result<(Config, Vec), Error> +pub fn init_config(cli_modifier: &C) -> Result<(Config, Vec), Error> where - C: clap::Parser + ui::CliModifier + fmt::Debug, - ::L: LogLevel, + C: ui::CliModifier, + ::L: clap_verbosity_flag::LogLevel, { let mut candidate_config_filepath_stubs: Vec = Vec::new(); @@ -35,17 +35,14 @@ where // Merge configuration values from global and local filepaths figment = candidate_config_filepath_stubs .iter() - .fold( - figment, - move |figment, candidate_config_filepath_stub| { - admerge_from_stub(candidate_config_filepath_stub, figment) - }, - ); + .fold(figment, move |figment, candidate_config_filepath_stub| { + admerge_from_stub(candidate_config_filepath_stub, figment) + }); // Merge configuration values from environment variables figment = figment.admerge(Env::prefixed(&format!("{}_", *app::APP_NAME))); - // Merge configuration values from additional config filepaths (usually + // Merge configuration values from additional config filepaths (usually // specified through CLI) if let Some(additional_config_filepath) = cli_modifier.config_file() { if let Some(parent) = additional_config_filepath.parent() { @@ -62,17 +59,17 @@ where // These are not set to be optional, so only action-required states are // merged with the configuration if cli_modifier.is_uncolored() { - figment.admerge(("no_color", true)); + figment = figment.admerge(("no_color", true)); } - if let Some(log_level_filter) = cli_input.verbosity_filter() { - figment.admerge(("log_level_filter", log_level_filter)); + if let Some(log_level_filter) = cli_modifier.verbosity_filter() { + figment = figment.admerge(("log_level_filter", log_level_filter)); } Ok(( figment .extract() .context(ExtractConfigSnafu {})?, - candidate_config_filepath_stubs + candidate_config_filepath_stubs, )) } @@ -96,7 +93,7 @@ fn admerge_from_stub(candidate_config_filepath_stub: &PathBuf, mut figment: Figm pub struct Config { pub log_directory: Option, - pub log_level_filter: Option, + pub log_level_filter: Option, pub no_color: Option, } @@ -111,7 +108,7 @@ impl Default for Config { fn default() -> Self { Config { log_directory: None, - log_level_filter: Some(log::LevelFilter::Info), + log_level_filter: Some(LevelFilter::Info), no_color: Some(false), } } @@ -151,7 +148,7 @@ pub enum Error { // region: IMPORTS -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use figment::{ providers::{Env, Format, Json, Toml, Yaml}, @@ -163,7 +160,8 @@ use figment::{ }; use serde::{Deserialize, Serialize}; use snafu::{OptionExt, ResultExt, Snafu}; +use log::LevelFilter; -use crate::{ui, app}; +use crate::{app, ui}; // endregion: IMPORTS diff --git a/paxy/src/app/logging.rs b/paxy/src/app/logging.rs index c7a6478..4b96c57 100644 --- a/paxy/src/app/logging.rs +++ b/paxy/src/app/logging.rs @@ -1,12 +1,14 @@ pub fn init_log( preferred_log_dirpath: Option, - preferred_log_level_filter: Option, + preferred_log_level_filter: Option, ) -> Result<(Handle, PathBuf), Error> { let log_filename = format!("{}.log", *app::APP_NAME); let log_dirpath = obtain_log_dirpath(preferred_log_dirpath)?; let log_file_appender = tracing_appender::rolling::daily(log_dirpath.clone(), log_filename.clone()); - let log_level_filter = preferred_log_level_filter.unwrap_or(LevelFilter::INFO); + let log_level_filter = tracing_level_filter_from_log_level_filter( + preferred_log_level_filter.unwrap_or(log::LevelFilter::Info), + ); // Obtain writers to various logging destinations and worker guards (for // keeping the streams alive) @@ -188,6 +190,17 @@ fn obtain_log_dirpath(preferred_log_dirpath: Option) -> Result LevelFilter { + match level_filter { + log::LevelFilter::Off => LevelFilter::OFF, + log::LevelFilter::Error => LevelFilter::ERROR, + log::LevelFilter::Warn => LevelFilter::WARN, + log::LevelFilter::Info => LevelFilter::INFO, + log::LevelFilter::Debug => LevelFilter::DEBUG, + log::LevelFilter::Trace => LevelFilter::TRACE, + } +} + type OutputModeSwitchFunction = Box Result<(), Error>>; pub struct Handle { diff --git a/paxy/src/ui/mod.rs b/paxy/src/ui/mod.rs index 736512e..21af28b 100644 --- a/paxy/src/ui/mod.rs +++ b/paxy/src/ui/mod.rs @@ -8,37 +8,17 @@ where let cli_input = C::parse(); // Obtain user configuration - let (figment, config_filepaths) = config::init_config( - cli_input - .config_file() - .as_ref() - .map(|f| PathBuf::as_path(&f)), - ) - .context(app::ConfigSnafu {}) - .context(crate::AppSnafu)?; - - if cli_input.is_uncolored() { - figment.admerge(("no_color", true)); - anstream::ColorChoice::Never.write_global(); - owo_colors::set_override(false); - } - if let Some(log_level_filter) = cli_input.verbosity_filter() { - figment.admerge(("log_level_filter", log_level_filter)); - } - - + let (config, config_filepaths) = config::init_config(&cli_input) + .context(app::ConfigSnafu {}) + .context(crate::AppSnafu)?; // Turn off colors if needed - let mut is_cli_uncolored = cli_input.is_uncolored(); - if !is_cli_uncolored { - if let Some(no_color) = config.no_color { - is_cli_uncolored = no_color; + if let Some(no_color) = config.no_color { + if no_color { + anstream::ColorChoice::Never.write_global(); + owo_colors::set_override(false); } } - if is_cli_uncolored { - anstream::ColorChoice::Never.write_global(); - owo_colors::set_override(false); - } // Begin logging with preferred log directory and preferred verbosity let config_log_dirpath = config @@ -52,10 +32,7 @@ where .parse() .ok() }); - let verbosity_filter = cli_input - .verbosity_filter() - .or(config_verbosity_filter); - let (mut handle, log_filepath) = logging::init_log(config_log_dirpath, verbosity_filter) + let (mut handle, log_filepath) = logging::init_log(config_log_dirpath, config_verbosity_filter.into()) .context(app::LoggingSnafu {}) .context(crate::AppSnafu {})?; @@ -77,7 +54,15 @@ where .context(crate::AppSnafu {})?; } - // Welcome message + emit_welcome_messages(); + emit_diagnostic_messages(config_filepaths, log_filepath, &cli_input); + emit_test_messages(); + + Ok((cli_input, handle.worker_guards)) +} + +fn emit_welcome_messages() { + // Welcome messages tracing::debug!( "{} - {}", "Paxy".bold(), @@ -89,41 +74,13 @@ where "shivanandvp".italic(), "".italic() ); - tracing::debug!( - target:"TEST", "{}{}{}{}{}{}{}{}", - "███".black(), - "███".red(), - "███".green(), - "███".yellow(), - "███".blue(), - "███".purple(), - "███".cyan(), - "███".white() - ); - tracing::debug!( - target:"TEST", "{}{}{}{}{}{}{}{}", - "███".bright_black(), - "███".bright_red(), - "███".bright_green(), - "███".bright_yellow(), - "███".bright_blue(), - "███".bright_purple(), - "███".bright_cyan(), - "███".bright_white() - ); - - if cli_input.is_test() { - // Test messages - tracing::trace!(target:"TEST", "{} Testing trace!...", console::Emoji("🧪", "")); - tracing::debug!(target:"TEST", "{} Testing debug!...", console::Emoji("🧪", "")); - tracing::info!(target:"TEST", "{} Testing info!...", console::Emoji("🧪", "")); - tracing::warn!(target:"TEST", "{} Testing warn!...", console::Emoji("🧪", "")); - tracing::error!(target:"TEST", "{} Testing error!...", console::Emoji("🧪", "")); - - tracing::info!(target:"JSON", "{} Testing: {}", console::Emoji("🧪", ""), "{\"JSON\": \"Target\"}"); - tracing::info!(target:"PLAIN", "{} Testing: Plain Target", console::Emoji("🧪", "")); - } +} +fn emit_diagnostic_messages(config_filepaths: Vec, log_filepath: PathBuf, cli_input: &C) +where + C: clap::Parser + CliModifier + fmt::Debug, + ::L: LogLevel, +{ tracing::debug!( "{} The {} is {}... {}", console::Emoji("⚙️", ""), @@ -160,8 +117,40 @@ where .dimmed(), cli_input.dimmed() ); +} - Ok((cli_input, handle.worker_guards)) +fn emit_test_messages() { + tracing::debug!( + target:"TEST", "{}{}{}{}{}{}{}{}", + "███".black(), + "███".red(), + "███".green(), + "███".yellow(), + "███".blue(), + "███".purple(), + "███".cyan(), + "███".white() + ); + tracing::debug!( + target:"TEST", "{}{}{}{}{}{}{}{}", + "███".bright_black(), + "███".bright_red(), + "███".bright_green(), + "███".bright_yellow(), + "███".bright_blue(), + "███".bright_purple(), + "███".bright_cyan(), + "███".bright_white() + ); + + tracing::trace!(target:"TEST", "{} Testing trace!...", console::Emoji("🧪", "")); + tracing::debug!(target:"TEST", "{} Testing debug!...", console::Emoji("🧪", "")); + tracing::info!(target:"TEST", "{} Testing info!...", console::Emoji("🧪", "")); + tracing::warn!(target:"TEST", "{} Testing warn!...", console::Emoji("🧪", "")); + tracing::error!(target:"TEST", "{} Testing error!...", console::Emoji("🧪", "")); + + tracing::info!(target:"JSON", "{} Testing: {}", console::Emoji("🧪", ""), "{\"JSON\": \"Target\"}"); + tracing::info!(target:"PLAIN", "{} Testing: Plain Target", console::Emoji("🧪", "")); } impl CliModifier for T @@ -177,7 +166,7 @@ where { fn verbosity_filter(&self) -> Option { if self.is_plain() || self.is_json() { - return Some(LevelFilter::INFO); + return Some(LevelFilter::Info); } let verbosity_flag_filter = self @@ -185,7 +174,7 @@ where .log_level_filter(); if verbosity_flag_filter < clap_verbosity_flag::LevelFilter::Debug && self.is_debug() { - return Some(LevelFilter::DEBUG); + return Some(LevelFilter::Debug); } verbosity_flag_filter @@ -215,7 +204,7 @@ pub trait GlobalArguments { fn config_file(&self) -> &Option; - fn is_json(&self) -> &Option; + fn is_json(&self) -> bool; fn is_plain(&self) -> bool; @@ -247,8 +236,7 @@ use clap_verbosity_flag::LogLevel; use owo_colors::OwoColorize; use snafu::{ResultExt, Snafu}; use tracing_appender::non_blocking::WorkerGuard; -use tracing_subscriber::filter::LevelFilter; -use figment; +use log::LevelFilter; use crate::app::{self, config, logging}; From 35dfc5f280eaa0aaed9d705d5fd6303963fc6dde Mon Sep 17 00:00:00 2001 From: pvshvp-oss Date: Fri, 12 Apr 2024 02:56:15 -0500 Subject: [PATCH 3/8] . --- Cargo.toml | 2 +- paxy/src/app/config.rs | 45 +++++++++++-------- paxy/src/app/i18n.rs | 16 ++++--- paxy/src/app/mod.rs | 47 +++++++++++-------- paxy/src/{ui/mod.rs => app/ui.rs} | 75 +++++++++++++++++++++++++------ paxy/src/lib.rs | 30 ++++++------- requirements.txt | 0 7 files changed, 141 insertions(+), 74 deletions(-) rename paxy/src/{ui/mod.rs => app/ui.rs} (83%) delete mode 100644 requirements.txt diff --git a/Cargo.toml b/Cargo.toml index fe7a04c..0688588 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ paxy = { path = "paxy" } # Logging tracing = "0.1" tracing-appender = "0.2" -tracing-subscriber = { version = "0.3", features = ["serde", "tracing-serde"] } +tracing-subscriber = "0.3" # Configuration figment = "0.10" diff --git a/paxy/src/app/config.rs b/paxy/src/app/config.rs index 69cb553..b66ffe8 100644 --- a/paxy/src/app/config.rs +++ b/paxy/src/app/config.rs @@ -91,6 +91,8 @@ fn admerge_from_stub(candidate_config_filepath_stub: &PathBuf, mut figment: Figm #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Config { + pub paths: app::Paths, + pub log_directory: Option, pub log_level_filter: Option, @@ -128,6 +130,29 @@ impl Provider for Config { } } +// region: IMPORTS + +use std::path::PathBuf; + +use figment::{ + providers::{Env, Format, Json, Toml, Yaml}, + value::{Dict, Map}, + Figment, + Metadata, + Profile, + Provider, +}; +use log::LevelFilter; +use serde::{Deserialize, Serialize}; +use snafu::{OptionExt, ResultExt, Snafu}; + +use crate::app; +use crate::app::ui; + +// endregion: IMPORTS + +// region: ERRORS + #[derive(Debug, Snafu)] #[non_exhaustive] pub enum Error { @@ -146,22 +171,4 @@ pub enum Error { ExtractConfig { source: figment::Error }, } -// region: IMPORTS - -use std::path::PathBuf; - -use figment::{ - providers::{Env, Format, Json, Toml, Yaml}, - value::{Dict, Map}, - Figment, - Metadata, - Profile, - Provider, -}; -use serde::{Deserialize, Serialize}; -use snafu::{OptionExt, ResultExt, Snafu}; -use log::LevelFilter; - -use crate::{app, ui}; - -// endregion: IMPORTS +// endregion: ERRORS diff --git a/paxy/src/app/i18n.rs b/paxy/src/app/i18n.rs index 5a5f554..13d5087 100644 --- a/paxy/src/app/i18n.rs +++ b/paxy/src/app/i18n.rs @@ -1,3 +1,13 @@ +// TODO: The module code goes here + +// region: IMPORTS + +use snafu::Snafu; + +// endregion: IMPORTS + +// region: ERRORS + #[derive(Debug, Snafu)] #[non_exhaustive] pub enum Error { @@ -6,8 +16,4 @@ pub enum Error { I18nDummy {}, } -// region: IMPORTS - -use snafu::Snafu; - -// endregion: IMPORTS +// endregion: ERRORS \ No newline at end of file diff --git a/paxy/src/app/mod.rs b/paxy/src/app/mod.rs index ddba724..6d367b8 100644 --- a/paxy/src/app/mod.rs +++ b/paxy/src/app/mod.rs @@ -2,6 +2,24 @@ lazy_static! { pub static ref APP_NAME: &'static str = "paxy"; } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Paths { + pub config_dirpaths: Vec, + pub log_dirpath: PathBuf, +} + +// region: IMPORTS + +use std::path::PathBuf; + +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use snafu::Snafu; + +// endregion: IMPORTS + +// region: ERRORS + #[derive(Debug, Snafu)] #[non_exhaustive] pub enum Error { @@ -19,6 +37,13 @@ pub enum Error { source: config::Error, }, + #[non_exhaustive] + #[snafu(display("in the UI: {source}"), visibility(pub))] + Ui { + #[snafu(backtrace)] + source: ui::Error, + }, + #[non_exhaustive] #[snafu(display("in internationalization: {source}"), visibility(pub))] Internationalization { @@ -27,27 +52,13 @@ pub enum Error { }, } -// region: IMPORTS -use lazy_static::lazy_static; -use snafu::Snafu; +// endregion: ERRORS -// endregion: IMPORTS - -// region: MODULES +// region: EXTERNAL-SUBMODULES pub mod config; pub mod i18n; pub mod logging; +pub mod ui; -// endregion: MODULES - -// region: RE-EXPORTS - -#[allow(unused_imports)] -pub use config::*; -#[allow(unused_imports)] -pub use i18n::*; -#[allow(unused_imports)] -pub use logging::*; - -// endregion: RE-EXPORTS +// endregion: EXTERNAL-SUBMODULES diff --git a/paxy/src/ui/mod.rs b/paxy/src/app/ui.rs similarity index 83% rename from paxy/src/ui/mod.rs rename to paxy/src/app/ui.rs index 21af28b..c5aae31 100644 --- a/paxy/src/ui/mod.rs +++ b/paxy/src/app/ui.rs @@ -32,9 +32,10 @@ where .parse() .ok() }); - let (mut handle, log_filepath) = logging::init_log(config_log_dirpath, config_verbosity_filter.into()) - .context(app::LoggingSnafu {}) - .context(crate::AppSnafu {})?; + let (mut handle, log_filepath) = + logging::init_log(config_log_dirpath, config_verbosity_filter.into()) + .context(app::LoggingSnafu {}) + .context(crate::AppSnafu {})?; // Modify logging behavior if Plain or Json output is desired if cli_input.is_json() { @@ -153,6 +154,51 @@ fn emit_test_messages() { tracing::info!(target:"PLAIN", "{} Testing: Plain Target", console::Emoji("🧪", "")); } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CliFormat { + pub output_mode: Option, + verbosity_level_filter: log::LevelFilter, + pub is_colored: Option, +} + +impl CliFormat { + pub fn try_set_max_verbosity_level(suggested_max_verbosity_level: log::LevelFilter) -> Result<(),> { + return match self.output_mode.unwrap_or_default() { + CliOutputMode::Regular => suggested_max_verbosity_level.as_str().parse().unwrap(), + CliOutputMode::Plain => todo!(), + CliOutputMode::Json => todo!(), + CliOutputMode::Test => todo!(), + }; + } + pub fn max_verbosity_level(&self) -> log::LevelFilter { + return verbosity_level_filter; + } +} + +impl Default for CliFormat { + fn default() -> Self { + Self { + output_mode: Default::default(), + verbosity_level_filter: log::LevelFilter::Info, + is_colored: Some(true), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum CliOutputMode { + Regular, + Plain, + Json, + Test, +} + +impl Default for CliOutputMode { + fn default() -> Self { + CliOutputMode::Regular + } +} + impl CliModifier for T where T: GlobalArguments, @@ -165,22 +211,21 @@ where ::L: LogLevel, { fn verbosity_filter(&self) -> Option { - if self.is_plain() || self.is_json() { - return Some(LevelFilter::Info); - } - let verbosity_flag_filter = self .verbosity() .log_level_filter(); - if verbosity_flag_filter < clap_verbosity_flag::LevelFilter::Debug && self.is_debug() { + if self.is_plain() || self.is_json() { + return Some(LevelFilter::Info); + } else if verbosity_flag_filter < clap_verbosity_flag::LevelFilter::Debug && self.is_debug() + { return Some(LevelFilter::Debug); + } else { + return verbosity_flag_filter + .as_str() + .parse() + .ok(); } - - verbosity_flag_filter - .as_str() - .parse() - .ok() } fn is_uncolored(&self) -> bool { @@ -233,10 +278,12 @@ use core::fmt; use std::{env, path::PathBuf}; use clap_verbosity_flag::LogLevel; +use log::LevelFilter; use owo_colors::OwoColorize; +use serde::{Deserialize, Serialize}; use snafu::{ResultExt, Snafu}; +use tracing::Level; use tracing_appender::non_blocking::WorkerGuard; -use log::LevelFilter; use crate::app::{self, config, logging}; diff --git a/paxy/src/lib.rs b/paxy/src/lib.rs index 0f1bfa9..0790bf0 100644 --- a/paxy/src/lib.rs +++ b/paxy/src/lib.rs @@ -2,6 +2,16 @@ pub fn type_of(_: &T) -> &str { any::type_name::() } +// region: IMPORTS + +use std::any; + +use snafu::Snafu; + +// endregion: IMPORTS + +// region: ERRORS + #[derive(Debug, Snafu)] #[non_exhaustive] pub enum Error { @@ -12,13 +22,6 @@ pub enum Error { source: app::Error, }, - #[non_exhaustive] - #[snafu(display("in the UI: {source}"), visibility(pub))] - Ui { - #[snafu(backtrace)] - source: ui::Error, - }, - #[non_exhaustive] #[snafu(display("in an action:{source}"), visibility(pub))] Actions { @@ -27,19 +30,12 @@ pub enum Error { }, } -// region: IMPORTS - -use std::any; - -use snafu::Snafu; - -// endregion: IMPORTS +// endregion: ERRORS -// region: MODULES +// region: EXTERNAL-SUBMODULES pub mod actions; pub mod app; pub mod data; -pub mod ui; -// endregion: MODULES +// endregion: EXTERNAL-SUBMODULES diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e69de29..0000000 From 1be564e9674dd97f47e87ff3a91adf46805d847a Mon Sep 17 00:00:00 2001 From: pvshvp-oss Date: Mon, 22 Apr 2024 19:16:05 -0500 Subject: [PATCH 4/8] . --- paxy/src/app/ui.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/paxy/src/app/ui.rs b/paxy/src/app/ui.rs index c5aae31..dc6d514 100644 --- a/paxy/src/app/ui.rs +++ b/paxy/src/app/ui.rs @@ -157,21 +157,16 @@ fn emit_test_messages() { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CliFormat { pub output_mode: Option, - verbosity_level_filter: log::LevelFilter, + pub requested_verbosity: Option, pub is_colored: Option, } impl CliFormat { - pub fn try_set_max_verbosity_level(suggested_max_verbosity_level: log::LevelFilter) -> Result<(),> { - return match self.output_mode.unwrap_or_default() { - CliOutputMode::Regular => suggested_max_verbosity_level.as_str().parse().unwrap(), - CliOutputMode::Plain => todo!(), - CliOutputMode::Json => todo!(), - CliOutputMode::Test => todo!(), - }; - } - pub fn max_verbosity_level(&self) -> log::LevelFilter { - return verbosity_level_filter; + pub fn resolve_max_verbosity_level() -> LevelFilter { + match self.output_mode { + Some(CliOutputMode::Plain) | Some(CliOutputMode::Json) => return Some(LevelFilter::Info), + _ => return verbosity_level_filter, + } } } From f3a8a9cd3d905d58c6a6371d81c2359bcf708a52 Mon Sep 17 00:00:00 2001 From: pvshvp-oss Date: Sun, 5 May 2024 17:27:44 -0500 Subject: [PATCH 5/8] fix: :construction: WIP fixing configuration --- paxy-cli/src/lib.rs | 11 ++- paxy-cli/src/main.rs | 7 ++ paxy-gui/src/lib.rs | 19 +++++- paxy-gui/src/main.rs | 7 ++ paxy/Cargo.toml | 2 +- paxy/src/app/config.rs | 29 ++++---- paxy/src/app/i18n.rs | 2 +- paxy/src/app/logging.rs | 9 +-- paxy/src/app/mod.rs | 6 -- paxy/src/app/ui.rs | 147 +++++++++++++++++++++++++--------------- 10 files changed, 150 insertions(+), 89 deletions(-) diff --git a/paxy-cli/src/lib.rs b/paxy-cli/src/lib.rs index a0883de..e81535e 100644 --- a/paxy-cli/src/lib.rs +++ b/paxy-cli/src/lib.rs @@ -1,5 +1,12 @@ +//! Has the [`run_cli`] function and the commandline interface template +//! [`cli_template::CliTemplate`] + +/// Calls the [`ui::run_common::`] function supplying it with the commandline +/// interface template as a type. Any errors are thrown back to the calling +/// function. A debug message is then displayed conveying that the program is +/// being run in the CLI mode. pub fn run_cli() -> Result<(), paxy::Error> { - let (_cli_input, _worker_guards) = ui::run_common::()?; + let (_cli_input, _logging_worker_guards) = ui::run_common::()?; tracing::debug!( "Running in {} mode... {}", @@ -408,6 +415,6 @@ mod cli_template { // region: RE-EXPORTS -pub use cli_template::*; +pub use cli_template::*; // Flatten the module heirarchy for easier access // endregion: RE-EXPORTS diff --git a/paxy-cli/src/main.rs b/paxy-cli/src/main.rs index 7a7b4c3..73509b7 100644 --- a/paxy-cli/src/main.rs +++ b/paxy-cli/src/main.rs @@ -1,3 +1,10 @@ +//! Starts execution at the [`main`] function. Offloads the implemenation +//! details to its corresponding library crate. + +/// Calls the [`paxy_cli::run_cli`] function and captures the returned +/// [`Result`]. If there was an error, the error message chain is printed to the +/// standard error stream (`stderr`). The program then returns an `0` or `1` +/// corresponding to "no error" or "error" based on the result. fn main() -> process::ExitCode { let return_value = paxy_cli::run_cli(); match return_value { diff --git a/paxy-gui/src/lib.rs b/paxy-gui/src/lib.rs index 7fb4393..dd71737 100644 --- a/paxy-gui/src/lib.rs +++ b/paxy-gui/src/lib.rs @@ -1,5 +1,12 @@ +//! Has the [`run_gui`] function and the commandline interface template +//! [`gui_cli_template::CliTemplate`] + +/// Calls the [`ui::run_common::`] function supplying it with the commandline +/// interface template as a type. Any errors are thrown back to the calling +/// function. A debug message is then displayed conveying that the program is +/// being run in the GUI mode. pub fn run_gui() -> Result<(), paxy::Error> { - let (_cli_input, _worker_guards) = ui::run_common::()?; + let (_cli_input, _logging_worker_guards) = ui::run_common::()?; tracing::debug!( "Running in {} mode... {}", @@ -15,7 +22,7 @@ pub fn run_gui() -> Result<(), paxy::Error> { pub enum Error { #[non_exhaustive] #[snafu(display(""), visibility(pub))] - GuiDummy {}, + GuiDummy {}, // No errors implemented yet } // region: IMPORTS @@ -28,7 +35,11 @@ use snafu::Snafu; // region: MODULES +/// The commandline interface for the GUI program. Allows one to specify flags +/// that control output on a console. mod gui_cli_template { + + /// The base commandline template consists of global arguments #[derive(Parser, Debug)] #[command(version, author, about, args_conflicts_with_subcommands = true)] pub struct CliTemplate { @@ -36,6 +47,8 @@ mod gui_cli_template { pub global_args: ui::cli_template::GlobalArgs, } + /// Implement a trait that can extract standard global arguments from our + /// own CLI template impl ui::GlobalArguments for CliTemplate { type L = clap_verbosity_flag::InfoLevel; @@ -91,6 +104,6 @@ mod gui_cli_template { // region: RE-EXPORTS -pub use gui_cli_template::*; +pub use gui_cli_template::*; // Flatten the module heirarchy for easier access // endregion: RE-EXPORTS diff --git a/paxy-gui/src/main.rs b/paxy-gui/src/main.rs index edf4ea4..f96597b 100644 --- a/paxy-gui/src/main.rs +++ b/paxy-gui/src/main.rs @@ -1,3 +1,10 @@ +//! Starts execution at the [`main`] function. Offloads the implemenation +//! details to its corresponding library crate. + +/// Calls the [`paxy_gui::run_gui`] function and captures the returned +/// [`Result`]. If there was an error, the error message chain is printed to the +/// standard error stream (`stderr`). The program then returns an `0` or `1` +/// corresponding to "no error" or "error" based on the result. fn main() -> process::ExitCode { let return_value = paxy_gui::run_gui(); match return_value { diff --git a/paxy/Cargo.toml b/paxy/Cargo.toml index 1af4864..949f1fe 100644 --- a/paxy/Cargo.toml +++ b/paxy/Cargo.toml @@ -53,7 +53,7 @@ console = { workspace = true } home = "0.5.9" toml = "0.8.10" pollster = "0.3" -reqwest = "0.11" +reqwest = "0.12" url = { version = "2.3", features = ["serde"] } extism = "1.2.0" bson = "2.9.0" diff --git a/paxy/src/app/config.rs b/paxy/src/app/config.rs index b66ffe8..1623935 100644 --- a/paxy/src/app/config.rs +++ b/paxy/src/app/config.rs @@ -3,10 +3,10 @@ /// local paths. overridden by environment variables starting with `PAXY_`, /// overridden by the configuration file specified by the commandline. /// Values from only files with supported file extensions would be merged. -pub fn init_config(cli_modifier: &C) -> Result<(Config, Vec), Error> +pub fn init_config(cli_global_arguments: G) -> Result<(Config, Vec), Error> where - C: ui::CliModifier, - ::L: clap_verbosity_flag::LogLevel, + G: ui::GlobalArguments, + ::L: clap_verbosity_flag::LogLevel, { let mut candidate_config_filepath_stubs: Vec = Vec::new(); @@ -44,7 +44,7 @@ where // Merge configuration values from additional config filepaths (usually // specified through CLI) - if let Some(additional_config_filepath) = cli_modifier.config_file() { + if let Some(additional_config_filepath) = preferred_config_filepath { if let Some(parent) = additional_config_filepath.parent() { if let Some(stem) = additional_config_filepath.file_stem() { let mut stub = PathBuf::from(parent); @@ -58,10 +58,10 @@ where // Merge configuration values from the CLI // These are not set to be optional, so only action-required states are // merged with the configuration - if cli_modifier.is_uncolored() { + if cli_global_arguments.is_uncolored() { figment = figment.admerge(("no_color", true)); } - if let Some(log_level_filter) = cli_modifier.verbosity_filter() { + if let Some(log_level_filter) = cli_global_arguments.verbosity_filter() { figment = figment.admerge(("log_level_filter", log_level_filter)); } @@ -91,13 +91,11 @@ fn admerge_from_stub(candidate_config_filepath_stub: &PathBuf, mut figment: Figm #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Config { - pub paths: app::Paths, + pub config_dirpaths: Vec, - pub log_directory: Option, + pub log_dirpath: PathBuf, - pub log_level_filter: Option, - - pub no_color: Option, + pub cli_output_format: ui::CliOutputFormat, } impl Config { @@ -108,10 +106,11 @@ impl Config { impl Default for Config { fn default() -> Self { - Config { - log_directory: None, - log_level_filter: Some(LevelFilter::Info), - no_color: Some(false), + Self { + config_dirpaths: None, + log_dirpath: None, + log_directory: todo!(), + cli_output_format: todo!(), } } } diff --git a/paxy/src/app/i18n.rs b/paxy/src/app/i18n.rs index 13d5087..ef0eadc 100644 --- a/paxy/src/app/i18n.rs +++ b/paxy/src/app/i18n.rs @@ -16,4 +16,4 @@ pub enum Error { I18nDummy {}, } -// endregion: ERRORS \ No newline at end of file +// endregion: ERRORS diff --git a/paxy/src/app/logging.rs b/paxy/src/app/logging.rs index 4b96c57..86f27f4 100644 --- a/paxy/src/app/logging.rs +++ b/paxy/src/app/logging.rs @@ -1,14 +1,9 @@ -pub fn init_log( - preferred_log_dirpath: Option, - preferred_log_level_filter: Option, -) -> Result<(Handle, PathBuf), Error> { +pub fn init_log(max_output_verbosity: log::LevelFilter) -> Result<(Handle, PathBuf), Error> { let log_filename = format!("{}.log", *app::APP_NAME); let log_dirpath = obtain_log_dirpath(preferred_log_dirpath)?; let log_file_appender = tracing_appender::rolling::daily(log_dirpath.clone(), log_filename.clone()); - let log_level_filter = tracing_level_filter_from_log_level_filter( - preferred_log_level_filter.unwrap_or(log::LevelFilter::Info), - ); + let log_level_filter = tracing_level_filter_from_log_level_filter(max_output_verbosity); // Obtain writers to various logging destinations and worker guards (for // keeping the streams alive) diff --git a/paxy/src/app/mod.rs b/paxy/src/app/mod.rs index 6d367b8..9e7e534 100644 --- a/paxy/src/app/mod.rs +++ b/paxy/src/app/mod.rs @@ -2,12 +2,6 @@ lazy_static! { pub static ref APP_NAME: &'static str = "paxy"; } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Paths { - pub config_dirpaths: Vec, - pub log_dirpath: PathBuf, -} - // region: IMPORTS use std::path::PathBuf; diff --git a/paxy/src/app/ui.rs b/paxy/src/app/ui.rs index dc6d514..199464c 100644 --- a/paxy/src/app/ui.rs +++ b/paxy/src/app/ui.rs @@ -1,65 +1,94 @@ #[tracing::instrument(level = "trace")] pub fn run_common() -> Result<(C, Vec), crate::Error> where - C: clap::Parser + CliModifier + fmt::Debug, + C: clap::Parser + GlobalArguments + fmt::Debug, ::L: LogLevel, { // Obtain CLI arguments let cli_input = C::parse(); // Obtain user configuration - let (config, config_filepaths) = config::init_config(&cli_input) + let (config, config_filepath_stubs) = config::init_config(&cli_input.config_file()) .context(app::ConfigSnafu {}) .context(crate::AppSnafu)?; - // Turn off colors if needed - if let Some(no_color) = config.no_color { - if no_color { - anstream::ColorChoice::Never.write_global(); - owo_colors::set_override(false); - } + // Begin logging + let (mut logging_handle, log_filepath) = logging::init_log(&config.) + .context(app::LoggingSnafu {}) + .context(crate::AppSnafu {})?; + + // Adjust output formatting if requested + adjust_output_formatting(&config.cli_output_format, &logging_handle); + + emit_welcome_messages(); + + emit_diagnostic_messages(config_filepath_stubs, log_filepath, &cli_input); + + emit_test_messages(); + + Ok((cli_input, logging_handle.worker_guards)) +} + +fn resolve_max_output_verbosity(cli_output_format: &CliOutputFormat, cli_global_arguments: G) { + let verbosity_flag_filter = cli_output_format + .verbosity() + .log_level_filter(); + + if matches!( + cli_output_format.output_mode, + CliOutputMode::Plain | CliOutputMode::Json + ){ + return Some(LevelFilter::Info); + } else if verbosity_flag_filter < clap_verbosity_flag::LevelFilter::Debug && cli_global_arguments.is_debug() + { + return Some(LevelFilter::Debug); + } else { + return verbosity_flag_filter + .as_str() + .parse() + .ok(); } +} - // Begin logging with preferred log directory and preferred verbosity - let config_log_dirpath = config - .log_directory - .as_ref() - .map(PathBuf::from); - let config_verbosity_filter: Option = config - .log_level_filter - .and_then(|lf| { - lf.as_str() - .parse() - .ok() - }); - let (mut handle, log_filepath) = - logging::init_log(config_log_dirpath, config_verbosity_filter.into()) - .context(app::LoggingSnafu {}) - .context(crate::AppSnafu {})?; +fn adjust_output_formatting( + cli_output_format: &CliOutputFormat, + mut logging_handle: &logging::Handle, +) { + // Turn off colors if requested + if matches!( + cli_output_format.output_mode, + CliOutputMode::Plain | CliOutputMode::Json + ) || cli_output_format.no_color + || is_env_variable_set("NO_COLOR") + || is_env_variable_set(format!( + "{}_NO_COLOR", + String::from(*app::APP_NAME).to_uppercase() + )) + { + anstream::ColorChoice::Never.write_global(); + owo_colors::set_override(false); + } - // Modify logging behavior if Plain or Json output is desired - if cli_input.is_json() { - handle - .switch_to_json() - .context(app::LoggingSnafu {}) - .context(crate::AppSnafu {})?; - } else if cli_input.is_plain() { - handle + // Change output mode if requested + match cli_output_format.output_mode { + CliOutputMode::Plain => logging_handle .switch_to_plain() .context(app::LoggingSnafu {}) - .context(crate::AppSnafu {})?; - } else if cli_input.is_test() { - handle + .context(crate::AppSnafu {})?, + CliOutputMode::Json => logging_handle + .switch_to_json() + .context(app::LoggingSnafu {}) + .context(crate::AppSnafu {})?, + CliOutputMode::Test => logging_handle .switch_to_test() .context(app::LoggingSnafu {}) - .context(crate::AppSnafu {})?; + .context(crate::AppSnafu {})?, + _ => {} } +} - emit_welcome_messages(); - emit_diagnostic_messages(config_filepaths, log_filepath, &cli_input); - emit_test_messages(); - - Ok((cli_input, handle.worker_guards)) +fn is_env_variable_set>(env_variable_name: S) -> bool { + env::var(env_variable_name.as_ref()).map_or(false, |value| !value.is_empty()) } fn emit_welcome_messages() { @@ -77,8 +106,11 @@ fn emit_welcome_messages() { ); } -fn emit_diagnostic_messages(config_filepaths: Vec, log_filepath: PathBuf, cli_input: &C) -where +fn emit_diagnostic_messages( + config_filepath_stubs: Vec, + log_filepath: PathBuf, + cli_input: &C, +) where C: clap::Parser + CliModifier + fmt::Debug, ::L: LogLevel, { @@ -101,8 +133,9 @@ where "{} {} {:?}", console::Emoji("📂", ""), "Config Filepath(s) (without file extensions):".magenta(), - config_filepaths, + config_dirpaths, ); + tracing::debug!( "{} {} {:?}", console::Emoji("📂", ""), @@ -155,27 +188,31 @@ fn emit_test_messages() { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CliFormat { - pub output_mode: Option, - pub requested_verbosity: Option, - pub is_colored: Option, +pub struct CliOutputFormat { + pub output_mode: CliOutputMode, + + pub requested_verbosity: log::LevelFilter, + + pub no_color: bool, } -impl CliFormat { +impl CliOutputFormat { pub fn resolve_max_verbosity_level() -> LevelFilter { match self.output_mode { - Some(CliOutputMode::Plain) | Some(CliOutputMode::Json) => return Some(LevelFilter::Info), + Some(CliOutputMode::Plain) | Some(CliOutputMode::Json) => { + return Some(LevelFilter::Info) + } _ => return verbosity_level_filter, } } } -impl Default for CliFormat { +impl Default for CliOutputFormat { fn default() -> Self { Self { output_mode: Default::default(), - verbosity_level_filter: log::LevelFilter::Info, - is_colored: Some(true), + requested_verbosity: Some(log::LevelFilter::Info), + is_colored: Some(true), } } } @@ -286,6 +323,8 @@ use crate::app::{self, config, logging}; // region: MODULES +/// Common commandline interface template for global arguments, intended to be +/// shared between the GUI and CLI programs. pub mod cli_template { #[derive(Clone, Debug, Args)] #[command(next_display_order = usize::MAX - 100)] @@ -360,6 +399,6 @@ pub mod cli_template { // region: RE-EXPORTS #[allow(unused_imports)] -pub use cli_template::*; +pub use cli_template::*; // Flatten the module heirarchy for easier access // endregion: RE-EXPORTS From dcd64e0cb06c2608e45739108735145c09b5f1ed Mon Sep 17 00:00:00 2001 From: pvshvp-oss Date: Mon, 6 May 2024 16:09:42 -0500 Subject: [PATCH 6/8] WIP --- paxy-cli/src/lib.rs | 42 ++++++------ paxy-gui/src/lib.rs | 30 ++++----- paxy/src/app/config.rs | 106 +++++++++++++++++++++++------- paxy/src/app/ui.rs | 142 ++++++++++++++++++++++++++++------------- 4 files changed, 213 insertions(+), 107 deletions(-) diff --git a/paxy-cli/src/lib.rs b/paxy-cli/src/lib.rs index e81535e..3402db6 100644 --- a/paxy-cli/src/lib.rs +++ b/paxy-cli/src/lib.rs @@ -58,7 +58,7 @@ mod cli_template { )] pub struct CliTemplate { #[command(flatten)] - pub global_arguments: ui::cli_template::GlobalArgs, + pub global_args: ui::cli_template::GlobalArgs, #[command(subcommand)] pub entity: Option, @@ -67,43 +67,39 @@ mod cli_template { /// Implement a trait that can extract standard global arguments from our /// own CLI template impl ui::GlobalArguments for CliTemplate { - type L = clap_verbosity_flag::InfoLevel; - - fn config_file(&self) -> &Option { - &self - .global_arguments - .config_file + fn config_filepath(&self) -> &Option { + self.global_args + .config_filepath() } fn is_json(&self) -> bool { - self.global_arguments - .json_flag + self.global_args + .is_json() } fn is_plain(&self) -> bool { - self.global_arguments - .plain_flag + self.global_args + .is_plain() } fn is_debug(&self) -> bool { - self.global_arguments - .debug_flag + self.global_args + .is_debug() } - fn is_no_color(&self) -> bool { - self.global_arguments - .no_color_flag + fn is_test(&self) -> bool { + self.global_args + .is_test() } - fn is_test(&self) -> bool { - self.global_arguments - .test_flag + fn is_no_color(&self) -> bool { + self.global_args + .is_no_color() } - fn verbosity(&self) -> &clap_verbosity_flag::Verbosity { - &self - .global_arguments - .verbose + fn verbosity_filter(&self) -> &log::LevelFilter { + self.global_args + .verbosity_filter() } } diff --git a/paxy-gui/src/lib.rs b/paxy-gui/src/lib.rs index dd71737..b110471 100644 --- a/paxy-gui/src/lib.rs +++ b/paxy-gui/src/lib.rs @@ -50,43 +50,39 @@ mod gui_cli_template { /// Implement a trait that can extract standard global arguments from our /// own CLI template impl ui::GlobalArguments for CliTemplate { - type L = clap_verbosity_flag::InfoLevel; - - fn config_file(&self) -> &Option { - &self - .global_args - .config_file + fn config_filepath(&self) -> &Option { + self.global_args + .config_filepath() } fn is_json(&self) -> bool { self.global_args - .json_flag + .is_json() } fn is_plain(&self) -> bool { self.global_args - .plain_flag + .is_plain() } fn is_debug(&self) -> bool { self.global_args - .debug_flag + .is_debug() } - fn is_no_color(&self) -> bool { + fn is_test(&self) -> bool { self.global_args - .no_color_flag + .is_test() } - fn is_test(&self) -> bool { + fn is_no_color(&self) -> bool { self.global_args - .test_flag + .is_no_color() } - fn verbosity(&self) -> &clap_verbosity_flag::Verbosity { - &self - .global_args - .verbose + fn verbosity_filter(&self) -> &log::LevelFilter { + self.global_args + .verbosity_filter() } } diff --git a/paxy/src/app/config.rs b/paxy/src/app/config.rs index 1623935..3820579 100644 --- a/paxy/src/app/config.rs +++ b/paxy/src/app/config.rs @@ -89,43 +89,102 @@ fn admerge_from_stub(candidate_config_filepath_stub: &PathBuf, mut figment: Figm figment } -#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Config { - pub config_dirpaths: Vec, - - pub log_dirpath: PathBuf, - - pub cli_output_format: ui::CliOutputFormat, + pub figment: Figment, } impl Config { pub fn new() -> Self { - Self::default() + Self { + figment: Figment::from(ConfigTemplate::default()), + } } -} -impl Default for Config { - fn default() -> Self { - Self { - config_dirpaths: None, - log_dirpath: None, - log_directory: todo!(), - cli_output_format: todo!(), + pub fn with_overriding_file>(&mut self, filepath: P) -> &mut Self { + let filepath: &Path = filepath.as_ref(); + if let Some(file_extension) = filepath.extension() { + file_extension = file_extension + .to_string_lossy() + .to_lowercase(); + match (file_extension) { + "toml" => { + self.figment = self + .figment + .admerge(Toml::file(filepath)); + } + "json" => { + self.figment = self + .figment + .admerge(Json::file(filepath)); + } + "yaml" | "yml" => { + self.figment = self + .figment + .admerge(Yaml::file(filepath)); + } + } } + + self } -} -impl Provider for Config { - fn metadata(&self) -> Metadata { - Metadata::named("Library Config") + pub fn with_overriding_files(&mut self, filepaths: I) -> &mut Self + where + P: AsRef, + I: Iterator, + { + filepaths.for_each(|filepath| self.with_overriding_file(filepath)); + + self + } + + pub fn with_overriding_env>(prefix: S) -> &mut Self { + let prefix = prefix.as_ref(); + self.figment = self + .figment + .admerge(Env::prefixed(prefix)); + + self } - fn data(&self) -> Result, figment::Error> { - figment::providers::Serialized::defaults(Config::default()).data() + pub fn with_overriding_env_var>(env_var_name: S) -> &mut Self { + let env_var_name = env_var_name.as_ref(); + self.figment = self + .figment + .admerge(Env::raw().only(&[env_var_name])); + + self + } + + pub fn with_overriding_args(&mut self, arguments: A) -> &mut Self { + if let Some(path) = arguments.config_filepath() { + self.figment = self.figment.admerge(("config_filepaths", path)); + } + + self } - fn profile(&self) -> Option { - None + pub fn object(&self) -> Result { + self.figment + .extract() + .context(ExtractConfigSnafu {})? + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ConfigTemplate { + pub config_filepaths: Vec, + pub log_filepath_stub: PathBuf, + pub console_output_format: ConsoleOutputFormat, +} + +impl Default for ConfigTemplate { + fn default() -> Self { + Self { + config_filepaths: Vec::new(), + log_filepath_stub: PathBuf::default(), + console_output_format: ConsoleOutputFormat::default(), + } } } @@ -145,6 +204,7 @@ use log::LevelFilter; use serde::{Deserialize, Serialize}; use snafu::{OptionExt, ResultExt, Snafu}; +use super::ui::GlobalArguments; use crate::app; use crate::app::ui; diff --git a/paxy/src/app/ui.rs b/paxy/src/app/ui.rs index 199464c..00d1fad 100644 --- a/paxy/src/app/ui.rs +++ b/paxy/src/app/ui.rs @@ -13,9 +13,13 @@ where .context(crate::AppSnafu)?; // Begin logging - let (mut logging_handle, log_filepath) = logging::init_log(&config.) - .context(app::LoggingSnafu {}) - .context(crate::AppSnafu {})?; + let (mut logging_handle, log_filepath) = logging::init_log( + &config + .cli_output_format + .requested_verbosity, + ) + .context(app::LoggingSnafu {}) + .context(crate::AppSnafu {})?; // Adjust output formatting if requested adjust_output_formatting(&config.cli_output_format, &logging_handle); @@ -29,17 +33,19 @@ where Ok((cli_input, logging_handle.worker_guards)) } -fn resolve_max_output_verbosity(cli_output_format: &CliOutputFormat, cli_global_arguments: G) { - let verbosity_flag_filter = cli_output_format - .verbosity() - .log_level_filter(); +fn resolve_max_output_verbosity( + cli_output_format: &CliOutputFormat, + cli_global_arguments: G, +) -> log::LevelFilter { + let verbosity_flag_filter = cli_output_format.requested_verbosity; if matches!( cli_output_format.output_mode, CliOutputMode::Plain | CliOutputMode::Json - ){ + ) { return Some(LevelFilter::Info); - } else if verbosity_flag_filter < clap_verbosity_flag::LevelFilter::Debug && cli_global_arguments.is_debug() + } else if verbosity_flag_filter < clap_verbosity_flag::LevelFilter::Debug + && cli_global_arguments.is_debug() { return Some(LevelFilter::Debug); } else { @@ -188,56 +194,38 @@ fn emit_test_messages() { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CliOutputFormat { - pub output_mode: CliOutputMode, +pub struct ConsoleOutputFormat { + pub output_mode: ConsoleOutputMode, - pub requested_verbosity: log::LevelFilter, + pub max_verbosity: log::LevelFilter, pub no_color: bool, } -impl CliOutputFormat { - pub fn resolve_max_verbosity_level() -> LevelFilter { - match self.output_mode { - Some(CliOutputMode::Plain) | Some(CliOutputMode::Json) => { - return Some(LevelFilter::Info) - } - _ => return verbosity_level_filter, - } - } -} - -impl Default for CliOutputFormat { +impl Default for ConsoleOutputFormat { fn default() -> Self { Self { - output_mode: Default::default(), - requested_verbosity: Some(log::LevelFilter::Info), - is_colored: Some(true), + output_mode: ConsoleOutputMode::default(), + max_verbosity: log::LevelFilter::Info, + no_color: false, } } } #[derive(Debug, Clone, Serialize, Deserialize)] -pub enum CliOutputMode { +pub enum ConsoleOutputMode { Regular, Plain, Json, Test, } -impl Default for CliOutputMode { +impl Default for ConsoleOutputMode { fn default() -> Self { CliOutputMode::Regular } } -impl CliModifier for T -where - T: GlobalArguments, - ::L: LogLevel, -{ -} - pub trait CliModifier: GlobalArguments where ::L: LogLevel, @@ -277,9 +265,7 @@ where } pub trait GlobalArguments { - type L; - - fn config_file(&self) -> &Option; + fn config_filepath(&self) -> &Option; fn is_json(&self) -> bool; @@ -291,9 +277,38 @@ pub trait GlobalArguments { fn is_test(&self) -> bool; - fn verbosity(&self) -> &clap_verbosity_flag::Verbosity - where - Self::L: LogLevel; + fn verbosity_filter(&self) -> &log::LevelFilter; + + fn console_output_mode(&self) -> ConsoleOutputMode { + if self.is_json() { + ConsoleOutputMode::Json + } else if self.is_plain() { + ConsoleOutputMode::Plain + } else if self.is_test() { + ConsoleOutputMode::Test + } else { + ConsoleOutputMode::Regular + } + } + + fn max_output_verbosity(&self) -> log::LevelFilter { + let verbosity_flag_filter = cli_output_format.requested_verbosity; + if matches!( + cli_output_format.output_mode, + CliOutputMode::Plain | CliOutputMode::Json + ) { + return Some(LevelFilter::Info); + } else if verbosity_flag_filter < clap_verbosity_flag::LevelFilter::Debug + && cli_global_arguments.is_debug() + { + return Some(LevelFilter::Debug); + } else { + return verbosity_flag_filter + .as_str() + .parse() + .ok(); + } + } } #[derive(Debug, Snafu)] @@ -309,7 +324,6 @@ pub enum Error { use core::fmt; use std::{env, path::PathBuf}; -use clap_verbosity_flag::LogLevel; use log::LevelFilter; use owo_colors::OwoColorize; use serde::{Deserialize, Serialize}; @@ -382,7 +396,45 @@ pub mod cli_template { pub test_flag: bool, #[command(flatten)] - pub verbose: clap_verbosity_flag::Verbosity, + pub verbosity: clap_verbosity_flag::Verbosity, + } + + impl GlobalArguments for GlobalArgs { + fn config_filepath(&self) -> &Option { + self.config_filepath + } + + fn is_json(&self) -> bool { + self.json_flag + } + + fn is_plain(&self) -> bool { + self.plain_flag + } + + fn is_debug(&self) -> bool { + self.debug_flag + } + + fn is_test(&self) -> bool { + self.test_flag + } + + fn is_no_color(&self) -> bool { + self.no_color_flag + } + + fn verbosity_filter(&self) -> &log::LevelFilter { + self.verbosity + .log_level_filter() + .and_then(|log_level_filter| { + log_level_filter + .as_str() + .parse() + .ok() + }) + .unwrap_or(log::LevelFilter::Info) + } } // region: IMPORTS @@ -391,6 +443,8 @@ pub mod cli_template { use clap::Args; + use super::GlobalArguments; + // endregion: IMPORTS } From d4cfd3b1a59cee3e6f1d52ae0929c62283a04941 Mon Sep 17 00:00:00 2001 From: pvshvp-oss Date: Mon, 6 May 2024 19:19:59 -0500 Subject: [PATCH 7/8] WIP --- paxy-cli/src/lib.rs | 2 +- paxy-gui/src/lib.rs | 2 +- paxy/src/app/config.rs | 89 +++++++++++++++++++++++++++++------------- paxy/src/app/ui.rs | 33 ++++++---------- 4 files changed, 75 insertions(+), 51 deletions(-) diff --git a/paxy-cli/src/lib.rs b/paxy-cli/src/lib.rs index 3402db6..7784a8d 100644 --- a/paxy-cli/src/lib.rs +++ b/paxy-cli/src/lib.rs @@ -97,7 +97,7 @@ mod cli_template { .is_no_color() } - fn verbosity_filter(&self) -> &log::LevelFilter { + fn verbosity_filter(&self) -> log::LevelFilter { self.global_args .verbosity_filter() } diff --git a/paxy-gui/src/lib.rs b/paxy-gui/src/lib.rs index b110471..4f97089 100644 --- a/paxy-gui/src/lib.rs +++ b/paxy-gui/src/lib.rs @@ -80,7 +80,7 @@ mod gui_cli_template { .is_no_color() } - fn verbosity_filter(&self) -> &log::LevelFilter { + fn verbosity_filter(&self) -> log::LevelFilter { self.global_args .verbosity_filter() } diff --git a/paxy/src/app/config.rs b/paxy/src/app/config.rs index 3820579..052a72c 100644 --- a/paxy/src/app/config.rs +++ b/paxy/src/app/config.rs @@ -89,6 +89,23 @@ fn admerge_from_stub(candidate_config_filepath_stub: &PathBuf, mut figment: Figm figment } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ConfigTemplate { + pub config_filepaths: Vec, + pub log_filepath_stub: PathBuf, + pub console_output_format: ConsoleOutputFormat, +} + +impl Default for ConfigTemplate { + fn default() -> Self { + Self { + config_filepaths: Vec::new(), + log_filepath_stub: PathBuf::default(), + console_output_format: ConsoleOutputFormat::default(), + } + } +} + pub struct Config { pub figment: Figment, } @@ -147,18 +164,51 @@ impl Config { self } - pub fn with_overriding_env_var>(env_var_name: S) -> &mut Self { - let env_var_name = env_var_name.as_ref(); - self.figment = self - .figment - .admerge(Env::raw().only(&[env_var_name])); + pub fn with_overriding_args(&mut self, cli_arguments: A) -> &mut Self { + if let Some(path) = cli_arguments.config_filepath() { + self.figment = self + .figment + .admerge(("config_filepaths", path)); + } - self - } + let console_output_mode = cli_arguments.console_output_mode(); + if console_output_mode != ConsoleOutputMode::Regular { + self.figment = self + .figment + .admerge(("console_output_format.mode", console_output_mode)); + } - pub fn with_overriding_args(&mut self, arguments: A) -> &mut Self { - if let Some(path) = arguments.config_filepath() { - self.figment = self.figment.admerge(("config_filepaths", path)); + let current_max_verbosity = self + .figment + .extract_inner::("console_output_format.max_verbosity"); + let requested_max_verbosity = cli_arguments.max_output_verbosity(); + if let Ok(current_max_verbosity) = current_max_verbosity { + if cli_requested_max_verbosity > current_max_verbosity { + self.figment = self + .figment + .admerge(( + "console_output_format.max_verbosity", + requested_max_verbosity, + )) + } + } + + let current_no_color = self + .figment + .extract_inner::("console_output_format.no_color"); + let requested_no_color = + cli_arguments.is_no_color() || cli_arguments.is_plain() || cli_arguments.is_json(); + let env_no_color = env::var("NO_COLOR").is_ok() + || env::var(format!( + "{}_NO_COLOR", + String::from(*app::APP_NAME).to_uppercase() + )) + .is_ok() + || env::var("TERM").is_ok_and(|env_term_value| env_term_value.to_lowercase == "dumb"); + if !current_no_color && (requested_no_color || env_no_color) { + self.figment = self + .figment + .admerge(("console_output_format.no_color", true)); } self @@ -171,23 +221,6 @@ impl Config { } } -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ConfigTemplate { - pub config_filepaths: Vec, - pub log_filepath_stub: PathBuf, - pub console_output_format: ConsoleOutputFormat, -} - -impl Default for ConfigTemplate { - fn default() -> Self { - Self { - config_filepaths: Vec::new(), - log_filepath_stub: PathBuf::default(), - console_output_format: ConsoleOutputFormat::default(), - } - } -} - // region: IMPORTS use std::path::PathBuf; @@ -204,7 +237,7 @@ use log::LevelFilter; use serde::{Deserialize, Serialize}; use snafu::{OptionExt, ResultExt, Snafu}; -use super::ui::GlobalArguments; +use super::ui::{ConsoleOutputMode, GlobalArguments}; use crate::app; use crate::app::ui; diff --git a/paxy/src/app/ui.rs b/paxy/src/app/ui.rs index 00d1fad..0aa279f 100644 --- a/paxy/src/app/ui.rs +++ b/paxy/src/app/ui.rs @@ -40,7 +40,7 @@ fn resolve_max_output_verbosity( let verbosity_flag_filter = cli_output_format.requested_verbosity; if matches!( - cli_output_format.output_mode, + cli_output_format.mode, CliOutputMode::Plain | CliOutputMode::Json ) { return Some(LevelFilter::Info); @@ -62,7 +62,7 @@ fn adjust_output_formatting( ) { // Turn off colors if requested if matches!( - cli_output_format.output_mode, + cli_output_format.mode, CliOutputMode::Plain | CliOutputMode::Json ) || cli_output_format.no_color || is_env_variable_set("NO_COLOR") @@ -76,7 +76,7 @@ fn adjust_output_formatting( } // Change output mode if requested - match cli_output_format.output_mode { + match cli_output_format.mode { CliOutputMode::Plain => logging_handle .switch_to_plain() .context(app::LoggingSnafu {}) @@ -195,7 +195,7 @@ fn emit_test_messages() { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConsoleOutputFormat { - pub output_mode: ConsoleOutputMode, + pub mode: ConsoleOutputMode, pub max_verbosity: log::LevelFilter, @@ -205,7 +205,7 @@ pub struct ConsoleOutputFormat { impl Default for ConsoleOutputFormat { fn default() -> Self { Self { - output_mode: ConsoleOutputMode::default(), + mode: ConsoleOutputMode::default(), max_verbosity: log::LevelFilter::Info, no_color: false, } @@ -277,7 +277,7 @@ pub trait GlobalArguments { fn is_test(&self) -> bool; - fn verbosity_filter(&self) -> &log::LevelFilter; + fn verbosity_filter(&self) -> log::LevelFilter; fn console_output_mode(&self) -> ConsoleOutputMode { if self.is_json() { @@ -292,21 +292,12 @@ pub trait GlobalArguments { } fn max_output_verbosity(&self) -> log::LevelFilter { - let verbosity_flag_filter = cli_output_format.requested_verbosity; - if matches!( - cli_output_format.output_mode, - CliOutputMode::Plain | CliOutputMode::Json - ) { - return Some(LevelFilter::Info); - } else if verbosity_flag_filter < clap_verbosity_flag::LevelFilter::Debug - && cli_global_arguments.is_debug() - { - return Some(LevelFilter::Debug); + if self.is_plain() || self.is_json() { + log::LevelFilter::Info + } else if self.is_debug() && log::LevelFilter::Debug > self.verbosity_filter() { + log::LevelFilter::Debug } else { - return verbosity_flag_filter - .as_str() - .parse() - .ok(); + self.verbosity_filter() } } } @@ -424,7 +415,7 @@ pub mod cli_template { self.no_color_flag } - fn verbosity_filter(&self) -> &log::LevelFilter { + fn verbosity_filter(&self) -> log::LevelFilter { self.verbosity .log_level_filter() .and_then(|log_level_filter| { From 298cba284ef88b26be66c8d7168a7268246c06c2 Mon Sep 17 00:00:00 2001 From: pvshvp-oss Date: Wed, 8 May 2024 12:06:24 -0500 Subject: [PATCH 8/8] WIP --- Cargo.toml | 3 ++- paxy/Cargo.toml | 3 ++- paxy/src/app/config.rs | 32 ++++++++++++++++++++------------ 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0688588..bf04a50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ paxy = { path = "paxy" } tracing = "0.1" tracing-appender = "0.2" tracing-subscriber = "0.3" +log = "0.4" # Configuration figment = "0.10" @@ -57,7 +58,7 @@ serde-aux = "4.5" serde_yaml = "0.9" tracing-serde = "0.1" speedy = "0.8" -log = "0.4" +itertools = "0.12" # Internationalization fluent = "0.16" diff --git a/paxy/Cargo.toml b/paxy/Cargo.toml index 949f1fe..479ba7b 100644 --- a/paxy/Cargo.toml +++ b/paxy/Cargo.toml @@ -20,6 +20,7 @@ edition.workspace = true tracing = { workspace = true } tracing-appender = { workspace = true } tracing-subscriber = { workspace = true } +log = { workspace = true, features = ["serde"] } # Configuration figment = { workspace = true, features = ["toml", "json", "yaml", "env"] } @@ -37,7 +38,7 @@ serde-aux = { workspace = true } serde_yaml = { workspace = true } tracing-serde = { workspace = true } speedy = { workspace = true } -log = { workspace = true, features = ["serde"] } +itertools = { workspace = true } # Internationalization fluent = { workspace = true } diff --git a/paxy/src/app/config.rs b/paxy/src/app/config.rs index 052a72c..ff512e3 100644 --- a/paxy/src/app/config.rs +++ b/paxy/src/app/config.rs @@ -8,16 +8,16 @@ where G: ui::GlobalArguments, ::L: clap_verbosity_flag::LogLevel, { - let mut candidate_config_filepath_stubs: Vec = Vec::new(); + let mut candidate_config_filepaths: Vec = Vec::new(); // Global directories #[cfg(target_family = "unix")] - candidate_config_filepath_stubs.extend(["/etc/xdg".into(), "/etc".into()]); + candidate_config_filepaths.extend(["/etc/xdg".into(), "/etc".into()]); #[cfg(target_os = "windows")] candidate_config_filepath_stubs.extend([""]); // Local directories - candidate_config_filepath_stubs.push( + candidate_config_filepaths.push( directories::BaseDirs::new() .context(RetreiveConfigUserAppBaseDirectoriesSnafu {})? .config_dir() @@ -25,22 +25,22 @@ where ); // Append filename to create filepath stubs - candidate_config_filepath_stubs + candidate_config_filepaths .iter_mut() .for_each(|f| f.push(*app::APP_NAME)); + candidate_config_filepaths = candidate_config_filepaths + .iter() + .cartesian_product(["toml", "TOML", "json", "JSON", "yaml", "YAML", "yml", "YML"]); + // Initialize configuration with app-wide defaults - let mut figment = Figment::from(Config::default()); + let mut config = Config::new(); // Merge configuration values from global and local filepaths - figment = candidate_config_filepath_stubs - .iter() - .fold(figment, move |figment, candidate_config_filepath_stub| { - admerge_from_stub(candidate_config_filepath_stub, figment) - }); + config = config.with_overriding_files(candidate_config_filepaths); // Merge configuration values from environment variables - figment = figment.admerge(Env::prefixed(&format!("{}_", *app::APP_NAME))); + config = config.with_overriding_env(&format!("{}_", *app::APP_NAME)); // Merge configuration values from additional config filepaths (usually // specified through CLI) @@ -145,6 +145,13 @@ impl Config { self } + pub fn with_overriding_filepath_stub>(&mut self, filepath_stub: P) -> &mut Self { + let filepath_stub: PathBuf = filepath_stub.into(); + + + self + } + pub fn with_overriding_files(&mut self, filepaths: I) -> &mut Self where P: AsRef, @@ -155,7 +162,7 @@ impl Config { self } - pub fn with_overriding_env>(prefix: S) -> &mut Self { + pub fn with_overriding_env>(&mut self, prefix: S) -> &mut Self { let prefix = prefix.as_ref(); self.figment = self .figment @@ -233,6 +240,7 @@ use figment::{ Profile, Provider, }; +use itertools::Itertools; use log::LevelFilter; use serde::{Deserialize, Serialize}; use snafu::{OptionExt, ResultExt, Snafu};