1use core::cmp::Ordering;
4use core::fmt;
5use core::str::FromStr;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14#[repr(u8)]
15pub enum Level {
16 Trace = 0,
18 Debug = 1,
20 Info = 2,
22 Warn = 3,
24 Error = 4,
26 Off = 255,
29}
30
31impl Level {
32 pub const fn as_str(self) -> &'static str {
37 match self {
38 Self::Off => "off",
39 Self::Trace => "trace",
40 Self::Debug => "debug",
41 Self::Info => "info",
42 Self::Warn => "warn",
43 Self::Error => "error",
44 }
45 }
46
47 pub const fn as_str_upper(self) -> &'static str {
52 match self {
53 Self::Off => "OFF",
54 Self::Trace => "TRACE",
55 Self::Debug => "DEBUG",
56 Self::Info => "INFO",
57 Self::Warn => "WARN",
58 Self::Error => "ERROR",
59 }
60 }
61
62 pub const fn is_enabled_at(self, threshold: Self) -> bool {
67 (self as u8) >= (threshold as u8)
68 }
69
70 pub const ALL: [Self; 5] = [
74 Self::Trace,
75 Self::Debug,
76 Self::Info,
77 Self::Warn,
78 Self::Error,
79 ];
80}
81
82impl fmt::Display for Level {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 f.write_str(self.as_str())
85 }
86}
87
88impl PartialOrd for Level {
89 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
90 Some(self.cmp(other))
91 }
92}
93
94impl Ord for Level {
95 fn cmp(&self, other: &Self) -> Ordering {
96 (*self as u8).cmp(&(*other as u8))
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct ParseLevelError;
103
104impl fmt::Display for ParseLevelError {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 f.write_str("invalid log level (expected one of: off, trace, debug, info, warn, error)")
107 }
108}
109
110#[cfg(feature = "std")]
111impl std::error::Error for ParseLevelError {}
112
113impl FromStr for Level {
114 type Err = ParseLevelError;
115
116 fn from_str(s: &str) -> Result<Self, Self::Err> {
117 if s.eq_ignore_ascii_case("off") {
119 Ok(Self::Off)
120 } else if s.eq_ignore_ascii_case("trace") {
121 Ok(Self::Trace)
122 } else if s.eq_ignore_ascii_case("debug") {
123 Ok(Self::Debug)
124 } else if s.eq_ignore_ascii_case("info") {
125 Ok(Self::Info)
126 } else if s.eq_ignore_ascii_case("warn") || s.eq_ignore_ascii_case("warning") {
127 Ok(Self::Warn)
128 } else if s.eq_ignore_ascii_case("error") || s.eq_ignore_ascii_case("err") {
129 Ok(Self::Error)
130 } else {
131 Err(ParseLevelError)
132 }
133 }
134}
135
136#[cfg(all(test, feature = "std"))]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn ordering_is_total_and_severity_aligned() {
142 assert!(Level::Trace < Level::Debug);
143 assert!(Level::Debug < Level::Info);
144 assert!(Level::Info < Level::Warn);
145 assert!(Level::Warn < Level::Error);
146 assert!(Level::Error < Level::Off);
147 }
148
149 #[test]
150 fn is_enabled_at_respects_threshold() {
151 assert!(Level::Error.is_enabled_at(Level::Info));
152 assert!(Level::Info.is_enabled_at(Level::Info));
153 assert!(!Level::Debug.is_enabled_at(Level::Info));
154 assert!(!Level::Trace.is_enabled_at(Level::Warn));
155 }
156
157 #[test]
158 fn parse_round_trip_canonical_forms() {
159 for level in Level::ALL {
160 let parsed: Level = level.as_str().parse().unwrap();
161 assert_eq!(parsed, level);
162 let parsed_upper: Level = level.as_str_upper().parse().unwrap();
163 assert_eq!(parsed_upper, level);
164 }
165 assert_eq!("off".parse::<Level>().unwrap(), Level::Off);
166 assert_eq!("OFF".parse::<Level>().unwrap(), Level::Off);
167 }
168
169 #[test]
170 fn parse_aliases() {
171 assert_eq!("warning".parse::<Level>().unwrap(), Level::Warn);
172 assert_eq!("err".parse::<Level>().unwrap(), Level::Error);
173 }
174
175 #[test]
176 fn parse_rejects_garbage() {
177 assert!("verbose".parse::<Level>().is_err());
178 assert!("".parse::<Level>().is_err());
179 }
180
181 #[test]
182 fn display_lowercase() {
183 assert_eq!(Level::Info.to_string(), "info");
184 }
185
186 #[test]
187 fn upper_form() {
188 assert_eq!(Level::Warn.as_str_upper(), "WARN");
189 }
190}