use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[repr(u8)]
pub enum LogLevel {
Trace = 0,
Debug = 1,
#[default]
Info = 2,
Warn = 3,
Error = 4,
Fatal = 5,
}
impl LogLevel {
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Option<Self> {
if s.eq_ignore_ascii_case("trace") {
Some(LogLevel::Trace)
} else if s.eq_ignore_ascii_case("debug") {
Some(LogLevel::Debug)
} else if s.eq_ignore_ascii_case("info") {
Some(LogLevel::Info)
} else if s.eq_ignore_ascii_case("warn") || s.eq_ignore_ascii_case("warning") {
Some(LogLevel::Warn)
} else if s.eq_ignore_ascii_case("error") {
Some(LogLevel::Error)
} else if s.eq_ignore_ascii_case("fatal") || s.eq_ignore_ascii_case("critical") {
Some(LogLevel::Fatal)
} else {
None
}
}
pub fn is_valid_level(s: &str) -> bool {
Self::from_str(s).is_some()
}
pub const VALID_LEVEL_STRINGS: &'static [&'static str] = &[
"trace", "debug", "info", "warn", "warning", "error", "fatal", "critical",
];
pub fn as_str(&self) -> &'static str {
match self {
LogLevel::Trace => "TRACE",
LogLevel::Debug => "DEBUG",
LogLevel::Info => "INFO",
LogLevel::Warn => "WARN",
LogLevel::Error => "ERROR",
LogLevel::Fatal => "FATAL",
}
}
pub fn as_short_str(&self) -> &'static str {
match self {
LogLevel::Trace => "TRC",
LogLevel::Debug => "DBG",
LogLevel::Info => "INF",
LogLevel::Warn => "WRN",
LogLevel::Error => "ERR",
LogLevel::Fatal => "FTL",
}
}
pub fn localized_name(&self) -> String {
match self {
LogLevel::Trace => crate::i18n::tr("log_level-name_trace"),
LogLevel::Debug => crate::i18n::tr("log_level-name_debug"),
LogLevel::Info => crate::i18n::tr("log_level-name_info"),
LogLevel::Warn => crate::i18n::tr("log_level-name_warn"),
LogLevel::Error => crate::i18n::tr("log_level-name_error"),
LogLevel::Fatal => crate::i18n::tr("log_level-name_fatal"),
}
}
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl FromStr for LogLevel {
type Err = LogLevelParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_str(s).ok_or_else(|| LogLevelParseError::Unknown(s.to_string()))
}
}
#[derive(Debug)]
pub enum LogLevelParseError {
Unknown(String),
}
impl std::fmt::Display for LogLevelParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LogLevelParseError::Unknown(level) => {
let mut args = fluent_bundle::FluentArgs::new();
args.set("level", level);
write!(
f,
"{}",
crate::i18n::tr_args("config-unknown_log_level", args)
)
}
}
}
}
impl std::error::Error for LogLevelParseError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_str_valid() {
assert_eq!(LogLevel::from_str("INFO"), Some(LogLevel::Info));
assert_eq!(LogLevel::from_str("info"), Some(LogLevel::Info));
assert_eq!(LogLevel::from_str("Info"), Some(LogLevel::Info)); assert_eq!(LogLevel::from_str("WARN"), Some(LogLevel::Warn));
assert_eq!(LogLevel::from_str("WARNING"), Some(LogLevel::Warn));
assert_eq!(LogLevel::from_str("warning"), Some(LogLevel::Warn));
assert_eq!(LogLevel::from_str("ERROR"), Some(LogLevel::Error));
assert_eq!(LogLevel::from_str("FATAL"), Some(LogLevel::Fatal));
assert_eq!(LogLevel::from_str("CRITICAL"), Some(LogLevel::Fatal));
assert_eq!(LogLevel::from_str("TRACE"), Some(LogLevel::Trace));
assert_eq!(LogLevel::from_str("DEBUG"), Some(LogLevel::Debug));
}
#[test]
fn test_from_str_invalid() {
assert_eq!(LogLevel::from_str("INVALID"), None);
assert_eq!(LogLevel::from_str(""), None);
}
#[test]
fn test_as_str() {
assert_eq!(LogLevel::Info.as_str(), "INFO");
assert_eq!(LogLevel::Error.as_str(), "ERROR");
}
#[test]
fn test_ordering() {
assert!(LogLevel::Error > LogLevel::Debug);
assert!(LogLevel::Info <= LogLevel::Warn);
assert!(LogLevel::Trace < LogLevel::Fatal);
}
#[test]
fn test_as_str_all_variants() {
assert_eq!(LogLevel::Trace.as_str(), "TRACE");
assert_eq!(LogLevel::Debug.as_str(), "DEBUG");
assert_eq!(LogLevel::Info.as_str(), "INFO");
assert_eq!(LogLevel::Warn.as_str(), "WARN");
assert_eq!(LogLevel::Error.as_str(), "ERROR");
assert_eq!(LogLevel::Fatal.as_str(), "FATAL");
}
#[test]
fn test_as_short_str_all_variants() {
assert_eq!(LogLevel::Trace.as_short_str(), "TRC");
assert_eq!(LogLevel::Debug.as_short_str(), "DBG");
assert_eq!(LogLevel::Info.as_short_str(), "INF");
assert_eq!(LogLevel::Warn.as_short_str(), "WRN");
assert_eq!(LogLevel::Error.as_short_str(), "ERR");
assert_eq!(LogLevel::Fatal.as_short_str(), "FTL");
}
#[test]
fn test_display_matches_as_str() {
for level in [
LogLevel::Trace,
LogLevel::Debug,
LogLevel::Info,
LogLevel::Warn,
LogLevel::Error,
LogLevel::Fatal,
] {
assert_eq!(format!("{}", level), level.as_str());
}
}
#[test]
fn test_from_str_trait_valid() {
let level: LogLevel = "debug".parse().expect("valid level should parse");
assert_eq!(level, LogLevel::Debug);
let level: LogLevel = "WARNING".parse().expect("WARNING should parse to Warn");
assert_eq!(level, LogLevel::Warn);
let level: LogLevel = "CRITICAL".parse().expect("CRITICAL should parse to Fatal");
assert_eq!(level, LogLevel::Fatal);
}
#[test]
fn test_from_str_trait_invalid_returns_error() {
let result: Result<LogLevel, _> = "invalid_level".parse();
let err = result.expect_err("invalid level should error");
assert!(format!("{}", err).contains("Unknown log level"));
assert!(format!("{}", err).contains("invalid_level"));
}
#[test]
fn test_from_str_trait_empty_string() {
let result: Result<LogLevel, _> = "".parse();
assert!(result.is_err());
}
#[test]
fn test_localized_name_all_levels() {
let levels = [
(LogLevel::Trace, "TRACE"),
(LogLevel::Debug, "DEBUG"),
(LogLevel::Info, "INFO"),
(LogLevel::Warn, "WARN"),
(LogLevel::Error, "ERROR"),
(LogLevel::Fatal, "FATAL"),
];
for (level, expected_en) in levels {
let name = level.localized_name();
assert!(!name.is_empty(), "localized_name() empty for {:?}", level);
if crate::i18n::current_locale() == "en" {
assert_eq!(name, expected_en, "en locale mismatch for {:?}", level);
}
}
}
#[test]
fn test_localized_name_differs_from_as_str_in_zh_cn() {
let name = LogLevel::Info.localized_name();
assert!(!name.is_empty());
if crate::i18n::current_locale() == "zh-CN" {
assert_eq!(name, "信息");
assert_ne!(name, LogLevel::Info.as_str());
}
}
}