1use crate::level::Level;
11
12#[derive(Debug, Clone)]
17pub struct FilterRule {
18 pub target: String,
20 pub level: Level,
22}
23
24impl FilterRule {
25 pub fn new(target: impl Into<String>, level: Level) -> Self {
27 Self {
28 target: target.into(),
29 level,
30 }
31 }
32}
33
34#[derive(Debug, Clone)]
36pub struct Filter {
37 default_level: Level,
38 rules: Vec<FilterRule>,
39}
40
41impl Filter {
42 pub fn new(default_level: Level) -> Self {
44 Self {
45 default_level,
46 rules: Vec::new(),
47 }
48 }
49
50 pub fn with_rule(mut self, target: impl Into<String>, level: Level) -> Self {
53 self.rules.push(FilterRule::new(target, level));
54 self
55 }
56
57 pub fn default_level(&self) -> Level {
59 self.default_level
60 }
61
62 pub fn rules(&self) -> &[FilterRule] {
64 &self.rules
65 }
66
67 pub fn is_enabled(&self, target: &str, level: Level) -> bool {
72 level.is_enabled_at(self.threshold_for(target))
73 }
74
75 pub fn threshold_for(&self, target: &str) -> Level {
77 for rule in &self.rules {
78 if target_matches(target, &rule.target) {
79 return rule.level;
80 }
81 }
82 self.default_level
83 }
84
85 pub fn parse(directive: &str) -> core::result::Result<Self, ParseFilterError> {
94 let mut default_level = Level::Off;
95 let mut rules = Vec::new();
96 let mut saw_default = false;
97
98 for segment in directive.split(',') {
99 let segment = segment.trim();
100 if segment.is_empty() {
101 continue;
102 }
103 if let Some((target, level)) = segment.split_once('=') {
104 let level: Level = level.trim().parse().map_err(|_| ParseFilterError)?;
105 rules.push(FilterRule::new(target.trim(), level));
106 } else {
107 let level: Level = segment.parse().map_err(|_| ParseFilterError)?;
108 default_level = level;
109 saw_default = true;
110 }
111 }
112
113 if !saw_default && !rules.is_empty() {
114 default_level = Level::Off;
115 }
116 Ok(Self {
117 default_level,
118 rules,
119 })
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct ParseFilterError;
127
128impl core::fmt::Display for ParseFilterError {
129 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
130 f.write_str("invalid filter directive")
131 }
132}
133
134impl std::error::Error for ParseFilterError {}
135
136fn target_matches(target: &str, prefix: &str) -> bool {
137 if target == prefix {
138 return true;
139 }
140 if target.len() > prefix.len() && target.starts_with(prefix) {
141 let rest = &target.as_bytes()[prefix.len()..];
142 return rest.starts_with(b"::") || rest.starts_with(b".");
144 }
145 false
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn default_applies_when_no_rules_match() {
154 let f = Filter::new(Level::Warn);
155 assert!(f.is_enabled("anything", Level::Warn));
156 assert!(!f.is_enabled("anything", Level::Info));
157 }
158
159 #[test]
160 fn rule_overrides_default_for_matching_prefix() {
161 let f = Filter::new(Level::Warn).with_rule("app::auth", Level::Debug);
162 assert!(f.is_enabled("app::auth", Level::Debug));
163 assert!(f.is_enabled("app::auth::login", Level::Debug));
164 assert!(!f.is_enabled("app::auth", Level::Trace));
165 assert!(!f.is_enabled("hyper", Level::Info));
167 }
168
169 #[test]
170 fn prefix_match_respects_module_boundary() {
171 let f = Filter::new(Level::Off).with_rule("app", Level::Info);
172 assert!(f.is_enabled("app", Level::Info));
173 assert!(f.is_enabled("app::sub", Level::Info));
174 assert!(!f.is_enabled("application", Level::Info));
176 }
177
178 #[test]
179 fn parse_simple_directive() {
180 let f = Filter::parse("info").unwrap();
181 assert_eq!(f.default_level(), Level::Info);
182 assert!(f.rules().is_empty());
183 }
184
185 #[test]
186 fn parse_with_overrides() {
187 let f = Filter::parse("info, app::auth = debug ,hyper=warn").unwrap();
188 assert_eq!(f.default_level(), Level::Info);
189 assert_eq!(f.rules().len(), 2);
190 assert!(f.is_enabled("hyper", Level::Warn));
191 assert!(!f.is_enabled("hyper", Level::Info));
192 assert!(f.is_enabled("app::auth", Level::Debug));
193 }
194
195 #[test]
196 fn parse_rejects_garbage() {
197 assert!(Filter::parse("nope").is_err());
198 assert!(Filter::parse("hyper=verbose").is_err());
199 }
200
201 #[test]
202 fn rules_without_default_disables_unmatched() {
203 let f = Filter::parse("hyper=warn").unwrap();
204 assert_eq!(f.default_level(), Level::Off);
205 assert!(f.is_enabled("hyper", Level::Warn));
206 assert!(!f.is_enabled("other", Level::Error));
207 }
208}