use std::str::FromStr;
use nom::{
branch::alt,
bytes::complete::{tag, take_while1},
character::complete::{char, digit1, multispace0, one_of},
combinator::{eof, map, map_opt, opt, recognize},
error::ParseError,
multi::fold_many0,
sequence::{delimited, preceded, tuple},
IResult,
};
#[derive(Debug, Clone, PartialEq)]
pub enum Filter {
And(Box<Filter>, Box<Filter>),
Or(Box<Filter>, Box<Filter>),
Not(Box<Filter>),
LessThan {
tag: String,
threshold: f32,
inclusive: bool,
},
}
fn ws<'a, F: 'a, O, E: ParseError<&'a str>>(
inner: F,
) -> impl FnMut(&'a str) -> IResult<&'a str, O, E>
where
F: FnMut(&'a str) -> IResult<&'a str, O, E>,
{
delimited(multispace0, inner, multispace0)
}
fn tag_name(i: &str) -> IResult<&str, String> {
map(
take_while1(|c: char| c.is_ascii_alphabetic() || c == '_'),
ToOwned::to_owned,
)(i)
}
fn float(input: &str) -> IResult<&str, &str> {
alt((
recognize(tuple((
char('.'),
digit1,
opt(tuple((one_of("eE"), opt(one_of("+-")), digit1))),
))), recognize(tuple((
digit1,
opt(preceded(char('.'), digit1)),
one_of("eE"),
opt(one_of("+-")),
digit1,
))), recognize(tuple((digit1, char('.'), opt(digit1)))),
))(input)
}
fn threshold(i: &str) -> IResult<&str, f32> {
map_opt(float, |n: &str| match n.parse::<f32>() {
Ok(n) if n >= 0.0 && n <= 1.0 => Some(n),
_ => None,
})(i)
}
fn filter3(i: &str) -> IResult<&str, Filter> {
println!("filter3 {:?}", i);
alt((map(
tuple((
tag_name,
ws(alt((tag("<="), tag("<"), tag(">="), tag(">"), tag("=")))),
threshold,
)),
|(tag, op, threshold)| match op {
"<" => Filter::LessThan {
tag,
threshold,
inclusive: false,
},
"<=" => Filter::LessThan {
tag,
threshold,
inclusive: true,
},
">" => Filter::Not(Box::new(Filter::LessThan {
tag,
threshold,
inclusive: true,
})),
">=" => Filter::Not(Box::new(Filter::LessThan {
tag,
threshold,
inclusive: false,
})),
"=" => Filter::And(
Box::new(Filter::LessThan {
tag: tag.clone(),
threshold,
inclusive: true,
}),
Box::new(Filter::Not(Box::new(Filter::LessThan {
tag,
threshold,
inclusive: false,
}))),
),
_ => unreachable!(),
},
),))(i)
}
fn filter2(i: &str) -> IResult<&str, Filter> {
println!("filter2 {:?}", i);
let (i, neg) = opt(ws(char('!')))(i)?;
let (i, filter) = filter3(i)?;
if neg.is_some() {
Ok((i, Filter::Not(Box::new(filter))))
} else {
Ok((i, filter))
}
}
fn filter1(i: &str) -> IResult<&str, Filter> {
println!("filter1 {:?}", i);
let (i, first) = filter2(i)?;
fold_many0(
preceded(ws(tag("&")), filter2),
move || first.clone(),
|lhs: Filter, rhs: Filter| Filter::And(Box::new(lhs), Box::new(rhs)),
)(i)
}
fn filter0(i: &str) -> IResult<&str, Filter> {
println!("filter {:?}", i);
let (i, first) = filter1(i)?;
fold_many0(
preceded(ws(tag("|")), filter1),
move || first.clone(),
|lhs: Filter, rhs: Filter| Filter::Or(Box::new(lhs), Box::new(rhs)),
)(i)
}
fn filter(i: &str) -> IResult<&str, Filter> {
alt((
filter0,
map(tag(""), |_| Filter::LessThan {
tag: String::from("a"),
threshold: 1.0,
inclusive: true,
}),
))(i)
}
impl FromStr for Filter {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let s = tuple((filter, eof))(s)
.map_err(|e| anyhow::anyhow!("failed to parse: {:?}", e))?
.1;
Ok(s.0)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_empty() {
assert_eq!(
Filter::from_str("").unwrap(),
Filter::LessThan {
tag: String::from("a"),
threshold: 1.0,
inclusive: true,
}
);
}
#[test]
fn test_conditional() {
assert_eq!(
Filter::from_str("foo > 0.5").unwrap(),
Filter::Not(Box::new(Filter::LessThan {
tag: String::from("foo"),
threshold: 0.5,
inclusive: true,
}))
);
}
#[test]
fn test_complex() {
assert_eq!(
Filter::from_str("foo > 0.5 & bar < 0.2 | !foo < 0.9 | baz = 0.3").unwrap(),
Filter::Or(
Box::new(Filter::Or(
Box::new(Filter::And(
Box::new(Filter::Not(Box::new(Filter::LessThan {
tag: String::from("foo"),
threshold: 0.5,
inclusive: true
}))),
Box::new(Filter::LessThan {
tag: String::from("bar"),
threshold: 0.2,
inclusive: false
})
)),
Box::new(Filter::Not(Box::new(Filter::LessThan {
tag: String::from("foo"),
threshold: 0.9,
inclusive: false
}))),
)),
Box::new(Filter::And(
Box::new(Filter::LessThan {
tag: String::from("baz"),
threshold: 0.3,
inclusive: true
}),
Box::new(Filter::Not(Box::new(Filter::LessThan {
tag: String::from("baz"),
threshold: 0.3,
inclusive: false
})))
)),
),
);
}
}