summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--rust/libnewsboat/src/filterparser.rs164
-rw-r--r--rust/libnewsboat/src/matcher.rs32
2 files changed, 134 insertions, 62 deletions
diff --git a/rust/libnewsboat/src/filterparser.rs b/rust/libnewsboat/src/filterparser.rs
index 0ed4feb8..99f310be 100644
--- a/rust/libnewsboat/src/filterparser.rs
+++ b/rust/libnewsboat/src/filterparser.rs
@@ -10,6 +10,8 @@ use nom::{
sequence::{delimited, separated_pair, terminated, tuple},
IResult, Offset, Parser,
};
+use once_cell::unsync::OnceCell;
+use regex_rs::Regex;
use std::vec::Vec;
use strprintf::fmt;
@@ -30,8 +32,72 @@ pub enum Operator {
}
/// Values that can be used on the right-hand side of comparisons.
-#[derive(Debug, Clone, PartialEq)]
-pub struct Value(pub String);
+pub struct Value {
+ literal: String,
+ regex: OnceCell<Result<Regex, String>>,
+}
+
+impl Value {
+ /// Construct a value from the parsed token.
+ fn new(literal: String) -> Self {
+ Self {
+ literal,
+ regex: OnceCell::new(),
+ }
+ }
+
+ /// Access the stored literal as a string.
+ pub fn literal(&self) -> &str {
+ &self.literal
+ }
+
+ /// The literal interpreted as a POSIX extended regular expression.
+ ///
+ /// When matching, case will be ignored, and no parenthesised sub-expressions will be
+ /// extracted.
+ ///
+ /// Returns `Ok` with a regex or an `Err` with an error message.
+ pub fn as_regex(&self) -> Result<&Regex, &str> {
+ let regex = self.regex.get_or_init(|| {
+ use regex_rs::CompFlags;
+
+ Regex::new(
+ &self.literal,
+ CompFlags::EXTENDED | CompFlags::IGNORE_CASE | CompFlags::NO_SUB,
+ )
+ });
+
+ match regex {
+ Ok(regex) => Ok(&regex),
+ Err(message) => Err(&message),
+ }
+ }
+}
+
+impl core::fmt::Debug for Value {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
+ f.debug_struct("Value")
+ .field("literal", &self.literal)
+ .finish()
+ }
+}
+
+impl Clone for Value {
+ fn clone(&self) -> Self {
+ Self {
+ literal: self.literal.clone(),
+ regex: OnceCell::new(),
+ }
+ }
+}
+
+impl PartialEq for Value {
+ fn eq(&self, other: &Value) -> bool {
+ self.literal == other.literal
+ }
+}
+
+impl Eq for Value {}
/// Parsed filter expression.
///
@@ -176,7 +242,7 @@ fn quoted_string<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str,
Ok((leftovers, String::from(chr)))
};
- map(alt((nonempty_string, empty_string)), Value)(input)
+ map(alt((nonempty_string, empty_string)), Value::new)(input)
}
fn number<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, &'a str, E> {
@@ -185,7 +251,7 @@ fn number<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, &'a st
fn range<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Value, E> {
separated_pair(number, tag(":"), number)(input)
- .map(|(leftovers, (a, b))| (leftovers, Value(format!("{}:{}", a, b))))
+ .map(|(leftovers, (a, b))| (leftovers, Value::new(format!("{}:{}", a, b))))
}
/// Skips zero or more space characters.
@@ -223,7 +289,11 @@ fn comparison<'a, E: ParseError<&'a str> + ExpectativeError<&'a str>>(
// especific error message when this parser fails.
let (leftovers, value) = expect(
Expected::Value,
- alt((quoted_string, range, map(number, |n| Value(n.to_string())))),
+ alt((
+ quoted_string,
+ range,
+ map(number, |n| Value::new(n.to_string())),
+ )),
)(input)?;
Ok((
@@ -445,7 +515,7 @@ mod tests {
let expected = Ok(Expression::Comparison {
attribute: "a".to_string(),
op: Operator::Equals,
- value: Value("abc".to_string()),
+ value: Value::new("abc".to_string()),
});
assert_eq!(internal_parse("a = \"abc\""), expected);
@@ -468,7 +538,7 @@ mod tests {
Ok(Expression::Comparison {
attribute: "attribute".to_string(),
op: Operator::Equals,
- value: Value("hello\0world".to_string()),
+ value: Value::new("hello\0world".to_string()),
})
);
}
@@ -478,7 +548,7 @@ mod tests {
let expected = Ok(Expression::Comparison {
attribute: "title".to_string(),
op: Operator::Equals,
- value: Value(String::new()),
+ value: Value::new(String::new()),
});
assert_eq!(internal_parse("title==\"\""), expected);
@@ -494,12 +564,12 @@ mod tests {
Box::new(Comparison {
attribute: "a".to_string(),
op: Operator::Equals,
- value: Value("42".to_string()),
+ value: Value::new("42".to_string()),
}),
Box::new(Comparison {
attribute: "y".to_string(),
op: Operator::Equals,
- value: Value("0".to_string()),
+ value: Value::new("0".to_string()),
}),
);
@@ -518,12 +588,12 @@ mod tests {
Box::new(Comparison {
attribute: "a".to_string(),
op: Operator::Equals,
- value: Value("42".to_string()),
+ value: Value::new("42".to_string()),
}),
Box::new(Comparison {
attribute: "y".to_string(),
op: Operator::Equals,
- value: Value("0".to_string()),
+ value: Value::new("0".to_string()),
}),
);
@@ -537,7 +607,7 @@ mod tests {
let expected = Comparison {
attribute: "array".to_string(),
op: Operator::Contains,
- value: Value("bar".to_string()),
+ value: Value::new("bar".to_string()),
};
assert_eq!(internal_parse("array # \"bar\""), Ok(expected.clone()));
@@ -584,12 +654,12 @@ mod tests {
Box::new(Comparison {
attribute: "x".to_string(),
op: Operator::Equals,
- value: Value("42".to_string())
+ value: Value::new("42".to_string())
}),
Box::new(Comparison {
attribute: "y".to_string(),
op: Operator::Equals,
- value: Value("0".to_string())
+ value: Value::new("0".to_string())
})
))
);
@@ -600,12 +670,12 @@ mod tests {
Box::new(Comparison {
attribute: "x".to_string(),
op: Operator::Equals,
- value: Value("42".to_string())
+ value: Value::new("42".to_string())
}),
Box::new(Comparison {
attribute: "y".to_string(),
op: Operator::Equals,
- value: Value("0".to_string())
+ value: Value::new("0".to_string())
})
))
);
@@ -616,12 +686,12 @@ mod tests {
Box::new(Comparison {
attribute: "x".to_string(),
op: Operator::Equals,
- value: Value("42".to_string())
+ value: Value::new("42".to_string())
}),
Box::new(Comparison {
attribute: "y".to_string(),
op: Operator::Equals,
- value: Value("42".to_string())
+ value: Value::new("42".to_string())
})
))
);
@@ -636,7 +706,7 @@ mod tests {
Ok(Comparison {
attribute: "a".to_string(),
op: Operator::Equals,
- value: Value("b".to_string())
+ value: Value::new("b".to_string())
})
);
@@ -645,7 +715,7 @@ mod tests {
Ok(Comparison {
attribute: "a".to_string(),
op: Operator::NotEquals,
- value: Value("b".to_string())
+ value: Value::new("b".to_string())
})
);
@@ -654,7 +724,7 @@ mod tests {
Ok(Comparison {
attribute: "a".to_string(),
op: Operator::RegexMatches,
- value: Value("b".to_string())
+ value: Value::new("b".to_string())
})
);
@@ -663,7 +733,7 @@ mod tests {
Ok(Comparison {
attribute: "a".to_string(),
op: Operator::NotRegexMatches,
- value: Value("b".to_string())
+ value: Value::new("b".to_string())
})
);
@@ -674,7 +744,7 @@ mod tests {
Ok(Comparison {
attribute: "a".to_string(),
op: Operator::LessThan,
- value: Value("b".to_string())
+ value: Value::new("b".to_string())
})
);
@@ -683,7 +753,7 @@ mod tests {
Ok(Comparison {
attribute: "a".to_string(),
op: Operator::LessThanOrEquals,
- value: Value("b".to_string())
+ value: Value::new("b".to_string())
})
);
@@ -692,7 +762,7 @@ mod tests {
Ok(Comparison {
attribute: "a".to_string(),
op: Operator::GreaterThan,
- value: Value("abc".to_string())
+ value: Value::new("abc".to_string())
})
);
@@ -701,7 +771,7 @@ mod tests {
Ok(Comparison {
attribute: "a".to_string(),
op: Operator::Equals,
- value: Value("abc".to_string())
+ value: Value::new("abc".to_string())
})
);
@@ -710,7 +780,7 @@ mod tests {
Ok(Comparison {
attribute: "a".to_string(),
op: Operator::GreaterThanOrEquals,
- value: Value("3".to_string())
+ value: Value::new("3".to_string())
})
);
@@ -719,7 +789,7 @@ mod tests {
Ok(Comparison {
attribute: "some_value".to_string(),
op: Operator::Between,
- value: Value("0:-1".to_string())
+ value: Value::new("0:-1".to_string())
})
);
@@ -728,7 +798,7 @@ mod tests {
Ok(Comparison {
attribute: "other_string".to_string(),
op: Operator::Between,
- value: Value("impossible".to_string())
+ value: Value::new("impossible".to_string())
})
);
@@ -737,7 +807,7 @@ mod tests {
Ok(Comparison {
attribute: "array".to_string(),
op: Operator::Contains,
- value: Value("name".to_string())
+ value: Value::new("name".to_string())
})
);
@@ -746,7 +816,7 @@ mod tests {
Ok(Comparison {
attribute: "answers".to_string(),
op: Operator::NotContains,
- value: Value("42".to_string())
+ value: Value::new("42".to_string())
})
);
@@ -755,7 +825,7 @@ mod tests {
Ok(Comparison {
attribute: "author".to_string(),
op: Operator::RegexMatches,
- value: Value("\\s*Doe$".to_string())
+ value: Value::new("\\s*Doe$".to_string())
})
);
}
@@ -768,18 +838,18 @@ mod tests {
Box::new(Comparison {
attribute: "a".to_string(),
op: Operator::Equals,
- value: Value("b".to_string())
+ value: Value::new("b".to_string())
}),
Box::new(Or(
Box::new(Comparison {
attribute: "b".to_string(),
op: Operator::Equals,
- value: Value("c".to_string())
+ value: Value::new("c".to_string())
}),
Box::new(Comparison {
attribute: "c".to_string(),
op: Operator::Equals,
- value: Value("d".to_string())
+ value: Value::new("d".to_string())
}),
))
)
@@ -791,18 +861,18 @@ mod tests {
Box::new(Comparison {
attribute: "a".to_string(),
op: Operator::Equals,
- value: Value("b".to_string())
+ value: Value::new("b".to_string())
}),
Box::new(And(
Box::new(Comparison {
attribute: "b".to_string(),
op: Operator::Equals,
- value: Value("c".to_string())
+ value: Value::new("c".to_string())
}),
Box::new(Comparison {
attribute: "c".to_string(),
op: Operator::Equals,
- value: Value("d".to_string())
+ value: Value::new("d".to_string())
}),
))
)
@@ -815,18 +885,18 @@ mod tests {
Box::new(Comparison {
attribute: "a".to_string(),
op: Operator::Equals,
- value: Value("b".to_string())
+ value: Value::new("b".to_string())
}),
Box::new(Comparison {
attribute: "b".to_string(),
op: Operator::Equals,
- value: Value("c".to_string())
+ value: Value::new("c".to_string())
}),
)),
Box::new(Comparison {
attribute: "c".to_string(),
op: Operator::Equals,
- value: Value("d".to_string())
+ value: Value::new("d".to_string())
})
)
);
@@ -843,7 +913,7 @@ mod tests {
Ok(Comparison {
attribute: "value".to_string(),
op: Operator::Between,
- value: Value("-100:-1".to_string())
+ value: Value::new("-100:-1".to_string())
})
);
@@ -852,7 +922,7 @@ mod tests {
Ok(Comparison {
attribute: "value".to_string(),
op: Operator::Between,
- value: Value("-100:100500".to_string())
+ value: Value::new("-100:100500".to_string())
})
);
@@ -861,7 +931,7 @@ mod tests {
Ok(Comparison {
attribute: "value".to_string(),
op: Operator::Between,
- value: Value("123:-10".to_string())
+ value: Value::new("123:-10".to_string())
})
);
}
@@ -880,7 +950,7 @@ mod tests {
Ok(Comparison {
attribute: "a".to_string(),
op: Operator::NotEquals,
- value: Value("b".to_string())
+ value: Value::new("b".to_string())
})
);
}
@@ -892,7 +962,7 @@ mod tests {
Ok(Comparison {
attribute: "a".to_string(),
op: Operator::NotEquals,
- value: Value("b".to_string())
+ value: Value::new("b".to_string())
})
);
}
diff --git a/rust/libnewsboat/src/matcher.rs b/rust/libnewsboat/src/matcher.rs
index fcabe5bf..15a1975a 100644
--- a/rust/libnewsboat/src/matcher.rs
+++ b/rust/libnewsboat/src/matcher.rs
@@ -3,7 +3,6 @@
use crate::filterparser::{self, Expression, Expression::*, Operator, Value};
use crate::matchable::Matchable;
use crate::matchererror::MatcherError;
-use regex_rs::{CompFlags, MatchFlags, Regex};
/// Checks if given filter expression is true for a given feed or article.
///
@@ -43,16 +42,15 @@ impl Matcher {
impl Operator {
fn apply(&self, attr: &str, value: &Value) -> Result<bool, MatcherError> {
match self {
- Operator::Equals => Ok(attr == value.0),
+ Operator::Equals => Ok(attr == value.literal()),
Operator::NotEquals => Operator::Equals.apply(attr, value).map(|result| !result),
- Operator::RegexMatches => match Regex::new(
- &value.0,
- CompFlags::EXTENDED | CompFlags::IGNORE_CASE | CompFlags::NO_SUB,
- ) {
+ Operator::RegexMatches => match value.as_regex() {
Ok(regex) => {
let mut result = false;
let max_matches = 1;
- if let Ok(matches) = regex.matches(attr, max_matches, MatchFlags::empty()) {
+ if let Ok(matches) =
+ regex.matches(attr, max_matches, regex_rs::MatchFlags::empty())
+ {
// Ok with non-empty Vec inside means a match was found
result = !matches.is_empty();
}
@@ -60,19 +58,23 @@ impl Operator {
}
Err(errmsg) => Err(MatcherError::InvalidRegex {
- regex: value.0.clone(),
- errmsg,
+ regex: value.literal().to_string(),
+ errmsg: errmsg.to_string(),
}),
},
Operator::NotRegexMatches => Operator::RegexMatches
.apply(attr, value)
.map(|result| !result),
- Operator::LessThan => Ok(string_to_num(attr) < string_to_num(&value.0)),
- Operator::GreaterThan => Ok(dbg!(string_to_num(attr)) > dbg!(string_to_num(&value.0))),
- Operator::LessThanOrEquals => Ok(string_to_num(attr) <= string_to_num(&value.0)),
- Operator::GreaterThanOrEquals => Ok(string_to_num(attr) >= string_to_num(&value.0)),
+ Operator::LessThan => Ok(string_to_num(attr) < string_to_num(&value.literal())),
+ Operator::GreaterThan => Ok(string_to_num(attr) > string_to_num(&value.literal())),
+ Operator::LessThanOrEquals => {
+ Ok(string_to_num(attr) <= string_to_num(&value.literal()))
+ }
+ Operator::GreaterThanOrEquals => {
+ Ok(string_to_num(attr) >= string_to_num(&value.literal()))
+ }
Operator::Between => {
- let fields = value.0.split(':').collect::<Vec<_>>();
+ let fields = value.literal().split(':').collect::<Vec<_>>();
if fields.len() != 2 {
return Ok(false);
}
@@ -87,7 +89,7 @@ impl Operator {
}
Operator::Contains => {
for token in attr.split(' ') {
- if token == value.0 {
+ if token == value.literal() {
return Ok(true);
}
}