Skip to content

Commit ad011b4

Browse files
committed
feat: Add Builder::try_parse method
Current implementation of the Builder::parse() method prints out specification errors to stderr and then just ignores them. This is fine for most console applications, but in some cases better control over what is happening is needed: * Sometimes there is no access to stderr, in that case there is no way to find out what went wrong. * For some applications it's more desirable to fail on startup than to run with (partially) invalid configuration. For such cases this commit introduces a new method try_parse that does the same thing as the parse method, but returns an Err in case an error in the specification is found.
1 parent 8429616 commit ad011b4

File tree

4 files changed

+183
-1
lines changed

4 files changed

+183
-1
lines changed

crates/env_filter/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
# Change Log
2+
23
All notable changes to this project will be documented in this file.
34

45
The format is based on [Keep a Changelog](http://keepachangelog.com/)
56
and this project adheres to [Semantic Versioning](http://semver.org/).
67

78
<!-- next-header -->
9+
810
## [Unreleased] - ReleaseDate
911

12+
### Features
13+
14+
- Added `env_filter::Builder::try_parse(&self, &str)` method (failable version of `env_filter::Builder::parse()`)
15+
1016
## [0.1.0] - 2024-01-19
1117

1218
<!-- next-url -->
19+
1320
[Unreleased]: https://github.com/rust-cli/env_logger/compare/env_filter-v0.1.0...HEAD
21+
1422
[0.1.0]: https://github.com/rust-cli/env_logger/compare/b4a2c304c16d1db4a2998f24c00e00c0f776113b...env_filter-v0.1.0

crates/env_filter/src/filter.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::parse_spec;
99
use crate::parser::ParseResult;
1010
use crate::Directive;
1111
use crate::FilterOp;
12+
use crate::ParseError;
1213

1314
/// A builder for a log filter.
1415
///
@@ -118,6 +119,22 @@ impl Builder {
118119
self
119120
}
120121

122+
/// Parses the directive string, returning an error if the given directive string is invalid.
123+
///
124+
/// See the [Enabling Logging] section for more details.
125+
///
126+
/// [Enabling Logging]: ../index.html#enabling-logging
127+
pub fn try_parse(&mut self, filters: &str) -> Result<&mut Self, ParseError> {
128+
let (directives, filter) = parse_spec(filters).ok()?;
129+
130+
self.filter = filter;
131+
132+
for directive in directives {
133+
self.insert_directive(directive);
134+
}
135+
Ok(self)
136+
}
137+
121138
/// Build a log filter.
122139
pub fn build(&mut self) -> Filter {
123140
assert!(!self.built, "attempt to re-use consumed builder");
@@ -241,6 +258,7 @@ impl fmt::Debug for Filter {
241258
#[cfg(test)]
242259
mod tests {
243260
use log::{Level, LevelFilter};
261+
use snapbox::{assert_data_eq, str};
244262

245263
use super::{enabled, Builder, Directive, Filter};
246264

@@ -455,6 +473,25 @@ mod tests {
455473
}
456474
}
457475

476+
#[test]
477+
fn try_parse_valid_filter() {
478+
let logger = Builder::new()
479+
.try_parse("info,crate1::mod1=warn")
480+
.expect("valid filter returned error")
481+
.build();
482+
assert!(enabled(&logger.directives, Level::Warn, "crate1::mod1"));
483+
assert!(enabled(&logger.directives, Level::Info, "crate2::mod2"));
484+
}
485+
486+
#[test]
487+
fn try_parse_invalid_filter() {
488+
let error = Builder::new().try_parse("info,crate1=invalid").unwrap_err();
489+
assert_data_eq!(
490+
error,
491+
str!["error parsing logger filter: invalid logging spec 'invalid'"]
492+
);
493+
}
494+
458495
#[test]
459496
fn match_full_path() {
460497
let logger = make_logger_filter(vec![

crates/env_filter/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,4 @@ use parser::parse_spec;
5656
pub use filter::Builder;
5757
pub use filter::Filter;
5858
pub use filtered_log::FilteredLog;
59+
pub use parser::ParseError;

crates/env_filter/src/parser.rs

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use log::LevelFilter;
2+
use std::error::Error;
3+
use std::fmt::{Display, Formatter};
24

35
use crate::Directive;
46
use crate::FilterOp;
@@ -22,8 +24,36 @@ impl ParseResult {
2224
fn add_error(&mut self, message: String) {
2325
self.errors.push(message);
2426
}
27+
28+
pub(crate) fn ok(self) -> Result<(Vec<Directive>, Option<FilterOp>), ParseError> {
29+
if self.errors.is_empty() {
30+
Ok((self.directives, self.filter))
31+
} else {
32+
Err(ParseError {
33+
details: self.errors,
34+
})
35+
}
36+
}
2537
}
2638

39+
/// Error during logger directive parsing process.
40+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
41+
pub struct ParseError {
42+
details: Vec<String>,
43+
}
44+
45+
impl Display for ParseError {
46+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
47+
write!(
48+
f,
49+
"error parsing logger filter: {}",
50+
self.details.join(", ")
51+
)
52+
}
53+
}
54+
55+
impl Error for ParseError {}
56+
2757
/// Parse a logging specification string (e.g: `crate1,crate2::mod3,crate3::x=error/foo`)
2858
/// and return a vector with log directives.
2959
pub(crate) fn parse_spec(spec: &str) -> ParseResult {
@@ -376,11 +406,117 @@ mod tests {
376406
let ParseResult {
377407
directives: dirs,
378408
filter,
379-
..
409+
errors,
380410
} = parse_spec("crate1/a*c");
381411
assert_eq!(dirs.len(), 1);
382412
assert_eq!(dirs[0].name, Some("crate1".to_owned()));
383413
assert_eq!(dirs[0].level, LevelFilter::max());
384414
assert!(filter.is_some() && filter.unwrap().to_string() == "a*c");
415+
assert!(errors.is_empty());
416+
}
417+
418+
#[test]
419+
fn parse_spec_with_multiple_filters() {
420+
let ParseResult {
421+
directives: dirs,
422+
filter,
423+
errors,
424+
} = parse_spec("debug/abc/a.c");
425+
assert!(dirs.is_empty());
426+
assert!(filter.is_none());
427+
428+
assert_eq!(errors.len(), 1);
429+
assert_data_eq!(
430+
&errors[0],
431+
str!["invalid logging spec 'debug/abc/a.c' (too many '/'s)"]
432+
);
433+
}
434+
435+
#[test]
436+
fn parse_spec_multiple_invalid_crates() {
437+
// test parse_spec with multiple = in specification
438+
let ParseResult {
439+
directives: dirs,
440+
filter,
441+
errors,
442+
} = parse_spec("crate1::mod1=warn=info,crate2=debug,crate3=error=error");
443+
444+
assert_eq!(dirs.len(), 1);
445+
assert_eq!(dirs[0].name, Some("crate2".to_owned()));
446+
assert_eq!(dirs[0].level, LevelFilter::Debug);
447+
assert!(filter.is_none());
448+
449+
assert_eq!(errors.len(), 2);
450+
assert_data_eq!(
451+
&errors[0],
452+
str!["invalid logging spec 'crate1::mod1=warn=info'"]
453+
);
454+
assert_data_eq!(
455+
&errors[1],
456+
str!["invalid logging spec 'crate3=error=error'"]
457+
);
458+
}
459+
460+
#[test]
461+
fn parse_spec_multiple_invalid_levels() {
462+
// test parse_spec with 'noNumber' as log level
463+
let ParseResult {
464+
directives: dirs,
465+
filter,
466+
errors,
467+
} = parse_spec("crate1::mod1=noNumber,crate2=debug,crate3=invalid");
468+
469+
assert_eq!(dirs.len(), 1);
470+
assert_eq!(dirs[0].name, Some("crate2".to_owned()));
471+
assert_eq!(dirs[0].level, LevelFilter::Debug);
472+
assert!(filter.is_none());
473+
474+
assert_eq!(errors.len(), 2);
475+
assert_data_eq!(&errors[0], str!["invalid logging spec 'noNumber'"]);
476+
assert_data_eq!(&errors[1], str!["invalid logging spec 'invalid'"]);
477+
}
478+
479+
#[test]
480+
fn parse_spec_invalid_crate_and_level() {
481+
// test parse_spec with 'noNumber' as log level
482+
let ParseResult {
483+
directives: dirs,
484+
filter,
485+
errors,
486+
} = parse_spec("crate1::mod1=debug=info,crate2=debug,crate3=invalid");
487+
488+
assert_eq!(dirs.len(), 1);
489+
assert_eq!(dirs[0].name, Some("crate2".to_owned()));
490+
assert_eq!(dirs[0].level, LevelFilter::Debug);
491+
assert!(filter.is_none());
492+
493+
assert_eq!(errors.len(), 2);
494+
assert_data_eq!(
495+
&errors[0],
496+
str!["invalid logging spec 'crate1::mod1=debug=info'"]
497+
);
498+
assert_data_eq!(&errors[1], str!["invalid logging spec 'invalid'"]);
499+
}
500+
501+
#[test]
502+
fn parse_error_message_single_error() {
503+
let error = parse_spec("crate1::mod1=debug=info,crate2=debug")
504+
.ok()
505+
.unwrap_err();
506+
assert_data_eq!(
507+
error,
508+
str!["error parsing logger filter: invalid logging spec 'crate1::mod1=debug=info'"]
509+
);
510+
}
511+
512+
#[test]
513+
fn parse_error_message_multiple_errors() {
514+
let error = parse_spec("crate1::mod1=debug=info,crate2=debug,crate3=invalid")
515+
.ok()
516+
.unwrap_err();
517+
assert_data_eq!(
518+
error,
519+
str!["error parsing logger filter: invalid logging spec 'crate1::mod1=debug=info', invalid logging spec 'invalid'"]
520+
);
385521
}
386522
}

0 commit comments

Comments
 (0)