1use std::fmt::{Display, Formatter};
2use std::str::FromStr;
3
4use clap::ValueEnum;
5
6#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, ValueEnum)]
7pub enum LogLevel {
8 Trace,
9 Debug,
10 #[default]
11 Info,
12 Warn,
13 Error,
14}
15
16impl LogLevel {
17 fn variants() -> &'static [&'static str] {
18 &["trace", "debug", "info", "warn", "error"]
19 }
20}
21
22impl Display for LogLevel {
23 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
24 f.write_fmt(format_args!("{:?}", self))
25 }
26}
27
28impl FromStr for LogLevel {
29 type Err = ParseLogLevelError;
30
31 fn from_str(input: &str) -> Result<Self, Self::Err> {
32 fn parse_num_level(input: &str) -> Option<LogLevel> {
33 input.parse::<u8>().ok().and_then(|number| match number {
34 1 => Some(LogLevel::Error),
35 2 => Some(LogLevel::Warn),
36 3 => Some(LogLevel::Info),
37 4 => Some(LogLevel::Debug),
38 5 => Some(LogLevel::Trace),
39 _ => None,
40 })
41 }
42
43 fn parse_str_level(input: &str) -> Option<LogLevel> {
44 match input {
45 s if s.eq_ignore_ascii_case("error") => Some(LogLevel::Error),
46 s if s.eq_ignore_ascii_case("warn") => Some(LogLevel::Warn),
47 s if s.eq_ignore_ascii_case("info") => Some(LogLevel::Info),
48 s if s.eq_ignore_ascii_case("debug") => Some(LogLevel::Debug),
49 s if s.eq_ignore_ascii_case("trace") => Some(LogLevel::Trace),
50 _ => None,
51 }
52 }
53
54 parse_num_level(input)
55 .or_else(|| parse_str_level(input))
56 .ok_or_else(|| ParseLogLevelError::NoMatchingLevel {
57 given_input: input.to_string(),
58 valid_options_formatted: Self::variants().join(","),
59 })
60 }
61}
62
63impl From<LogLevel> for tracing::Level {
64 fn from(level: LogLevel) -> Self {
65 match level {
66 LogLevel::Trace => tracing::Level::TRACE,
67 LogLevel::Debug => tracing::Level::DEBUG,
68 LogLevel::Info => tracing::Level::INFO,
69 LogLevel::Warn => tracing::Level::WARN,
70 LogLevel::Error => tracing::Level::ERROR,
71 }
72 }
73}
74
75#[derive(Debug, thiserror::Error)]
76pub enum ParseLogLevelError {
77 #[error(
78 "The given log level '{given_input}' does not exist, valid options are: {valid_options_formatted}]"
79 )]
80 NoMatchingLevel {
81 given_input: String,
82 valid_options_formatted: String,
83 },
84}
85
86#[cfg(test)]
87mod tests {
88 use crate::log_level::{LogLevel, ParseLogLevelError};
89
90 #[yare::parameterized(
91 trace_numeric = { "5", LogLevel::Trace },
92 trace_alphabetic = { "trace", LogLevel::Trace },
93 debug_numeric = { "4", LogLevel::Debug },
94 debug_alphabetic = { "debug", LogLevel::Debug },
95 debug_alphabetic_uppercase = { "DEBUG", LogLevel::Debug },
96 info_numeric = { "3", LogLevel::Info },
97 info_alphabetic = { "info", LogLevel::Info },
98 info_alphabetic_uppercase = { "INFO", LogLevel::Info },
99 warn_numeric = { "2", LogLevel::Warn },
100 warn_alphabetic = { "warn", LogLevel::Warn },
101 warn_alphabetic_uppercase = { "WARN", LogLevel::Warn },
102 error_numeric = { "1", LogLevel::Error },
103 error_alphabetic = { "error", LogLevel::Error },
104 error_alphabetic_uppercase = { "ERROR", LogLevel::Error },
105 )]
106 fn valid_log_levels(input: &str, expected: LogLevel) {
107 assert_eq!(input.parse::<LogLevel>().unwrap(), expected);
108 }
109
110 #[yare::parameterized(
111 numeric_floor = { "0" },
112 numeric_ceil = { "6" },
113 non_existing = { "null" },
114 empty = { "" },
115 )]
116 fn invalid_log_levels(input: &str) {
117 let expected_err = input.parse::<LogLevel>().unwrap_err();
118
119 match expected_err {
120 ParseLogLevelError::NoMatchingLevel {
121 ref given_input, ..
122 } => assert_eq!(given_input, input),
123 };
124 }
125}