Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement tracing support with associated type on LogLevel trait #124

Closed
wants to merge 11 commits into from
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,13 @@ pre-release-replacements = [
[badges]
codecov = { repository = "clap-rs/clap-verbosity-flag" }

[features]
default = ["log"]
log = ["dep:log"]

[dependencies]
log = "0.4.1"
clap = { version = "4.0.0", default-features = false, features = ["std", "derive"] }
log = { version = "0.4.1", optional = true }

[dev-dependencies]
clap = { version = "4.5.4", default-features = false, features = ["help", "usage"] }
Expand Down
242 changes: 53 additions & 189 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,16 @@
#![warn(clippy::print_stderr)]
#![warn(clippy::print_stdout)]

pub use log::Level;
pub use log::LevelFilter;
/// These types are re-exported for backwards compatibility only.
#[cfg(any(doc, feature = "log"))]
#[doc(hidden)]
pub use self::log::{ErrorLevel, InfoLevel, WarnLevel};

#[cfg(any(doc, feature = "log"))]
pub mod log;

/// Logging flags to `#[command(flatten)]` into your CLI
#[cfg(any(doc, feature = "log"))]
#[derive(clap::Args, Debug, Clone, Default)]
#[command(about = None, long_about = None)]
pub struct Verbosity<L: LogLevel = ErrorLevel> {
Expand Down Expand Up @@ -91,7 +97,40 @@ pub struct Verbosity<L: LogLevel = ErrorLevel> {
phantom: std::marker::PhantomData<L>,
}

impl<L: LogLevel> Verbosity<L> {
/// Logging flags to `#[command(flatten)]` into your CLI
#[cfg(not(any(doc, feature = "log")))]
#[derive(clap::Args, Debug, Clone, Default)]
#[command(about = None, long_about = None)]
pub struct Verbosity<L: LogLevel> {
#[arg(
long,
short = 'v',
action = clap::ArgAction::Count,
global = true,
help = L::verbose_help(),
long_help = L::verbose_long_help(),
)]
verbose: u8,

#[arg(
long,
short = 'q',
action = clap::ArgAction::Count,
global = true,
help = L::quiet_help(),
long_help = L::quiet_long_help(),
conflicts_with = "verbose",
)]
quiet: u8,

#[arg(skip)]
phantom: std::marker::PhantomData<L>,
}

impl<L: LogLevel> Verbosity<L>
where
Filter: Into<Option<L::Level>> + Into<L::LevelFilter> + From<Option<L::Level>>,
{
/// Create a new verbosity instance by explicitly setting the values
pub fn new(verbose: u8, quiet: u8) -> Self {
Verbosity {
Expand All @@ -110,12 +149,12 @@ impl<L: LogLevel> Verbosity<L> {
/// Get the log level.
///
/// `None` means all output is disabled.
pub fn log_level(&self) -> Option<Level> {
pub fn log_level(&self) -> Option<L::Level> {
self.filter().into()
}

/// Get the log level filter.
pub fn log_level_filter(&self) -> LevelFilter {
pub fn log_level_filter(&self) -> L::LevelFilter {
self.filter().into()
}

Expand All @@ -134,7 +173,7 @@ impl<L: LogLevel> Verbosity<L> {
///
/// Used to calculate the log level and filter.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Filter {
pub enum Filter {
joshka marked this conversation as resolved.
Show resolved Hide resolved
Off,
Error,
Warn,
Expand Down Expand Up @@ -191,70 +230,24 @@ impl fmt::Display for Filter {
}
}

impl From<Filter> for LevelFilter {
fn from(filter: Filter) -> Self {
match filter {
Filter::Off => LevelFilter::Off,
Filter::Error => LevelFilter::Error,
Filter::Warn => LevelFilter::Warn,
Filter::Info => LevelFilter::Info,
Filter::Debug => LevelFilter::Debug,
Filter::Trace => LevelFilter::Trace,
}
}
}

impl From<LevelFilter> for Filter {
fn from(level: LevelFilter) -> Self {
match level {
LevelFilter::Off => Filter::Off,
LevelFilter::Error => Filter::Error,
LevelFilter::Warn => Filter::Warn,
LevelFilter::Info => Filter::Info,
LevelFilter::Debug => Filter::Debug,
LevelFilter::Trace => Filter::Trace,
}
}
}

impl From<Filter> for Option<Level> {
fn from(filter: Filter) -> Self {
match filter {
Filter::Off => None,
Filter::Error => Some(Level::Error),
Filter::Warn => Some(Level::Warn),
Filter::Info => Some(Level::Info),
Filter::Debug => Some(Level::Debug),
Filter::Trace => Some(Level::Trace),
}
}
}

impl From<Option<Level>> for Filter {
fn from(level: Option<Level>) -> Self {
match level {
None => Filter::Off,
Some(Level::Error) => Filter::Error,
Some(Level::Warn) => Filter::Warn,
Some(Level::Info) => Filter::Info,
Some(Level::Debug) => Filter::Debug,
Some(Level::Trace) => Filter::Trace,
}
}
}

use std::fmt;

impl<L: LogLevel> fmt::Display for Verbosity<L> {
impl<L: LogLevel> fmt::Display for Verbosity<L>
where
Filter: Into<Option<L::Level>> + Into<L::LevelFilter> + From<Option<L::Level>>,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.filter())
}
}

/// Customize the default log-level and associated help
pub trait LogLevel {
type Level;
type LevelFilter;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no associated types used

With Filter, we shouldn't need any of this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was either use associated types, or create crate specific methods (e.g. log_level, tracing_level). I think the specific methods are a much simpler approach.

7b8b965


/// Base-line level before applying `--verbose` and `--quiet`
fn default() -> Option<Level>;
fn default() -> Option<Self::Level>;

/// Short-help message for `--verbose`
fn verbose_help() -> Option<&'static str> {
Expand All @@ -277,39 +270,6 @@ pub trait LogLevel {
}
}

/// Default to [`log::Level::Error`]
#[allow(clippy::exhaustive_structs)]
#[derive(Copy, Clone, Debug, Default)]
pub struct ErrorLevel;

impl LogLevel for ErrorLevel {
fn default() -> Option<Level> {
Some(Level::Error)
}
}

/// Default to [`log::Level::Warn`]
#[allow(clippy::exhaustive_structs)]
#[derive(Copy, Clone, Debug, Default)]
pub struct WarnLevel;

impl LogLevel for WarnLevel {
fn default() -> Option<Level> {
Some(Level::Warn)
}
}

/// Default to [`log::Level::Info`]
#[allow(clippy::exhaustive_structs)]
#[derive(Copy, Clone, Debug, Default)]
pub struct InfoLevel;

impl LogLevel for InfoLevel {
fn default() -> Option<Level> {
Some(Level::Info)
}
}

#[cfg(test)]
mod test {
use super::*;
Expand All @@ -325,100 +285,4 @@ mod test {
use clap::CommandFactory;
Cli::command().debug_assert();
}

#[test]
fn verbosity_error_level() {
let tests = [
// verbose, quiet, expected_level, expected_filter
(0, 0, Some(Level::Error), LevelFilter::Error),
(1, 0, Some(Level::Warn), LevelFilter::Warn),
(2, 0, Some(Level::Info), LevelFilter::Info),
(3, 0, Some(Level::Debug), LevelFilter::Debug),
(4, 0, Some(Level::Trace), LevelFilter::Trace),
(5, 0, Some(Level::Trace), LevelFilter::Trace),
(255, 0, Some(Level::Trace), LevelFilter::Trace),
(0, 1, None, LevelFilter::Off),
(0, 2, None, LevelFilter::Off),
(0, 255, None, LevelFilter::Off),
(255, 255, Some(Level::Error), LevelFilter::Error),
];

for (verbose, quiet, expected_level, expected_filter) in tests.iter() {
let v = Verbosity::<ErrorLevel>::new(*verbose, *quiet);
assert_eq!(
v.log_level(),
*expected_level,
"verbose = {verbose}, quiet = {quiet}"
);
assert_eq!(
v.log_level_filter(),
*expected_filter,
"verbose = {verbose}, quiet = {quiet}"
);
}
}

#[test]
fn verbosity_warn_level() {
let tests = [
// verbose, quiet, expected_level, expected_filter
(0, 0, Some(Level::Warn), LevelFilter::Warn),
(1, 0, Some(Level::Info), LevelFilter::Info),
(2, 0, Some(Level::Debug), LevelFilter::Debug),
(3, 0, Some(Level::Trace), LevelFilter::Trace),
(4, 0, Some(Level::Trace), LevelFilter::Trace),
(255, 0, Some(Level::Trace), LevelFilter::Trace),
(0, 1, Some(Level::Error), LevelFilter::Error),
(0, 2, None, LevelFilter::Off),
(0, 3, None, LevelFilter::Off),
(0, 255, None, LevelFilter::Off),
(255, 255, Some(Level::Warn), LevelFilter::Warn),
];

for (verbose, quiet, expected_level, expected_filter) in tests.iter() {
let v = Verbosity::<WarnLevel>::new(*verbose, *quiet);
assert_eq!(
v.log_level(),
*expected_level,
"verbose = {verbose}, quiet = {quiet}"
);
assert_eq!(
v.log_level_filter(),
*expected_filter,
"verbose = {verbose}, quiet = {quiet}"
);
}
}

#[test]
fn verbosity_info_level() {
let tests = [
// verbose, quiet, expected_level, expected_filter
(0, 0, Some(Level::Info), LevelFilter::Info),
(1, 0, Some(Level::Debug), LevelFilter::Debug),
(2, 0, Some(Level::Trace), LevelFilter::Trace),
(3, 0, Some(Level::Trace), LevelFilter::Trace),
(255, 0, Some(Level::Trace), LevelFilter::Trace),
(0, 1, Some(Level::Warn), LevelFilter::Warn),
(0, 2, Some(Level::Error), LevelFilter::Error),
(0, 3, None, LevelFilter::Off),
(0, 4, None, LevelFilter::Off),
(0, 255, None, LevelFilter::Off),
(255, 255, Some(Level::Info), LevelFilter::Info),
];

for (verbose, quiet, expected_level, expected_filter) in tests.iter() {
let v = Verbosity::<InfoLevel>::new(*verbose, *quiet);
assert_eq!(
v.log_level(),
*expected_level,
"verbose = {verbose}, quiet = {quiet}"
);
assert_eq!(
v.log_level_filter(),
*expected_filter,
"verbose = {verbose}, quiet = {quiet}"
);
}
}
}
Loading