cargo-msrv 0.19.2

Find your minimum supported Rust version (MSRV)!
Documentation
use std::fmt::{Display, Formatter};
use std::str::FromStr;

use clap::ValueEnum;

#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, ValueEnum)]
pub enum LogLevel {
    Trace,
    Debug,
    #[default]
    Info,
    Warn,
    Error,
}

impl LogLevel {
    fn variants() -> &'static [&'static str] {
        &["trace", "debug", "info", "warn", "error"]
    }
}

impl Display for LogLevel {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.write_fmt(format_args!("{:?}", self))
    }
}

impl FromStr for LogLevel {
    type Err = ParseLogLevelError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        fn parse_num_level(input: &str) -> Option<LogLevel> {
            input.parse::<u8>().ok().and_then(|number| match number {
                1 => Some(LogLevel::Error),
                2 => Some(LogLevel::Warn),
                3 => Some(LogLevel::Info),
                4 => Some(LogLevel::Debug),
                5 => Some(LogLevel::Trace),
                _ => None,
            })
        }

        fn parse_str_level(input: &str) -> Option<LogLevel> {
            match input {
                s if s.eq_ignore_ascii_case("error") => Some(LogLevel::Error),
                s if s.eq_ignore_ascii_case("warn") => Some(LogLevel::Warn),
                s if s.eq_ignore_ascii_case("info") => Some(LogLevel::Info),
                s if s.eq_ignore_ascii_case("debug") => Some(LogLevel::Debug),
                s if s.eq_ignore_ascii_case("trace") => Some(LogLevel::Trace),
                _ => None,
            }
        }

        parse_num_level(input)
            .or_else(|| parse_str_level(input))
            .ok_or_else(|| ParseLogLevelError::NoMatchingLevel {
                given_input: input.to_string(),
                valid_options_formatted: Self::variants().join(","),
            })
    }
}

impl From<LogLevel> for tracing::Level {
    fn from(level: LogLevel) -> Self {
        match level {
            LogLevel::Trace => tracing::Level::TRACE,
            LogLevel::Debug => tracing::Level::DEBUG,
            LogLevel::Info => tracing::Level::INFO,
            LogLevel::Warn => tracing::Level::WARN,
            LogLevel::Error => tracing::Level::ERROR,
        }
    }
}

#[derive(Debug, thiserror::Error)]
pub enum ParseLogLevelError {
    #[error("The given log level '{given_input}' does not exist, valid options are: {valid_options_formatted}]")]
    NoMatchingLevel {
        given_input: String,
        valid_options_formatted: String,
    },
}

#[cfg(test)]
mod tests {
    use crate::log_level::{LogLevel, ParseLogLevelError};

    #[yare::parameterized(
        trace_numeric = {  "5", LogLevel::Trace },
        trace_alphabetic = { "trace", LogLevel::Trace },
        debug_numeric = {  "4", LogLevel::Debug },
        debug_alphabetic = { "debug", LogLevel::Debug },
        debug_alphabetic_uppercase = { "DEBUG", LogLevel::Debug },
        info_numeric = {  "3", LogLevel::Info },
        info_alphabetic = { "info", LogLevel::Info },
        info_alphabetic_uppercase = { "INFO", LogLevel::Info },
        warn_numeric = {  "2", LogLevel::Warn },
        warn_alphabetic = { "warn", LogLevel::Warn },
        warn_alphabetic_uppercase = { "WARN", LogLevel::Warn },
        error_numeric = {  "1", LogLevel::Error },
        error_alphabetic = { "error", LogLevel::Error },
        error_alphabetic_uppercase = { "ERROR", LogLevel::Error },
    )]
    fn valid_log_levels(input: &str, expected: LogLevel) {
        assert_eq!(input.parse::<LogLevel>().unwrap(), expected);
    }

    #[yare::parameterized(
        numeric_floor = { "0" },
        numeric_ceil = { "6" },
        non_existing = { "null" },
        empty = { "" },
    )]
    fn invalid_log_levels(input: &str) {
        let expected_err = input.parse::<LogLevel>().unwrap_err();

        match expected_err {
            ParseLogLevelError::NoMatchingLevel {
                ref given_input, ..
            } => assert_eq!(given_input, input),
        };
    }
}