1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
62#![warn(clippy::print_stderr)]
63#![warn(clippy::print_stdout)]
64
65use std::fmt;
66
67#[cfg(feature = "log")]
68pub mod log;
69#[cfg(feature = "tracing")]
70pub mod tracing;
71
72#[derive(clap::Args, Debug, Clone, Copy, Default)]
74#[command(about = None, long_about = None)]
75pub struct Verbosity<L: LogLevel = ErrorLevel> {
76 #[arg(
77 long,
78 short = 'v',
79 action = clap::ArgAction::Count,
80 global = true,
81 help = L::verbose_help(),
82 long_help = L::verbose_long_help(),
83 )]
84 verbose: u8,
85
86 #[arg(
87 long,
88 short = 'q',
89 action = clap::ArgAction::Count,
90 global = true,
91 help = L::quiet_help(),
92 long_help = L::quiet_long_help(),
93 conflicts_with = "verbose",
94 )]
95 quiet: u8,
96
97 #[arg(skip)]
98 phantom: std::marker::PhantomData<L>,
99}
100
101impl<L: LogLevel> Verbosity<L> {
102 pub fn new(verbose: u8, quiet: u8) -> Self {
104 Verbosity {
105 verbose,
106 quiet,
107 phantom: std::marker::PhantomData,
108 }
109 }
110
111 pub fn is_present(&self) -> bool {
114 self.verbose != 0 || self.quiet != 0
115 }
116
117 pub fn is_silent(&self) -> bool {
119 self.filter() == VerbosityFilter::Off
120 }
121
122 pub fn filter(&self) -> VerbosityFilter {
124 let offset = self.verbose as i16 - self.quiet as i16;
125 L::default_filter().with_offset(offset)
126 }
127}
128
129#[cfg(feature = "log")]
130impl<L: LogLevel> Verbosity<L> {
131 pub fn log_level(&self) -> Option<log::Level> {
135 self.filter().into()
136 }
137
138 pub fn log_level_filter(&self) -> log::LevelFilter {
140 self.filter().into()
141 }
142}
143
144#[cfg(feature = "tracing")]
145impl<L: LogLevel> Verbosity<L> {
146 pub fn tracing_level(&self) -> Option<tracing_core::Level> {
150 self.filter().into()
151 }
152
153 pub fn tracing_level_filter(&self) -> tracing_core::LevelFilter {
155 self.filter().into()
156 }
157}
158
159impl<L: LogLevel> fmt::Display for Verbosity<L> {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 self.filter().fmt(f)
162 }
163}
164
165pub trait LogLevel {
167 fn default_filter() -> VerbosityFilter;
169
170 fn verbose_help() -> Option<&'static str> {
172 Some("Increase logging verbosity")
173 }
174
175 fn verbose_long_help() -> Option<&'static str> {
177 None
178 }
179
180 fn quiet_help() -> Option<&'static str> {
182 Some("Decrease logging verbosity")
183 }
184
185 fn quiet_long_help() -> Option<&'static str> {
187 None
188 }
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub enum VerbosityFilter {
196 Off,
197 Error,
198 Warn,
199 Info,
200 Debug,
201 Trace,
202}
203
204impl VerbosityFilter {
205 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) {
218 i16::MIN..=0 => Self::Off,
219 1 => Self::Error,
220 2 => Self::Warn,
221 3 => Self::Info,
222 4 => Self::Debug,
223 5..=i16::MAX => Self::Trace,
224 }
225 }
226}
227
228impl fmt::Display for VerbosityFilter {
229 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230 match self {
231 Self::Off => write!(f, "off"),
232 Self::Error => write!(f, "error"),
233 Self::Warn => write!(f, "warn"),
234 Self::Info => write!(f, "info"),
235 Self::Debug => write!(f, "debug"),
236 Self::Trace => write!(f, "trace"),
237 }
238 }
239}
240
241#[derive(Copy, Clone, Debug, Default)]
243pub struct ErrorLevel;
244
245impl LogLevel for ErrorLevel {
246 fn default_filter() -> VerbosityFilter {
247 VerbosityFilter::Error
248 }
249}
250
251#[derive(Copy, Clone, Debug, Default)]
253pub struct WarnLevel;
254
255impl LogLevel for WarnLevel {
256 fn default_filter() -> VerbosityFilter {
257 VerbosityFilter::Warn
258 }
259}
260
261#[derive(Copy, Clone, Debug, Default)]
263pub struct InfoLevel;
264
265impl LogLevel for InfoLevel {
266 fn default_filter() -> VerbosityFilter {
267 VerbosityFilter::Info
268 }
269}
270
271#[derive(Copy, Clone, Debug, Default)]
273pub struct DebugLevel;
274
275impl LogLevel for DebugLevel {
276 fn default_filter() -> VerbosityFilter {
277 VerbosityFilter::Debug
278 }
279}
280
281#[derive(Copy, Clone, Debug, Default)]
283pub struct TraceLevel;
284
285impl LogLevel for TraceLevel {
286 fn default_filter() -> VerbosityFilter {
287 VerbosityFilter::Trace
288 }
289}
290
291#[derive(Copy, Clone, Debug, Default)]
293pub struct OffLevel;
294
295impl LogLevel for OffLevel {
296 fn default_filter() -> VerbosityFilter {
297 VerbosityFilter::Off
298 }
299}
300
301#[cfg(test)]
302mod test {
303 use super::*;
304
305 #[test]
306 fn verify_app() {
307 #[derive(Debug, clap::Parser)]
308 struct Cli {
309 #[command(flatten)]
310 verbose: Verbosity,
311 }
312
313 use clap::CommandFactory;
314 Cli::command().debug_assert();
315 }
316
317 #[track_caller]
319 fn assert_filter<L: LogLevel>(verbose: u8, quiet: u8, expected: VerbosityFilter) {
320 assert_eq!(
321 Verbosity::<L>::new(verbose, quiet).filter(),
322 expected,
323 "verbose = {verbose}, quiet = {quiet}"
324 );
325 }
326
327 #[test]
328 fn verbosity_off_level() {
329 let tests = [
330 (0, 0, VerbosityFilter::Off),
331 (1, 0, VerbosityFilter::Error),
332 (2, 0, VerbosityFilter::Warn),
333 (3, 0, VerbosityFilter::Info),
334 (4, 0, VerbosityFilter::Debug),
335 (5, 0, VerbosityFilter::Trace),
336 (6, 0, VerbosityFilter::Trace),
337 (255, 0, VerbosityFilter::Trace),
338 (0, 1, VerbosityFilter::Off),
339 (0, 255, VerbosityFilter::Off),
340 (255, 255, VerbosityFilter::Off),
341 ];
342
343 for (verbose, quiet, expected_filter) in tests {
344 assert_filter::<OffLevel>(verbose, quiet, expected_filter);
345 }
346 }
347
348 #[test]
349 fn verbosity_error_level() {
350 let tests = [
351 (0, 0, VerbosityFilter::Error),
352 (1, 0, VerbosityFilter::Warn),
353 (2, 0, VerbosityFilter::Info),
354 (3, 0, VerbosityFilter::Debug),
355 (4, 0, VerbosityFilter::Trace),
356 (5, 0, VerbosityFilter::Trace),
357 (255, 0, VerbosityFilter::Trace),
358 (0, 1, VerbosityFilter::Off),
359 (0, 2, VerbosityFilter::Off),
360 (0, 255, VerbosityFilter::Off),
361 (255, 255, VerbosityFilter::Error),
362 ];
363
364 for (verbose, quiet, expected_filter) in tests {
365 assert_filter::<ErrorLevel>(verbose, quiet, expected_filter);
366 }
367 }
368
369 #[test]
370 fn verbosity_warn_level() {
371 let tests = [
372 (0, 0, VerbosityFilter::Warn),
374 (1, 0, VerbosityFilter::Info),
375 (2, 0, VerbosityFilter::Debug),
376 (3, 0, VerbosityFilter::Trace),
377 (4, 0, VerbosityFilter::Trace),
378 (255, 0, VerbosityFilter::Trace),
379 (0, 1, VerbosityFilter::Error),
380 (0, 2, VerbosityFilter::Off),
381 (0, 3, VerbosityFilter::Off),
382 (0, 255, VerbosityFilter::Off),
383 (255, 255, VerbosityFilter::Warn),
384 ];
385
386 for (verbose, quiet, expected_filter) in tests {
387 assert_filter::<WarnLevel>(verbose, quiet, expected_filter);
388 }
389 }
390
391 #[test]
392 fn verbosity_info_level() {
393 let tests = [
394 (0, 0, VerbosityFilter::Info),
396 (1, 0, VerbosityFilter::Debug),
397 (2, 0, VerbosityFilter::Trace),
398 (3, 0, VerbosityFilter::Trace),
399 (255, 0, VerbosityFilter::Trace),
400 (0, 1, VerbosityFilter::Warn),
401 (0, 2, VerbosityFilter::Error),
402 (0, 3, VerbosityFilter::Off),
403 (0, 4, VerbosityFilter::Off),
404 (0, 255, VerbosityFilter::Off),
405 (255, 255, VerbosityFilter::Info),
406 ];
407
408 for (verbose, quiet, expected_filter) in tests {
409 assert_filter::<InfoLevel>(verbose, quiet, expected_filter);
410 }
411 }
412
413 #[test]
414 fn verbosity_debug_level() {
415 let tests = [
416 (0, 0, VerbosityFilter::Debug),
418 (1, 0, VerbosityFilter::Trace),
419 (2, 0, VerbosityFilter::Trace),
420 (255, 0, VerbosityFilter::Trace),
421 (0, 1, VerbosityFilter::Info),
422 (0, 2, VerbosityFilter::Warn),
423 (0, 3, VerbosityFilter::Error),
424 (0, 4, VerbosityFilter::Off),
425 (0, 5, VerbosityFilter::Off),
426 (0, 255, VerbosityFilter::Off),
427 (255, 255, VerbosityFilter::Debug),
428 ];
429
430 for (verbose, quiet, expected_filter) in tests {
431 assert_filter::<DebugLevel>(verbose, quiet, expected_filter);
432 }
433 }
434
435 #[test]
436 fn verbosity_trace_level() {
437 let tests = [
438 (0, 0, VerbosityFilter::Trace),
440 (1, 0, VerbosityFilter::Trace),
441 (255, 0, VerbosityFilter::Trace),
442 (0, 1, VerbosityFilter::Debug),
443 (0, 2, VerbosityFilter::Info),
444 (0, 3, VerbosityFilter::Warn),
445 (0, 4, VerbosityFilter::Error),
446 (0, 5, VerbosityFilter::Off),
447 (0, 6, VerbosityFilter::Off),
448 (0, 255, VerbosityFilter::Off),
449 (255, 255, VerbosityFilter::Trace),
450 ];
451
452 for (verbose, quiet, expected_filter) in tests {
453 assert_filter::<TraceLevel>(verbose, quiet, expected_filter);
454 }
455 }
456}