1use core::cmp::Ordering;
4use core::fmt;
5use core::str::FromStr;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29#[repr(u8)]
30pub enum Level {
31 Trace = 0,
33 Debug = 1,
35 Info = 2,
37 Warn = 3,
39 Error = 4,
41 Off = 255,
44}
45
46impl Level {
47 #[must_use]
52 pub const fn as_str(self) -> &'static str {
53 match self {
54 Self::Off => "off",
55 Self::Trace => "trace",
56 Self::Debug => "debug",
57 Self::Info => "info",
58 Self::Warn => "warn",
59 Self::Error => "error",
60 }
61 }
62
63 #[must_use]
68 pub const fn as_str_upper(self) -> &'static str {
69 match self {
70 Self::Off => "OFF",
71 Self::Trace => "TRACE",
72 Self::Debug => "DEBUG",
73 Self::Info => "INFO",
74 Self::Warn => "WARN",
75 Self::Error => "ERROR",
76 }
77 }
78
79 #[must_use]
84 pub const fn is_enabled_at(self, threshold: Self) -> bool {
85 (self as u8) >= (threshold as u8)
86 }
87
88 pub const ALL: [Self; 5] = [
92 Self::Trace,
93 Self::Debug,
94 Self::Info,
95 Self::Warn,
96 Self::Error,
97 ];
98}
99
100impl fmt::Display for Level {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 f.write_str(self.as_str())
103 }
104}
105
106impl PartialOrd for Level {
107 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
108 Some(self.cmp(other))
109 }
110}
111
112impl Ord for Level {
113 fn cmp(&self, other: &Self) -> Ordering {
114 (*self as u8).cmp(&(*other as u8))
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct ParseLevelError;
124
125impl fmt::Display for ParseLevelError {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 f.write_str("invalid log level (expected one of: off, trace, debug, info, warn, error)")
128 }
129}
130
131#[cfg(feature = "std")]
132impl std::error::Error for ParseLevelError {}
133
134impl FromStr for Level {
135 type Err = ParseLevelError;
136
137 fn from_str(s: &str) -> Result<Self, Self::Err> {
138 if s.eq_ignore_ascii_case("off") {
140 Ok(Self::Off)
141 } else if s.eq_ignore_ascii_case("trace") {
142 Ok(Self::Trace)
143 } else if s.eq_ignore_ascii_case("debug") {
144 Ok(Self::Debug)
145 } else if s.eq_ignore_ascii_case("info") {
146 Ok(Self::Info)
147 } else if s.eq_ignore_ascii_case("warn") || s.eq_ignore_ascii_case("warning") {
148 Ok(Self::Warn)
149 } else if s.eq_ignore_ascii_case("error") || s.eq_ignore_ascii_case("err") {
150 Ok(Self::Error)
151 } else {
152 Err(ParseLevelError)
153 }
154 }
155}
156
157#[cfg(all(test, feature = "std"))]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn test_level_ordering_is_severity_aligned() {
163 assert!(Level::Trace < Level::Debug);
164 assert!(Level::Debug < Level::Info);
165 assert!(Level::Info < Level::Warn);
166 assert!(Level::Warn < Level::Error);
167 assert!(Level::Error < Level::Off);
168 }
169
170 #[test]
171 fn test_level_is_enabled_at_respects_threshold() {
172 assert!(Level::Error.is_enabled_at(Level::Info));
173 assert!(Level::Info.is_enabled_at(Level::Info));
174 assert!(!Level::Debug.is_enabled_at(Level::Info));
175 assert!(!Level::Trace.is_enabled_at(Level::Warn));
176 }
177
178 #[test]
179 fn test_level_parse_round_trips_canonical_forms() {
180 for level in Level::ALL {
181 let parsed: Level = level.as_str().parse().unwrap();
182 assert_eq!(parsed, level);
183 let parsed_upper: Level = level.as_str_upper().parse().unwrap();
184 assert_eq!(parsed_upper, level);
185 }
186 assert_eq!("off".parse::<Level>().unwrap(), Level::Off);
187 assert_eq!("OFF".parse::<Level>().unwrap(), Level::Off);
188 }
189
190 #[test]
191 fn test_level_parse_accepts_aliases() {
192 assert_eq!("warning".parse::<Level>().unwrap(), Level::Warn);
193 assert_eq!("err".parse::<Level>().unwrap(), Level::Error);
194 }
195
196 #[test]
197 fn test_level_parse_rejects_garbage() {
198 assert!("verbose".parse::<Level>().is_err());
199 assert!("".parse::<Level>().is_err());
200 }
201
202 #[test]
203 fn test_parse_level_error_display_lists_accepted_values() {
204 let msg = ParseLevelError.to_string();
205 for name in ["off", "trace", "debug", "info", "warn", "error"] {
206 assert!(msg.contains(name), "{msg}");
207 }
208 }
209
210 #[test]
211 fn test_level_display_is_lowercase() {
212 assert_eq!(Level::Info.to_string(), "info");
213 }
214
215 #[test]
216 fn test_level_upper_form() {
217 assert_eq!(Level::Warn.as_str_upper(), "WARN");
218 }
219}