1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
96#![warn(clippy::print_stderr)]
97#![warn(clippy::print_stdout)]
98
99#[doc = include_str!("../README.md")]
100#[cfg(all(doctest, feature = "log", feature = "tracing"))]
101pub struct ReadmeDoctests;
102
103use std::fmt;
104
105#[cfg(feature = "log")]
106pub mod log;
107#[cfg(feature = "tracing")]
108pub mod tracing;
109
110#[derive(clap::Args, Debug, Clone, Copy, Default, PartialEq, Eq)]
112#[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)]
126pub struct Verbosity<L: LogLevel = ErrorLevel> {
127 #[arg(
128 long,
129 short = 'v',
130 action = clap::ArgAction::Count,
131 global = true,
132 help = L::verbose_help(),
133 long_help = L::verbose_long_help(),
134 )]
135 verbose: u8,
136
137 #[arg(
138 long,
139 short = 'q',
140 action = clap::ArgAction::Count,
141 global = true,
142 help = L::quiet_help(),
143 long_help = L::quiet_long_help(),
144 conflicts_with = "verbose",
145 )]
146 quiet: u8,
147
148 #[arg(skip)]
149 phantom: std::marker::PhantomData<L>,
150}
151
152impl<L: LogLevel> Verbosity<L> {
153 pub fn new(verbose: u8, quiet: u8) -> Self {
155 Verbosity {
156 verbose,
157 quiet,
158 phantom: std::marker::PhantomData,
159 }
160 }
161
162 pub fn is_present(&self) -> bool {
165 self.verbose != 0 || self.quiet != 0
166 }
167
168 pub fn is_silent(&self) -> bool {
170 self.filter() == VerbosityFilter::Off
171 }
172
173 pub fn filter(&self) -> VerbosityFilter {
175 let offset = self.verbose as i16 - self.quiet as i16;
176 L::default_filter().with_offset(offset)
177 }
178}
179
180#[cfg(feature = "log")]
181impl<L: LogLevel> Verbosity<L> {
182 pub fn log_level(&self) -> Option<log::Level> {
186 self.filter().into()
187 }
188
189 pub fn log_level_filter(&self) -> log::LevelFilter {
191 self.filter().into()
192 }
193}
194
195#[cfg(feature = "tracing")]
196impl<L: LogLevel> Verbosity<L> {
197 pub fn tracing_level(&self) -> Option<tracing_core::Level> {
201 self.filter().into()
202 }
203
204 pub fn tracing_level_filter(&self) -> tracing_core::LevelFilter {
206 self.filter().into()
207 }
208}
209
210impl<L: LogLevel> fmt::Display for Verbosity<L> {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212 self.filter().fmt(f)
213 }
214}
215
216impl<L: LogLevel> From<Verbosity<L>> for VerbosityFilter {
217 fn from(verbosity: Verbosity<L>) -> Self {
218 verbosity.filter()
219 }
220}
221
222impl<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
231pub trait LogLevel {
233 fn default_filter() -> VerbosityFilter;
235
236 fn verbose_help() -> Option<&'static str> {
238 Some("Increase logging verbosity")
239 }
240
241 fn verbose_long_help() -> Option<&'static str> {
243 None
244 }
245
246 fn quiet_help() -> Option<&'static str> {
248 Some("Decrease logging verbosity")
249 }
250
251 fn quiet_long_help() -> Option<&'static str> {
253 None
254 }
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
261#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
262#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
263pub enum VerbosityFilter {
264 Off,
265 Error,
266 Warn,
267 Info,
268 Debug,
269 Trace,
270}
271
272impl VerbosityFilter {
273 fn with_offset(&self, offset: i16) -> VerbosityFilter {
277 match i16::from(self.value()).saturating_add(offset) {
278 i16::MIN..=0 => Self::Off,
279 1 => Self::Error,
280 2 => Self::Warn,
281 3 => Self::Info,
282 4 => Self::Debug,
283 5..=i16::MAX => Self::Trace,
284 }
285 }
286
287 fn value(&self) -> u8 {
291 match self {
292 Self::Off => 0,
293 Self::Error => 1,
294 Self::Warn => 2,
295 Self::Info => 3,
296 Self::Debug => 4,
297 Self::Trace => 5,
298 }
299 }
300}
301
302impl fmt::Display for VerbosityFilter {
303 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304 match self {
305 Self::Off => write!(f, "off"),
306 Self::Error => write!(f, "error"),
307 Self::Warn => write!(f, "warn"),
308 Self::Info => write!(f, "info"),
309 Self::Debug => write!(f, "debug"),
310 Self::Trace => write!(f, "trace"),
311 }
312 }
313}
314
315#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
317pub struct ErrorLevel;
318
319impl LogLevel for ErrorLevel {
320 fn default_filter() -> VerbosityFilter {
321 VerbosityFilter::Error
322 }
323}
324
325#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
327pub struct WarnLevel;
328
329impl LogLevel for WarnLevel {
330 fn default_filter() -> VerbosityFilter {
331 VerbosityFilter::Warn
332 }
333}
334
335#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
337pub struct InfoLevel;
338
339impl LogLevel for InfoLevel {
340 fn default_filter() -> VerbosityFilter {
341 VerbosityFilter::Info
342 }
343}
344
345#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
347pub struct DebugLevel;
348
349impl LogLevel for DebugLevel {
350 fn default_filter() -> VerbosityFilter {
351 VerbosityFilter::Debug
352 }
353}
354
355#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
357pub struct TraceLevel;
358
359impl LogLevel for TraceLevel {
360 fn default_filter() -> VerbosityFilter {
361 VerbosityFilter::Trace
362 }
363}
364
365#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
367pub struct OffLevel;
368
369impl LogLevel for OffLevel {
370 fn default_filter() -> VerbosityFilter {
371 VerbosityFilter::Off
372 }
373}
374
375#[cfg(test)]
376mod test {
377 use super::*;
378
379 #[test]
380 fn verify_app() {
381 #[derive(Debug, clap::Parser)]
382 struct Cli {
383 #[command(flatten)]
384 verbose: Verbosity,
385 }
386
387 use clap::CommandFactory;
388 Cli::command().debug_assert();
389 }
390
391 #[track_caller]
393 fn assert_filter<L: LogLevel>(verbose: u8, quiet: u8, expected: VerbosityFilter) {
394 assert_eq!(
395 Verbosity::<L>::new(verbose, quiet).filter(),
396 expected,
397 "verbose = {verbose}, quiet = {quiet}"
398 );
399 }
400
401 #[test]
402 fn verbosity_off_level() {
403 let tests = [
404 (0, 0, VerbosityFilter::Off),
405 (1, 0, VerbosityFilter::Error),
406 (2, 0, VerbosityFilter::Warn),
407 (3, 0, VerbosityFilter::Info),
408 (4, 0, VerbosityFilter::Debug),
409 (5, 0, VerbosityFilter::Trace),
410 (6, 0, VerbosityFilter::Trace),
411 (255, 0, VerbosityFilter::Trace),
412 (0, 1, VerbosityFilter::Off),
413 (0, 255, VerbosityFilter::Off),
414 (255, 255, VerbosityFilter::Off),
415 ];
416
417 for (verbose, quiet, expected_filter) in tests {
418 assert_filter::<OffLevel>(verbose, quiet, expected_filter);
419 }
420 }
421
422 #[test]
423 fn verbosity_error_level() {
424 let tests = [
425 (0, 0, VerbosityFilter::Error),
426 (1, 0, VerbosityFilter::Warn),
427 (2, 0, VerbosityFilter::Info),
428 (3, 0, VerbosityFilter::Debug),
429 (4, 0, VerbosityFilter::Trace),
430 (5, 0, VerbosityFilter::Trace),
431 (255, 0, VerbosityFilter::Trace),
432 (0, 1, VerbosityFilter::Off),
433 (0, 2, VerbosityFilter::Off),
434 (0, 255, VerbosityFilter::Off),
435 (255, 255, VerbosityFilter::Error),
436 ];
437
438 for (verbose, quiet, expected_filter) in tests {
439 assert_filter::<ErrorLevel>(verbose, quiet, expected_filter);
440 }
441 }
442
443 #[test]
444 fn verbosity_warn_level() {
445 let tests = [
446 (0, 0, VerbosityFilter::Warn),
448 (1, 0, VerbosityFilter::Info),
449 (2, 0, VerbosityFilter::Debug),
450 (3, 0, VerbosityFilter::Trace),
451 (4, 0, VerbosityFilter::Trace),
452 (255, 0, VerbosityFilter::Trace),
453 (0, 1, VerbosityFilter::Error),
454 (0, 2, VerbosityFilter::Off),
455 (0, 3, VerbosityFilter::Off),
456 (0, 255, VerbosityFilter::Off),
457 (255, 255, VerbosityFilter::Warn),
458 ];
459
460 for (verbose, quiet, expected_filter) in tests {
461 assert_filter::<WarnLevel>(verbose, quiet, expected_filter);
462 }
463 }
464
465 #[test]
466 fn verbosity_info_level() {
467 let tests = [
468 (0, 0, VerbosityFilter::Info),
470 (1, 0, VerbosityFilter::Debug),
471 (2, 0, VerbosityFilter::Trace),
472 (3, 0, VerbosityFilter::Trace),
473 (255, 0, VerbosityFilter::Trace),
474 (0, 1, VerbosityFilter::Warn),
475 (0, 2, VerbosityFilter::Error),
476 (0, 3, VerbosityFilter::Off),
477 (0, 4, VerbosityFilter::Off),
478 (0, 255, VerbosityFilter::Off),
479 (255, 255, VerbosityFilter::Info),
480 ];
481
482 for (verbose, quiet, expected_filter) in tests {
483 assert_filter::<InfoLevel>(verbose, quiet, expected_filter);
484 }
485 }
486
487 #[test]
488 fn verbosity_debug_level() {
489 let tests = [
490 (0, 0, VerbosityFilter::Debug),
492 (1, 0, VerbosityFilter::Trace),
493 (2, 0, VerbosityFilter::Trace),
494 (255, 0, VerbosityFilter::Trace),
495 (0, 1, VerbosityFilter::Info),
496 (0, 2, VerbosityFilter::Warn),
497 (0, 3, VerbosityFilter::Error),
498 (0, 4, VerbosityFilter::Off),
499 (0, 5, VerbosityFilter::Off),
500 (0, 255, VerbosityFilter::Off),
501 (255, 255, VerbosityFilter::Debug),
502 ];
503
504 for (verbose, quiet, expected_filter) in tests {
505 assert_filter::<DebugLevel>(verbose, quiet, expected_filter);
506 }
507 }
508
509 #[test]
510 fn verbosity_trace_level() {
511 let tests = [
512 (0, 0, VerbosityFilter::Trace),
514 (1, 0, VerbosityFilter::Trace),
515 (255, 0, VerbosityFilter::Trace),
516 (0, 1, VerbosityFilter::Debug),
517 (0, 2, VerbosityFilter::Info),
518 (0, 3, VerbosityFilter::Warn),
519 (0, 4, VerbosityFilter::Error),
520 (0, 5, VerbosityFilter::Off),
521 (0, 6, VerbosityFilter::Off),
522 (0, 255, VerbosityFilter::Off),
523 (255, 255, VerbosityFilter::Trace),
524 ];
525
526 for (verbose, quiet, expected_filter) in tests {
527 assert_filter::<TraceLevel>(verbose, quiet, expected_filter);
528 }
529 }
530
531 #[test]
532 fn from_verbosity_filter() {
533 for &filter in &[
534 VerbosityFilter::Off,
535 VerbosityFilter::Error,
536 VerbosityFilter::Warn,
537 VerbosityFilter::Info,
538 VerbosityFilter::Debug,
539 VerbosityFilter::Trace,
540 ] {
541 assert_eq!(Verbosity::<OffLevel>::from(filter).filter(), filter);
542 assert_eq!(Verbosity::<ErrorLevel>::from(filter).filter(), filter);
543 assert_eq!(Verbosity::<WarnLevel>::from(filter).filter(), filter);
544 assert_eq!(Verbosity::<InfoLevel>::from(filter).filter(), filter);
545 assert_eq!(Verbosity::<DebugLevel>::from(filter).filter(), filter);
546 assert_eq!(Verbosity::<TraceLevel>::from(filter).filter(), filter);
547 }
548 }
549}
550
551#[cfg(feature = "serde")]
552#[cfg(test)]
553mod serde_tests {
554 use super::*;
555
556 use clap::Parser;
557 use serde::{Deserialize, Serialize};
558
559 #[derive(Debug, Parser, Serialize, Deserialize)]
560 struct Cli {
561 meaning_of_life: u8,
562 #[command(flatten)]
563 verbosity: Verbosity<InfoLevel>,
564 }
565
566 #[test]
567 fn serialize_toml() {
568 let cli = Cli {
569 meaning_of_life: 42,
570 verbosity: Verbosity::new(2, 1),
571 };
572 let toml = toml::to_string(&cli).unwrap();
573 assert_eq!(toml, "meaning_of_life = 42\nverbosity = \"debug\"\n");
574 }
575
576 #[test]
577 fn deserialize_toml() {
578 let toml = "meaning_of_life = 42\nverbosity = \"debug\"\n";
579 let cli: Cli = toml::from_str(toml).unwrap();
580 assert_eq!(cli.meaning_of_life, 42);
581 assert_eq!(cli.verbosity.filter(), VerbosityFilter::Debug);
582 }
583
584 #[test]
586 fn serde_round_trips() {
587 use serde_test::{assert_tokens, Token};
588
589 for (filter, variant) in [
590 (VerbosityFilter::Off, "off"),
591 (VerbosityFilter::Error, "error"),
592 (VerbosityFilter::Warn, "warn"),
593 (VerbosityFilter::Info, "info"),
594 (VerbosityFilter::Debug, "debug"),
595 (VerbosityFilter::Trace, "trace"),
596 ] {
597 let tokens = &[Token::UnitVariant {
598 name: "VerbosityFilter",
599 variant,
600 }];
601
602 assert_tokens(&Verbosity::<OffLevel>::from(filter), tokens);
604 assert_tokens(&Verbosity::<ErrorLevel>::from(filter), tokens);
605 assert_tokens(&Verbosity::<WarnLevel>::from(filter), tokens);
606 assert_tokens(&Verbosity::<InfoLevel>::from(filter), tokens);
607 assert_tokens(&Verbosity::<DebugLevel>::from(filter), tokens);
608 assert_tokens(&Verbosity::<TraceLevel>::from(filter), tokens);
609 }
610 }
611}