1use log::LevelFilter;
2use std::error::Error;
3use std::fmt::{Display, Formatter};
4
5use crate::Directive;
6use crate::FilterOp;
7
8#[derive(Default, Debug)]
9pub(crate) struct ParseResult {
10 pub(crate) directives: Vec<Directive>,
11 pub(crate) filter: Option<FilterOp>,
12 pub(crate) errors: Vec<String>,
13}
14
15impl ParseResult {
16 fn add_directive(&mut self, directive: Directive) {
17 self.directives.push(directive);
18 }
19
20 fn set_filter(&mut self, filter: FilterOp) {
21 self.filter = Some(filter);
22 }
23
24 fn add_error(&mut self, message: String) {
25 self.errors.push(message);
26 }
27
28 pub(crate) fn ok(self) -> Result<(Vec<Directive>, Option<FilterOp>), ParseError> {
29 let Self {
30 directives,
31 filter,
32 errors,
33 } = self;
34 if let Some(error) = errors.into_iter().next() {
35 Err(ParseError { details: error })
36 } else {
37 Ok((directives, filter))
38 }
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44pub struct ParseError {
45 details: String,
46}
47
48impl Display for ParseError {
49 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
50 write!(f, "error parsing logger filter: {}", self.details)
51 }
52}
53
54impl Error for ParseError {}
55
56pub(crate) fn parse_spec(spec: &str) -> ParseResult {
59 let mut result = ParseResult::default();
60
61 let mut parts = spec.split('/');
62 let mods = parts.next();
63 let filter = parts.next();
64 if parts.next().is_some() {
65 result.add_error(format!("invalid logging spec '{spec}' (too many '/'s)"));
66 return result;
67 }
68 if let Some(m) = mods {
69 for s in m.split(',').map(|ss| ss.trim()) {
70 if s.is_empty() {
71 continue;
72 }
73 let mut parts = s.split('=');
74 let (log_level, name) =
75 match (parts.next(), parts.next().map(|s| s.trim()), parts.next()) {
76 (Some(part0), None, None) => {
77 match part0.parse() {
80 Ok(num) => (num, None),
81 Err(_) => (LevelFilter::max(), Some(part0)),
82 }
83 }
84 (Some(part0), Some(""), None) => (LevelFilter::max(), Some(part0)),
85 (Some(part0), Some(part1), None) => {
86 if let Ok(num) = part1.parse() {
87 (num, Some(part0))
88 } else {
89 result.add_error(format!("invalid logging spec '{part1}'"));
90 continue;
91 }
92 }
93 _ => {
94 result.add_error(format!("invalid logging spec '{s}'"));
95 continue;
96 }
97 };
98
99 result.add_directive(Directive {
100 name: name.map(|s| s.to_owned()),
101 level: log_level,
102 });
103 }
104 }
105
106 if let Some(filter) = filter {
107 match FilterOp::new(filter) {
108 Ok(filter_op) => result.set_filter(filter_op),
109 Err(err) => result.add_error(format!("invalid regex filter - {err}")),
110 }
111 }
112
113 result
114}
115
116#[cfg(test)]
117mod tests {
118 use crate::ParseError;
119 use log::LevelFilter;
120 use snapbox::{assert_data_eq, str, Data, IntoData};
121
122 use super::{parse_spec, ParseResult};
123
124 impl IntoData for ParseError {
125 fn into_data(self) -> Data {
126 self.to_string().into_data()
127 }
128 }
129
130 #[test]
131 fn parse_spec_valid() {
132 let ParseResult {
133 directives: dirs,
134 filter,
135 errors,
136 } = parse_spec("crate1::mod1=error,crate1::mod2,crate2=debug");
137
138 assert_eq!(dirs.len(), 3);
139 assert_eq!(dirs[0].name, Some("crate1::mod1".to_owned()));
140 assert_eq!(dirs[0].level, LevelFilter::Error);
141
142 assert_eq!(dirs[1].name, Some("crate1::mod2".to_owned()));
143 assert_eq!(dirs[1].level, LevelFilter::max());
144
145 assert_eq!(dirs[2].name, Some("crate2".to_owned()));
146 assert_eq!(dirs[2].level, LevelFilter::Debug);
147 assert!(filter.is_none());
148
149 assert!(errors.is_empty());
150 }
151
152 #[test]
153 fn parse_spec_invalid_crate() {
154 let ParseResult {
156 directives: dirs,
157 filter,
158 errors,
159 } = parse_spec("crate1::mod1=warn=info,crate2=debug");
160
161 assert_eq!(dirs.len(), 1);
162 assert_eq!(dirs[0].name, Some("crate2".to_owned()));
163 assert_eq!(dirs[0].level, LevelFilter::Debug);
164 assert!(filter.is_none());
165
166 assert_eq!(errors.len(), 1);
167 assert_data_eq!(
168 &errors[0],
169 str!["invalid logging spec 'crate1::mod1=warn=info'"]
170 );
171 }
172
173 #[test]
174 fn parse_spec_invalid_level() {
175 let ParseResult {
177 directives: dirs,
178 filter,
179 errors,
180 } = parse_spec("crate1::mod1=noNumber,crate2=debug");
181
182 assert_eq!(dirs.len(), 1);
183 assert_eq!(dirs[0].name, Some("crate2".to_owned()));
184 assert_eq!(dirs[0].level, LevelFilter::Debug);
185 assert!(filter.is_none());
186
187 assert_eq!(errors.len(), 1);
188 assert_data_eq!(&errors[0], str!["invalid logging spec 'noNumber'"]);
189 }
190
191 #[test]
192 fn parse_spec_string_level() {
193 let ParseResult {
195 directives: dirs,
196 filter,
197 errors,
198 } = parse_spec("crate1::mod1=wrong,crate2=warn");
199
200 assert_eq!(dirs.len(), 1);
201 assert_eq!(dirs[0].name, Some("crate2".to_owned()));
202 assert_eq!(dirs[0].level, LevelFilter::Warn);
203 assert!(filter.is_none());
204
205 assert_eq!(errors.len(), 1);
206 assert_data_eq!(&errors[0], str!["invalid logging spec 'wrong'"]);
207 }
208
209 #[test]
210 fn parse_spec_empty_level() {
211 let ParseResult {
213 directives: dirs,
214 filter,
215 errors,
216 } = parse_spec("crate1::mod1=wrong,crate2=");
217
218 assert_eq!(dirs.len(), 1);
219 assert_eq!(dirs[0].name, Some("crate2".to_owned()));
220 assert_eq!(dirs[0].level, LevelFilter::max());
221 assert!(filter.is_none());
222
223 assert_eq!(errors.len(), 1);
224 assert_data_eq!(&errors[0], str!["invalid logging spec 'wrong'"]);
225 }
226
227 #[test]
228 fn parse_spec_empty_level_isolated() {
229 let ParseResult {
231 directives: dirs,
232 filter,
233 errors,
234 } = parse_spec(""); assert_eq!(dirs.len(), 0);
236 assert!(filter.is_none());
237 assert!(errors.is_empty());
238 }
239
240 #[test]
241 fn parse_spec_blank_level_isolated() {
242 let ParseResult {
245 directives: dirs,
246 filter,
247 errors,
248 } = parse_spec(" "); assert_eq!(dirs.len(), 0);
250 assert!(filter.is_none());
251 assert!(errors.is_empty());
252 }
253
254 #[test]
255 fn parse_spec_blank_level_isolated_comma_only() {
256 let ParseResult {
260 directives: dirs,
261 filter,
262 errors,
263 } = parse_spec(","); assert_eq!(dirs.len(), 0);
265 assert!(filter.is_none());
266 assert!(errors.is_empty());
267 }
268
269 #[test]
270 fn parse_spec_blank_level_isolated_comma_blank() {
271 let ParseResult {
276 directives: dirs,
277 filter,
278 errors,
279 } = parse_spec(", "); assert_eq!(dirs.len(), 0);
281 assert!(filter.is_none());
282 assert!(errors.is_empty());
283 }
284
285 #[test]
286 fn parse_spec_blank_level_isolated_blank_comma() {
287 let ParseResult {
292 directives: dirs,
293 filter,
294 errors,
295 } = parse_spec(" ,"); assert_eq!(dirs.len(), 0);
297 assert!(filter.is_none());
298 assert!(errors.is_empty());
299 }
300
301 #[test]
302 fn parse_spec_global() {
303 let ParseResult {
305 directives: dirs,
306 filter,
307 errors,
308 } = parse_spec("warn,crate2=debug");
309 assert_eq!(dirs.len(), 2);
310 assert_eq!(dirs[0].name, None);
311 assert_eq!(dirs[0].level, LevelFilter::Warn);
312 assert_eq!(dirs[1].name, Some("crate2".to_owned()));
313 assert_eq!(dirs[1].level, LevelFilter::Debug);
314 assert!(filter.is_none());
315 assert!(errors.is_empty());
316 }
317
318 #[test]
319 fn parse_spec_global_bare_warn_lc() {
320 let ParseResult {
322 directives: dirs,
323 filter,
324 errors,
325 } = parse_spec("warn");
326 assert_eq!(dirs.len(), 1);
327 assert_eq!(dirs[0].name, None);
328 assert_eq!(dirs[0].level, LevelFilter::Warn);
329 assert!(filter.is_none());
330 assert!(errors.is_empty());
331 }
332
333 #[test]
334 fn parse_spec_global_bare_warn_uc() {
335 let ParseResult {
337 directives: dirs,
338 filter,
339 errors,
340 } = parse_spec("WARN");
341 assert_eq!(dirs.len(), 1);
342 assert_eq!(dirs[0].name, None);
343 assert_eq!(dirs[0].level, LevelFilter::Warn);
344 assert!(filter.is_none());
345 assert!(errors.is_empty());
346 }
347
348 #[test]
349 fn parse_spec_global_bare_warn_mixed() {
350 let ParseResult {
352 directives: dirs,
353 filter,
354 errors,
355 } = parse_spec("wArN");
356 assert_eq!(dirs.len(), 1);
357 assert_eq!(dirs[0].name, None);
358 assert_eq!(dirs[0].level, LevelFilter::Warn);
359 assert!(filter.is_none());
360 assert!(errors.is_empty());
361 }
362
363 #[test]
364 fn parse_spec_valid_filter() {
365 let ParseResult {
366 directives: dirs,
367 filter,
368 errors,
369 } = parse_spec("crate1::mod1=error,crate1::mod2,crate2=debug/abc");
370 assert_eq!(dirs.len(), 3);
371 assert_eq!(dirs[0].name, Some("crate1::mod1".to_owned()));
372 assert_eq!(dirs[0].level, LevelFilter::Error);
373
374 assert_eq!(dirs[1].name, Some("crate1::mod2".to_owned()));
375 assert_eq!(dirs[1].level, LevelFilter::max());
376
377 assert_eq!(dirs[2].name, Some("crate2".to_owned()));
378 assert_eq!(dirs[2].level, LevelFilter::Debug);
379 assert!(filter.is_some() && filter.unwrap().to_string() == "abc");
380 assert!(errors.is_empty());
381 }
382
383 #[test]
384 fn parse_spec_invalid_crate_filter() {
385 let ParseResult {
386 directives: dirs,
387 filter,
388 errors,
389 } = parse_spec("crate1::mod1=error=warn,crate2=debug/a.c");
390
391 assert_eq!(dirs.len(), 1);
392 assert_eq!(dirs[0].name, Some("crate2".to_owned()));
393 assert_eq!(dirs[0].level, LevelFilter::Debug);
394 assert!(filter.is_some() && filter.unwrap().to_string() == "a.c");
395
396 assert_eq!(errors.len(), 1);
397 assert_data_eq!(
398 &errors[0],
399 str!["invalid logging spec 'crate1::mod1=error=warn'"]
400 );
401 }
402
403 #[test]
404 fn parse_spec_empty_with_filter() {
405 let ParseResult {
406 directives: dirs,
407 filter,
408 errors,
409 } = parse_spec("crate1/a*c");
410 assert_eq!(dirs.len(), 1);
411 assert_eq!(dirs[0].name, Some("crate1".to_owned()));
412 assert_eq!(dirs[0].level, LevelFilter::max());
413 assert!(filter.is_some() && filter.unwrap().to_string() == "a*c");
414 assert!(errors.is_empty());
415 }
416
417 #[test]
418 fn parse_spec_with_multiple_filters() {
419 let ParseResult {
420 directives: dirs,
421 filter,
422 errors,
423 } = parse_spec("debug/abc/a.c");
424 assert!(dirs.is_empty());
425 assert!(filter.is_none());
426
427 assert_eq!(errors.len(), 1);
428 assert_data_eq!(
429 &errors[0],
430 str!["invalid logging spec 'debug/abc/a.c' (too many '/'s)"]
431 );
432 }
433
434 #[test]
435 fn parse_spec_multiple_invalid_crates() {
436 let ParseResult {
438 directives: dirs,
439 filter,
440 errors,
441 } = parse_spec("crate1::mod1=warn=info,crate2=debug,crate3=error=error");
442
443 assert_eq!(dirs.len(), 1);
444 assert_eq!(dirs[0].name, Some("crate2".to_owned()));
445 assert_eq!(dirs[0].level, LevelFilter::Debug);
446 assert!(filter.is_none());
447
448 assert_eq!(errors.len(), 2);
449 assert_data_eq!(
450 &errors[0],
451 str!["invalid logging spec 'crate1::mod1=warn=info'"]
452 );
453 assert_data_eq!(
454 &errors[1],
455 str!["invalid logging spec 'crate3=error=error'"]
456 );
457 }
458
459 #[test]
460 fn parse_spec_multiple_invalid_levels() {
461 let ParseResult {
463 directives: dirs,
464 filter,
465 errors,
466 } = parse_spec("crate1::mod1=noNumber,crate2=debug,crate3=invalid");
467
468 assert_eq!(dirs.len(), 1);
469 assert_eq!(dirs[0].name, Some("crate2".to_owned()));
470 assert_eq!(dirs[0].level, LevelFilter::Debug);
471 assert!(filter.is_none());
472
473 assert_eq!(errors.len(), 2);
474 assert_data_eq!(&errors[0], str!["invalid logging spec 'noNumber'"]);
475 assert_data_eq!(&errors[1], str!["invalid logging spec 'invalid'"]);
476 }
477
478 #[test]
479 fn parse_spec_invalid_crate_and_level() {
480 let ParseResult {
482 directives: dirs,
483 filter,
484 errors,
485 } = parse_spec("crate1::mod1=debug=info,crate2=debug,crate3=invalid");
486
487 assert_eq!(dirs.len(), 1);
488 assert_eq!(dirs[0].name, Some("crate2".to_owned()));
489 assert_eq!(dirs[0].level, LevelFilter::Debug);
490 assert!(filter.is_none());
491
492 assert_eq!(errors.len(), 2);
493 assert_data_eq!(
494 &errors[0],
495 str!["invalid logging spec 'crate1::mod1=debug=info'"]
496 );
497 assert_data_eq!(&errors[1], str!["invalid logging spec 'invalid'"]);
498 }
499
500 #[test]
501 fn parse_error_message_single_error() {
502 let error = parse_spec("crate1::mod1=debug=info,crate2=debug")
503 .ok()
504 .unwrap_err();
505 assert_data_eq!(
506 error,
507 str!["error parsing logger filter: invalid logging spec 'crate1::mod1=debug=info'"]
508 );
509 }
510
511 #[test]
512 fn parse_error_message_multiple_errors() {
513 let error = parse_spec("crate1::mod1=debug=info,crate2=debug,crate3=invalid")
514 .ok()
515 .unwrap_err();
516 assert_data_eq!(
517 error,
518 str!["error parsing logger filter: invalid logging spec 'crate1::mod1=debug=info'"]
519 );
520 }
521}