diff --git a/Cargo.lock b/Cargo.lock index 64a9563..32d6da3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,9 @@ dependencies = [ "clap", "env_logger", "log", + "serde", + "serde_test", + "toml", "tracing", "tracing-log", "tracing-subscriber", @@ -138,6 +141,18 @@ dependencies = [ "log", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "heck" version = "0.5.0" @@ -150,6 +165,16 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "indexmap" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -161,6 +186,9 @@ name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +dependencies = [ + "serde", +] [[package]] name = "memchr" @@ -198,18 +226,18 @@ checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "proc-macro2" -version = "1.0.73" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dd5e8a1f1029c43224ad5898e50140c2aebb1705f19e67c918ebf5b9e797fe1" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.34" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a37c9326af5ed140c86a46655b5278de879853be5573c01df185b6f49a580a" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -231,6 +259,44 @@ version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_test" +version = "1.0.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f901ee573cab6b3060453d2d5f0bae4e6d628c23c0a962ff9b5f1d7c8d4f1ed" +dependencies = [ + "serde", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -248,9 +314,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "syn" -version = "2.0.44" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d27c2c202598d05175a6dd3af46824b7f747f8d8e9b14c623f19fa5069735d" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -266,6 +332,40 @@ dependencies = [ "once_cell", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tracing" version = "0.1.40" @@ -428,3 +528,12 @@ name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml index 3d8d9f6..169ce7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,13 +112,19 @@ pre-release-replacements = [ [badges] codecov = { repository = "clap-rs/clap-verbosity-flag" } +[features] +serde = ["dep:serde", "log/serde"] + [dependencies] log = "0.4.1" clap = { version = "4.0.0", default-features = false, features = ["std", "derive"] } +serde = { version = "1.0.210", features = ["derive"], optional = true} [dev-dependencies] clap = { version = "4.5.4", default-features = false, features = ["help", "usage"] } env_logger = "0.11.3" +serde_test = { version = "1.0.177" } +toml = { version = "0.8.19" } tracing = "0.1" tracing-subscriber = "0.3" tracing-log = "0.2" diff --git a/src/lib.rs b/src/lib.rs index 72ab300..a2ce623 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,8 +63,17 @@ pub use log::Level; pub use log::LevelFilter; /// Logging flags to `#[command(flatten)]` into your CLI -#[derive(clap::Args, Debug, Clone, Default)] +#[derive(clap::Args, Debug, Clone, Default, PartialEq, Eq)] #[command(about = None, long_about = None)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "serde", + serde( + from = "LevelFilter", + into = "LevelFilter", + bound(serialize = "L: Clone") + ) +)] pub struct Verbosity { #[arg( long, @@ -144,6 +153,17 @@ fn level_value(level: Option) -> u8 { } } +fn level_filter_value(filter: LevelFilter) -> u8 { + match filter { + LevelFilter::Off => 0, + LevelFilter::Error => 1, + LevelFilter::Warn => 2, + LevelFilter::Info => 3, + LevelFilter::Debug => 4, + LevelFilter::Trace => 5, + } +} + fn level_enum(verbosity: u8) -> Option { match verbosity { 0 => None, @@ -163,6 +183,22 @@ impl fmt::Display for Verbosity { } } +impl From> for LevelFilter { + fn from(v: Verbosity) -> Self { + v.log_level_filter() + } +} + +impl From for Verbosity { + fn from(filter: LevelFilter) -> Self { + let default_verbosity = level_value(L::default()); + let verbosity = level_filter_value(filter); + let verbose = verbosity.saturating_sub(default_verbosity); + let quiet = default_verbosity.saturating_sub(verbosity); + Verbosity::new(verbose, quiet) + } +} + /// Customize the default log-level and associated help pub trait LogLevel { /// Base-line level before applying `--verbose` and `--quiet` @@ -191,7 +227,7 @@ pub trait LogLevel { /// Default to [`log::Level::Error`] #[allow(clippy::exhaustive_structs)] -#[derive(Copy, Clone, Debug, Default)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct ErrorLevel; impl LogLevel for ErrorLevel { @@ -202,7 +238,7 @@ impl LogLevel for ErrorLevel { /// Default to [`log::Level::Warn`] #[allow(clippy::exhaustive_structs)] -#[derive(Copy, Clone, Debug, Default)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct WarnLevel; impl LogLevel for WarnLevel { @@ -213,7 +249,7 @@ impl LogLevel for WarnLevel { /// Default to [`log::Level::Info`] #[allow(clippy::exhaustive_structs)] -#[derive(Copy, Clone, Debug, Default)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct InfoLevel; impl LogLevel for InfoLevel { @@ -333,4 +369,242 @@ mod test { ); } } + + #[test] + fn from_level_filter() { + // ErrorLevel + let v = Verbosity::::from(LevelFilter::Off); + assert_eq!(v, Verbosity::new(0, 1)); + + let v = Verbosity::::from(LevelFilter::Error); + assert_eq!(v, Verbosity::new(0, 0)); + + let v = Verbosity::::from(LevelFilter::Warn); + assert_eq!(v, Verbosity::new(1, 0)); + + let v = Verbosity::::from(LevelFilter::Info); + assert_eq!(v, Verbosity::new(2, 0)); + + let v = Verbosity::::from(LevelFilter::Debug); + assert_eq!(v, Verbosity::new(3, 0)); + + let v = Verbosity::::from(LevelFilter::Trace); + assert_eq!(v, Verbosity::new(4, 0)); + + // WarnLevel + let v = Verbosity::::from(LevelFilter::Off); + assert_eq!(v, Verbosity::new(0, 2)); + + let v = Verbosity::::from(LevelFilter::Error); + assert_eq!(v, Verbosity::new(0, 1)); + + let v = Verbosity::::from(LevelFilter::Warn); + assert_eq!(v, Verbosity::new(0, 0)); + + let v = Verbosity::::from(LevelFilter::Info); + assert_eq!(v, Verbosity::new(1, 0)); + + let v = Verbosity::::from(LevelFilter::Debug); + assert_eq!(v, Verbosity::new(2, 0)); + + let v = Verbosity::::from(LevelFilter::Trace); + assert_eq!(v, Verbosity::new(3, 0)); + + // InfoLevel + let v = Verbosity::::from(LevelFilter::Off); + assert_eq!(v, Verbosity::new(0, 3)); + + let v = Verbosity::::from(LevelFilter::Error); + assert_eq!(v, Verbosity::new(0, 2)); + + let v = Verbosity::::from(LevelFilter::Warn); + assert_eq!(v, Verbosity::new(0, 1)); + + let v = Verbosity::::from(LevelFilter::Info); + assert_eq!(v, Verbosity::new(0, 0)); + + let v = Verbosity::::from(LevelFilter::Debug); + assert_eq!(v, Verbosity::new(1, 0)); + + let v = Verbosity::::from(LevelFilter::Trace); + assert_eq!(v, Verbosity::new(2, 0)); + + let v = Verbosity::::from(LevelFilter::Trace); + assert_eq!(v, Verbosity::new(2, 0)); + } + + #[test] + #[cfg(feature = "serde")] + fn serde() { + use serde_test::{assert_tokens, Token}; + + assert_tokens( + &Verbosity::::new(0, 1), + &[Token::UnitVariant { + name: "LevelFilter", + variant: "OFF", + }], + ); + + assert_tokens( + &Verbosity::::new(0, 0), + &[Token::UnitVariant { + name: "LevelFilter", + variant: "ERROR", + }], + ); + + assert_tokens( + &Verbosity::::new(1, 0), + &[Token::UnitVariant { + name: "LevelFilter", + variant: "WARN", + }], + ); + + assert_tokens( + &Verbosity::::new(2, 0), + &[Token::UnitVariant { + name: "LevelFilter", + variant: "INFO", + }], + ); + + assert_tokens( + &Verbosity::::new(3, 0), + &[Token::UnitVariant { + name: "LevelFilter", + variant: "DEBUG", + }], + ); + + assert_tokens( + &Verbosity::::new(4, 0), + &[Token::UnitVariant { + name: "LevelFilter", + variant: "TRACE", + }], + ); + + assert_tokens( + &Verbosity::::new(0, 2), + &[Token::UnitVariant { + name: "LevelFilter", + variant: "OFF", + }], + ); + + assert_tokens( + &Verbosity::::new(0, 1), + &[Token::UnitVariant { + name: "LevelFilter", + variant: "ERROR", + }], + ); + + assert_tokens( + &Verbosity::::new(0, 0), + &[Token::UnitVariant { + name: "LevelFilter", + variant: "WARN", + }], + ); + + assert_tokens( + &Verbosity::::new(1, 0), + &[Token::UnitVariant { + name: "LevelFilter", + variant: "INFO", + }], + ); + + assert_tokens( + &Verbosity::::new(2, 0), + &[Token::UnitVariant { + name: "LevelFilter", + variant: "DEBUG", + }], + ); + + assert_tokens( + &Verbosity::::new(3, 0), + &[Token::UnitVariant { + name: "LevelFilter", + variant: "TRACE", + }], + ); + + assert_tokens( + &Verbosity::::new(0, 3), + &[Token::UnitVariant { + name: "LevelFilter", + variant: "OFF", + }], + ); + + assert_tokens( + &Verbosity::::new(0, 2), + &[Token::UnitVariant { + name: "LevelFilter", + variant: "ERROR", + }], + ); + + assert_tokens( + &Verbosity::::new(0, 1), + &[Token::UnitVariant { + name: "LevelFilter", + variant: "WARN", + }], + ); + + assert_tokens( + &Verbosity::::new(0, 0), + &[Token::UnitVariant { + name: "LevelFilter", + variant: "INFO", + }], + ); + + assert_tokens( + &Verbosity::::new(1, 0), + &[Token::UnitVariant { + name: "LevelFilter", + variant: "DEBUG", + }], + ); + + assert_tokens( + &Verbosity::::new(2, 0), + &[Token::UnitVariant { + name: "LevelFilter", + variant: "TRACE", + }], + ); + } + + #[test] + #[cfg(feature = "serde")] + fn serde_toml() { + use clap::Parser; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Parser, Serialize, Deserialize)] + struct Cli { + meaning_of_life: u8, + #[command(flatten)] + verbose: Verbosity, + } + + // round-trips + let toml = "meaning_of_life = 42\nverbose = \"DEBUG\"\n"; + let cli: Cli = toml::from_str(toml).unwrap(); + assert_eq!(cli.verbose.log_level_filter(), LevelFilter::Debug); + assert_eq!(toml::to_string(&cli).unwrap(), toml); + + // is case-insensitive + let toml = "meaning_of_life = 42\nverbose = \"debug\"\n"; + let cli: Cli = toml::from_str(toml).unwrap(); + assert_eq!(cli.verbose.log_level_filter(), LevelFilter::Debug); + } }