Skip to content

Commit 162d6ca

Browse files
committed
feat: Implement Serialization and Deserialization
Verbosity is serialized and deserialized using the title case of the VerbosityFilter (e.g. "Debug") The `serde` dependency is gated behind an optional feature flag. Added conversion methods between Verbosity and VerbosityFilter to simplify the implementation and derived PartialEq, Eq impls on types where this was necesary for testing. Fixes: #88
1 parent 5e1afbe commit 162d6ca

File tree

3 files changed

+242
-16
lines changed

3 files changed

+242
-16
lines changed

Cargo.lock

Lines changed: 106 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,15 +127,19 @@ codecov = { repository = "clap-rs/clap-verbosity-flag" }
127127
default = ["log"]
128128
log = ["dep:log"]
129129
tracing = ["dep:tracing-core"]
130+
serde = ["dep:serde"]
130131

131132
[dependencies]
132133
clap = { version = "4.0.0", default-features = false, features = ["std", "derive"] }
133134
log = { version = "0.4.1", optional = true }
135+
serde = { version = "1", features = ["derive"], optional = true }
134136
tracing-core = { version = "0.1", optional = true }
135137

136138
[dev-dependencies]
137139
clap = { version = "4.5.4", default-features = false, features = ["help", "usage"] }
138140
env_logger = "0.11.3"
141+
serde_test = { version = "1.0.177" }
142+
toml = { version = "0.8.19" }
139143
tracing = "0.1"
140144
tracing-subscriber = "0.3"
141145

src/lib.rs

Lines changed: 132 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,21 @@ pub mod log;
108108
pub mod tracing;
109109

110110
/// Logging flags to `#[command(flatten)]` into your CLI
111-
#[derive(clap::Args, Debug, Clone, Copy, Default)]
111+
#[derive(clap::Args, Debug, Clone, Copy, Default, PartialEq, Eq)]
112112
#[command(about = None, long_about = None)]
113+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
114+
#[cfg_attr(
115+
feature = "serde",
116+
serde(
117+
from = "VerbosityFilter",
118+
into = "VerbosityFilter",
119+
bound(serialize = "L: Clone")
120+
)
121+
)]
122+
#[cfg_attr(
123+
feature = "serde",
124+
doc = r#"This type serializes to a string representation of the log level, e.g. `"Debug"`"#
125+
)]
113126
pub struct Verbosity<L: LogLevel = ErrorLevel> {
114127
#[arg(
115128
long,
@@ -200,6 +213,21 @@ impl<L: LogLevel> fmt::Display for Verbosity<L> {
200213
}
201214
}
202215

216+
impl<L: LogLevel> From<Verbosity<L>> for VerbosityFilter {
217+
fn from(verbosity: Verbosity<L>) -> Self {
218+
verbosity.filter()
219+
}
220+
}
221+
222+
impl<L: LogLevel> From<VerbosityFilter> for Verbosity<L> {
223+
fn from(filter: VerbosityFilter) -> Self {
224+
let default = L::default_filter();
225+
let verbose = filter.value().saturating_sub(default.value());
226+
let quiet = default.value().saturating_sub(filter.value());
227+
Verbosity::new(verbose, quiet)
228+
}
229+
}
230+
203231
/// Customize the default log-level and associated help
204232
pub trait LogLevel {
205233
/// Baseline level before applying `--verbose` and `--quiet`
@@ -230,6 +258,7 @@ pub trait LogLevel {
230258
///
231259
/// Used to calculate the log level and filter.
232260
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
261+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
233262
pub enum VerbosityFilter {
234263
Off,
235264
Error,
@@ -244,15 +273,7 @@ impl VerbosityFilter {
244273
///
245274
/// Negative values will decrease the verbosity, while positive values will increase it.
246275
fn with_offset(&self, offset: i16) -> VerbosityFilter {
247-
let value = match self {
248-
Self::Off => 0_i16,
249-
Self::Error => 1,
250-
Self::Warn => 2,
251-
Self::Info => 3,
252-
Self::Debug => 4,
253-
Self::Trace => 5,
254-
};
255-
match value.saturating_add(offset) {
276+
match i16::from(self.value()).saturating_add(offset) {
256277
i16::MIN..=0 => Self::Off,
257278
1 => Self::Error,
258279
2 => Self::Warn,
@@ -261,6 +282,20 @@ impl VerbosityFilter {
261282
5..=i16::MAX => Self::Trace,
262283
}
263284
}
285+
286+
/// Get the numeric value of the filter.
287+
///
288+
/// This is an internal representation of the filter level used only for conversion / offset.
289+
fn value(&self) -> u8 {
290+
match self {
291+
Self::Off => 0,
292+
Self::Error => 1,
293+
Self::Warn => 2,
294+
Self::Info => 3,
295+
Self::Debug => 4,
296+
Self::Trace => 5,
297+
}
298+
}
264299
}
265300

266301
impl fmt::Display for VerbosityFilter {
@@ -277,7 +312,7 @@ impl fmt::Display for VerbosityFilter {
277312
}
278313

279314
/// Default to [`VerbosityFilter::Error`]
280-
#[derive(Copy, Clone, Debug, Default)]
315+
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
281316
pub struct ErrorLevel;
282317

283318
impl LogLevel for ErrorLevel {
@@ -287,7 +322,7 @@ impl LogLevel for ErrorLevel {
287322
}
288323

289324
/// Default to [`VerbosityFilter::Warn`]
290-
#[derive(Copy, Clone, Debug, Default)]
325+
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
291326
pub struct WarnLevel;
292327

293328
impl LogLevel for WarnLevel {
@@ -297,7 +332,7 @@ impl LogLevel for WarnLevel {
297332
}
298333

299334
/// Default to [`VerbosityFilter::Info`]
300-
#[derive(Copy, Clone, Debug, Default)]
335+
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
301336
pub struct InfoLevel;
302337

303338
impl LogLevel for InfoLevel {
@@ -307,7 +342,7 @@ impl LogLevel for InfoLevel {
307342
}
308343

309344
/// Default to [`VerbosityFilter::Debug`]
310-
#[derive(Copy, Clone, Debug, Default)]
345+
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
311346
pub struct DebugLevel;
312347

313348
impl LogLevel for DebugLevel {
@@ -317,7 +352,7 @@ impl LogLevel for DebugLevel {
317352
}
318353

319354
/// Default to [`VerbosityFilter::Trace`]
320-
#[derive(Copy, Clone, Debug, Default)]
355+
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
321356
pub struct TraceLevel;
322357

323358
impl LogLevel for TraceLevel {
@@ -327,7 +362,7 @@ impl LogLevel for TraceLevel {
327362
}
328363

329364
/// Default to [`VerbosityFilter::Off`] (no logging)
330-
#[derive(Copy, Clone, Debug, Default)]
365+
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
331366
pub struct OffLevel;
332367

333368
impl LogLevel for OffLevel {
@@ -491,4 +526,85 @@ mod test {
491526
assert_filter::<TraceLevel>(verbose, quiet, expected_filter);
492527
}
493528
}
529+
530+
#[test]
531+
fn from_verbosity_filter() {
532+
for &filter in &[
533+
VerbosityFilter::Off,
534+
VerbosityFilter::Error,
535+
VerbosityFilter::Warn,
536+
VerbosityFilter::Info,
537+
VerbosityFilter::Debug,
538+
VerbosityFilter::Trace,
539+
] {
540+
assert_eq!(Verbosity::<OffLevel>::from(filter).filter(), filter);
541+
assert_eq!(Verbosity::<ErrorLevel>::from(filter).filter(), filter);
542+
assert_eq!(Verbosity::<WarnLevel>::from(filter).filter(), filter);
543+
assert_eq!(Verbosity::<InfoLevel>::from(filter).filter(), filter);
544+
assert_eq!(Verbosity::<DebugLevel>::from(filter).filter(), filter);
545+
assert_eq!(Verbosity::<TraceLevel>::from(filter).filter(), filter);
546+
}
547+
}
548+
}
549+
550+
#[cfg(feature = "serde")]
551+
#[cfg(test)]
552+
mod serde_tests {
553+
use super::*;
554+
555+
use clap::Parser;
556+
use serde::{Deserialize, Serialize};
557+
558+
#[derive(Debug, Parser, Serialize, Deserialize)]
559+
struct Cli {
560+
meaning_of_life: u8,
561+
#[command(flatten)]
562+
verbosity: Verbosity<InfoLevel>,
563+
}
564+
565+
#[test]
566+
fn serialize_toml() {
567+
let cli = Cli {
568+
meaning_of_life: 42,
569+
verbosity: Verbosity::new(2, 1),
570+
};
571+
let toml = toml::to_string(&cli).unwrap();
572+
assert_eq!(toml, "meaning_of_life = 42\nverbosity = \"Debug\"\n");
573+
}
574+
575+
#[test]
576+
fn deserialize_toml() {
577+
let toml = "meaning_of_life = 42\nverbosity = \"Debug\"\n";
578+
let cli: Cli = toml::from_str(toml).unwrap();
579+
assert_eq!(cli.meaning_of_life, 42);
580+
assert_eq!(cli.verbosity.filter(), VerbosityFilter::Debug);
581+
}
582+
583+
/// Tests that the `Verbosity` can be serialized and deserialized correctly from an a token.
584+
#[test]
585+
fn serde_round_trips() {
586+
use serde_test::{assert_tokens, Token};
587+
588+
for (filter, variant) in [
589+
(VerbosityFilter::Off, "Off"),
590+
(VerbosityFilter::Error, "Error"),
591+
(VerbosityFilter::Warn, "Warn"),
592+
(VerbosityFilter::Info, "Info"),
593+
(VerbosityFilter::Debug, "Debug"),
594+
(VerbosityFilter::Trace, "Trace"),
595+
] {
596+
let tokens = &[Token::UnitVariant {
597+
name: "VerbosityFilter",
598+
variant,
599+
}];
600+
601+
// `assert_tokens` checks both serialization and deserialization.
602+
assert_tokens(&Verbosity::<OffLevel>::from(filter), tokens);
603+
assert_tokens(&Verbosity::<ErrorLevel>::from(filter), tokens);
604+
assert_tokens(&Verbosity::<WarnLevel>::from(filter), tokens);
605+
assert_tokens(&Verbosity::<InfoLevel>::from(filter), tokens);
606+
assert_tokens(&Verbosity::<DebugLevel>::from(filter), tokens);
607+
assert_tokens(&Verbosity::<TraceLevel>::from(filter), tokens);
608+
}
609+
}
494610
}

0 commit comments

Comments
 (0)