Skip to content

Commit f5da5c6

Browse files
joshkaepage
authored andcommitted
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: clap-rs#88
1 parent 9f72d6c commit f5da5c6

File tree

3 files changed

+242
-16
lines changed

3 files changed

+242
-16
lines changed

Cargo.lock

+106
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,19 @@ codecov = { repository = "clap-rs/clap-verbosity-flag" }
117117
default = ["log"]
118118
log = ["dep:log"]
119119
tracing = ["dep:tracing-core"]
120+
serde = ["dep:serde"]
120121

121122
[dependencies]
122123
clap = { version = "4.0.0", default-features = false, features = ["std", "derive"] }
123124
log = { version = "0.4.1", optional = true }
125+
serde = { version = "1", features = ["derive"], optional = true }
124126
tracing-core = { version = "0.1", optional = true }
125127

126128
[dev-dependencies]
127129
clap = { version = "4.5.4", default-features = false, features = ["help", "usage"] }
128130
env_logger = "0.11.3"
131+
serde_test = { version = "1.0.177" }
132+
toml = { version = "0.8.19" }
129133
tracing = "0.1"
130134
tracing-subscriber = "0.3"
131135

src/lib.rs

+132-16
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,21 @@ pub mod log;
7070
pub mod tracing;
7171

7272
/// Logging flags to `#[command(flatten)]` into your CLI
73-
#[derive(clap::Args, Debug, Clone, Default)]
73+
#[derive(clap::Args, Debug, Clone, Default, PartialEq, Eq)]
7474
#[command(about = None, long_about = None)]
75+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
76+
#[cfg_attr(
77+
feature = "serde",
78+
serde(
79+
from = "VerbosityFilter",
80+
into = "VerbosityFilter",
81+
bound(serialize = "L: Clone")
82+
)
83+
)]
84+
#[cfg_attr(
85+
feature = "serde",
86+
doc = r#"This type serializes to a string representation of the log level, e.g. `"Debug"`"#
87+
)]
7588
pub struct Verbosity<L: LogLevel = ErrorLevel> {
7689
#[arg(
7790
long,
@@ -162,6 +175,21 @@ impl<L: LogLevel> fmt::Display for Verbosity<L> {
162175
}
163176
}
164177

178+
impl<L: LogLevel> From<Verbosity<L>> for VerbosityFilter {
179+
fn from(verbosity: Verbosity<L>) -> Self {
180+
verbosity.filter()
181+
}
182+
}
183+
184+
impl<L: LogLevel> From<VerbosityFilter> for Verbosity<L> {
185+
fn from(filter: VerbosityFilter) -> Self {
186+
let default = L::default_filter();
187+
let verbose = filter.value().saturating_sub(default.value());
188+
let quiet = default.value().saturating_sub(filter.value());
189+
Verbosity::new(verbose, quiet)
190+
}
191+
}
192+
165193
/// Customize the default log-level and associated help
166194
pub trait LogLevel {
167195
/// Baseline level before applying `--verbose` and `--quiet`
@@ -192,6 +220,7 @@ pub trait LogLevel {
192220
///
193221
/// Used to calculate the log level and filter.
194222
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
195224
pub enum VerbosityFilter {
196225
Off,
197226
Error,
@@ -206,15 +235,7 @@ impl VerbosityFilter {
206235
///
207236
/// Negative values will decrease the verbosity, while positive values will increase it.
208237
fn with_offset(&self, offset: i16) -> VerbosityFilter {
209-
let value = match self {
210-
Self::Off => 0_i16,
211-
Self::Error => 1,
212-
Self::Warn => 2,
213-
Self::Info => 3,
214-
Self::Debug => 4,
215-
Self::Trace => 5,
216-
};
217-
match value.saturating_add(offset) {
238+
match i16::from(self.value()).saturating_add(offset) {
218239
i16::MIN..=0 => Self::Off,
219240
1 => Self::Error,
220241
2 => Self::Warn,
@@ -223,6 +244,20 @@ impl VerbosityFilter {
223244
5..=i16::MAX => Self::Trace,
224245
}
225246
}
247+
248+
/// Get the numeric value of the filter.
249+
///
250+
/// This is an internal representation of the filter level used only for conversion / offset.
251+
fn value(&self) -> u8 {
252+
match self {
253+
Self::Off => 0,
254+
Self::Error => 1,
255+
Self::Warn => 2,
256+
Self::Info => 3,
257+
Self::Debug => 4,
258+
Self::Trace => 5,
259+
}
260+
}
226261
}
227262

228263
impl fmt::Display for VerbosityFilter {
@@ -239,7 +274,7 @@ impl fmt::Display for VerbosityFilter {
239274
}
240275

241276
/// Default to [`VerbosityFilter::Error`]
242-
#[derive(Copy, Clone, Debug, Default)]
277+
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
243278
pub struct ErrorLevel;
244279

245280
impl LogLevel for ErrorLevel {
@@ -249,7 +284,7 @@ impl LogLevel for ErrorLevel {
249284
}
250285

251286
/// Default to [`VerbosityFilter::Warn`]
252-
#[derive(Copy, Clone, Debug, Default)]
287+
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
253288
pub struct WarnLevel;
254289

255290
impl LogLevel for WarnLevel {
@@ -259,7 +294,7 @@ impl LogLevel for WarnLevel {
259294
}
260295

261296
/// Default to [`VerbosityFilter::Info`]
262-
#[derive(Copy, Clone, Debug, Default)]
297+
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
263298
pub struct InfoLevel;
264299

265300
impl LogLevel for InfoLevel {
@@ -269,7 +304,7 @@ impl LogLevel for InfoLevel {
269304
}
270305

271306
/// Default to [`VerbosityFilter::Debug`]
272-
#[derive(Copy, Clone, Debug, Default)]
307+
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
273308
pub struct DebugLevel;
274309

275310
impl LogLevel for DebugLevel {
@@ -279,7 +314,7 @@ impl LogLevel for DebugLevel {
279314
}
280315

281316
/// Default to [`VerbosityFilter::Trace`]
282-
#[derive(Copy, Clone, Debug, Default)]
317+
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
283318
pub struct TraceLevel;
284319

285320
impl LogLevel for TraceLevel {
@@ -289,7 +324,7 @@ impl LogLevel for TraceLevel {
289324
}
290325

291326
/// Default to [`VerbosityFilter::Off`] (no logging)
292-
#[derive(Copy, Clone, Debug, Default)]
327+
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
293328
pub struct OffLevel;
294329

295330
impl LogLevel for OffLevel {
@@ -453,4 +488,85 @@ mod test {
453488
assert_filter::<TraceLevel>(verbose, quiet, expected_filter);
454489
}
455490
}
491+
492+
#[test]
493+
fn from_verbosity_filter() {
494+
for &filter in &[
495+
VerbosityFilter::Off,
496+
VerbosityFilter::Error,
497+
VerbosityFilter::Warn,
498+
VerbosityFilter::Info,
499+
VerbosityFilter::Debug,
500+
VerbosityFilter::Trace,
501+
] {
502+
assert_eq!(Verbosity::<OffLevel>::from(filter).filter(), filter);
503+
assert_eq!(Verbosity::<ErrorLevel>::from(filter).filter(), filter);
504+
assert_eq!(Verbosity::<WarnLevel>::from(filter).filter(), filter);
505+
assert_eq!(Verbosity::<InfoLevel>::from(filter).filter(), filter);
506+
assert_eq!(Verbosity::<DebugLevel>::from(filter).filter(), filter);
507+
assert_eq!(Verbosity::<TraceLevel>::from(filter).filter(), filter);
508+
}
509+
}
510+
}
511+
512+
#[cfg(feature = "serde")]
513+
#[cfg(test)]
514+
mod serde_tests {
515+
use super::*;
516+
517+
use clap::Parser;
518+
use serde::{Deserialize, Serialize};
519+
520+
#[derive(Debug, Parser, Serialize, Deserialize)]
521+
struct Cli {
522+
meaning_of_life: u8,
523+
#[command(flatten)]
524+
verbosity: Verbosity<InfoLevel>,
525+
}
526+
527+
#[test]
528+
fn serialize_toml() {
529+
let cli = Cli {
530+
meaning_of_life: 42,
531+
verbosity: Verbosity::new(2, 1),
532+
};
533+
let toml = toml::to_string(&cli).unwrap();
534+
assert_eq!(toml, "meaning_of_life = 42\nverbosity = \"Debug\"\n");
535+
}
536+
537+
#[test]
538+
fn deserialize_toml() {
539+
let toml = "meaning_of_life = 42\nverbosity = \"Debug\"\n";
540+
let cli: Cli = toml::from_str(toml).unwrap();
541+
assert_eq!(cli.meaning_of_life, 42);
542+
assert_eq!(cli.verbosity.filter(), VerbosityFilter::Debug);
543+
}
544+
545+
/// Tests that the `Verbosity` can be serialized and deserialized correctly from an a token.
546+
#[test]
547+
fn serde_round_trips() {
548+
use serde_test::{assert_tokens, Token};
549+
550+
for (filter, variant) in [
551+
(VerbosityFilter::Off, "Off"),
552+
(VerbosityFilter::Error, "Error"),
553+
(VerbosityFilter::Warn, "Warn"),
554+
(VerbosityFilter::Info, "Info"),
555+
(VerbosityFilter::Debug, "Debug"),
556+
(VerbosityFilter::Trace, "Trace"),
557+
] {
558+
let tokens = &[Token::UnitVariant {
559+
name: "VerbosityFilter",
560+
variant,
561+
}];
562+
563+
// `assert_tokens` checks both serialization and deserialization.
564+
assert_tokens(&Verbosity::<OffLevel>::from(filter), tokens);
565+
assert_tokens(&Verbosity::<ErrorLevel>::from(filter), tokens);
566+
assert_tokens(&Verbosity::<WarnLevel>::from(filter), tokens);
567+
assert_tokens(&Verbosity::<InfoLevel>::from(filter), tokens);
568+
assert_tokens(&Verbosity::<DebugLevel>::from(filter), tokens);
569+
assert_tokens(&Verbosity::<TraceLevel>::from(filter), tokens);
570+
}
571+
}
456572
}

0 commit comments

Comments
 (0)