Skip to main content

log_io/
level.rs

1//! Severity levels.
2
3use core::cmp::Ordering;
4use core::fmt;
5use core::str::FromStr;
6
7/// Severity of a log record.
8///
9/// Levels are ordered from least to most severe: [`Self::Trace`] <
10/// [`Self::Debug`] < [`Self::Info`] < [`Self::Warn`] < [`Self::Error`].
11/// [`Self::Off`] is greater than all real severities and is used
12/// exclusively to disable a filter or logger entirely.
13///
14/// The set is deliberately closed (it mirrors the `log` and `tracing`
15/// crates), so the enum is exhaustive and can be matched without a
16/// wildcard arm.
17///
18/// # Examples
19///
20/// ```
21/// use log_io::Level;
22///
23/// assert!(Level::Error > Level::Warn);
24/// assert!(Level::Warn.is_enabled_at(Level::Info));
25/// assert_eq!("WARNING".parse::<Level>(), Ok(Level::Warn));
26/// assert_eq!(Level::Debug.to_string(), "debug");
27/// ```
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29#[repr(u8)]
30pub enum Level {
31    /// Fine-grained diagnostic information for tracing execution flow.
32    Trace = 0,
33    /// Diagnostic information useful while debugging.
34    Debug = 1,
35    /// Informational messages about normal operation.
36    Info = 2,
37    /// A condition that may require attention but is not an error.
38    Warn = 3,
39    /// An error condition that prevented an operation from completing.
40    Error = 4,
41    /// Disable logging entirely. Never attached to a real [`crate::Record`].
42    /// Used only as a filter threshold sentinel meaning "admit nothing".
43    Off = 255,
44}
45
46impl Level {
47    /// The lowercase canonical string for this level.
48    ///
49    /// Returns `"off"` for [`Self::Off`]. This is the form emitted by
50    /// formatters by default.
51    #[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    /// The uppercase canonical string for this level.
64    ///
65    /// Useful for human-readable formats that conventionally upper-case
66    /// the severity column.
67    #[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    /// Returns `true` if this level is at least as severe as `other`.
80    ///
81    /// Used by filters: a record at level `self` passes a threshold of
82    /// `other` when `self.is_enabled_at(other)` is `true`.
83    #[must_use]
84    pub const fn is_enabled_at(self, threshold: Self) -> bool {
85        (self as u8) >= (threshold as u8)
86    }
87
88    /// All real severities, ordered from least to most severe.
89    ///
90    /// Excludes [`Self::Off`]. Useful for iteration in tests.
91    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/// Error returned when a string cannot be parsed as a [`Level`].
119///
120/// Accepted spellings (ASCII case-insensitive): `off`, `trace`,
121/// `debug`, `info`, `warn`, `warning`, `error`, `err`.
122#[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        // Case-insensitive without allocating: compare byte-by-byte.
139        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}