use crate::level::Level;
#[derive(Debug, Clone)]
pub struct FilterRule {
pub target: String,
pub level: Level,
}
impl FilterRule {
pub fn new(target: impl Into<String>, level: Level) -> Self {
Self {
target: target.into(),
level,
}
}
}
#[derive(Debug, Clone)]
pub struct Filter {
default_level: Level,
rules: Vec<FilterRule>,
}
impl Filter {
pub fn new(default_level: Level) -> Self {
Self {
default_level,
rules: Vec::new(),
}
}
pub fn with_rule(mut self, target: impl Into<String>, level: Level) -> Self {
self.rules.push(FilterRule::new(target, level));
self
}
pub fn default_level(&self) -> Level {
self.default_level
}
pub fn rules(&self) -> &[FilterRule] {
&self.rules
}
pub fn is_enabled(&self, target: &str, level: Level) -> bool {
level.is_enabled_at(self.threshold_for(target))
}
pub fn threshold_for(&self, target: &str) -> Level {
for rule in &self.rules {
if target_matches(target, &rule.target) {
return rule.level;
}
}
self.default_level
}
pub fn parse(directive: &str) -> core::result::Result<Self, ParseFilterError> {
let mut default_level = Level::Off;
let mut rules = Vec::new();
let mut saw_default = false;
for segment in directive.split(',') {
let segment = segment.trim();
if segment.is_empty() {
continue;
}
if let Some((target, level)) = segment.split_once('=') {
let level: Level = level.trim().parse().map_err(|_| ParseFilterError)?;
rules.push(FilterRule::new(target.trim(), level));
} else {
let level: Level = segment.parse().map_err(|_| ParseFilterError)?;
default_level = level;
saw_default = true;
}
}
if !saw_default && !rules.is_empty() {
default_level = Level::Off;
}
Ok(Self {
default_level,
rules,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseFilterError;
impl core::fmt::Display for ParseFilterError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("invalid filter directive")
}
}
impl std::error::Error for ParseFilterError {}
fn target_matches(target: &str, prefix: &str) -> bool {
if target == prefix {
return true;
}
if target.len() > prefix.len() && target.starts_with(prefix) {
let rest = &target.as_bytes()[prefix.len()..];
return rest.starts_with(b"::") || rest.starts_with(b".");
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_applies_when_no_rules_match() {
let f = Filter::new(Level::Warn);
assert!(f.is_enabled("anything", Level::Warn));
assert!(!f.is_enabled("anything", Level::Info));
}
#[test]
fn rule_overrides_default_for_matching_prefix() {
let f = Filter::new(Level::Warn).with_rule("app::auth", Level::Debug);
assert!(f.is_enabled("app::auth", Level::Debug));
assert!(f.is_enabled("app::auth::login", Level::Debug));
assert!(!f.is_enabled("app::auth", Level::Trace));
assert!(!f.is_enabled("hyper", Level::Info));
}
#[test]
fn prefix_match_respects_module_boundary() {
let f = Filter::new(Level::Off).with_rule("app", Level::Info);
assert!(f.is_enabled("app", Level::Info));
assert!(f.is_enabled("app::sub", Level::Info));
assert!(!f.is_enabled("application", Level::Info));
}
#[test]
fn parse_simple_directive() {
let f = Filter::parse("info").unwrap();
assert_eq!(f.default_level(), Level::Info);
assert!(f.rules().is_empty());
}
#[test]
fn parse_with_overrides() {
let f = Filter::parse("info, app::auth = debug ,hyper=warn").unwrap();
assert_eq!(f.default_level(), Level::Info);
assert_eq!(f.rules().len(), 2);
assert!(f.is_enabled("hyper", Level::Warn));
assert!(!f.is_enabled("hyper", Level::Info));
assert!(f.is_enabled("app::auth", Level::Debug));
}
#[test]
fn parse_rejects_garbage() {
assert!(Filter::parse("nope").is_err());
assert!(Filter::parse("hyper=verbose").is_err());
}
#[test]
fn rules_without_default_disables_unmatched() {
let f = Filter::parse("hyper=warn").unwrap();
assert_eq!(f.default_level(), Level::Off);
assert!(f.is_enabled("hyper", Level::Warn));
assert!(!f.is_enabled("other", Level::Error));
}
}