use nom::{
IResult, Parser,
branch::alt,
bytes::complete::{tag, tag_no_case},
character::complete::{char, satisfy},
combinator::{map, not, opt, peek, value},
multi::separated_list0,
sequence::delimited,
};
use crate::ast::{
BinaryModifier, BinaryOp, GroupModifier, GroupSide, VectorMatching, VectorMatchingOp,
};
use crate::lexer::{identifier::clause_label_name, whitespace::ws_opt};
fn word_boundary(input: &str) -> IResult<&str, ()> {
not(peek(satisfy(|c| c.is_alphanumeric() || c == '_'))).parse(input)
}
pub fn binary_op(input: &str) -> IResult<&str, BinaryOp> {
alt((
value(BinaryOp::Eq, tag("==")),
value(BinaryOp::Ne, tag("!=")),
value(BinaryOp::Le, tag("<=")),
value(BinaryOp::Ge, tag(">=")),
value(BinaryOp::Add, tag("+")),
value(BinaryOp::Sub, tag("-")),
value(BinaryOp::Mul, tag("*")),
value(BinaryOp::Div, tag("/")),
value(BinaryOp::Mod, tag("%")),
value(BinaryOp::Pow, tag("^")),
value(BinaryOp::Lt, tag("<")),
value(BinaryOp::Gt, tag(">")),
keyword_binary_op,
))
.parse(input)
}
fn keyword_binary_op(input: &str) -> IResult<&str, BinaryOp> {
(
alt((
value(BinaryOp::And, tag_no_case("and")),
value(BinaryOp::Or, tag_no_case("or")),
value(BinaryOp::Unless, tag_no_case("unless")),
value(BinaryOp::Atan2, tag_no_case("atan2")),
)),
word_boundary,
)
.map(|(op, _)| op)
.parse(input)
}
fn bool_modifier(input: &str) -> IResult<&str, bool> {
(tag_no_case("bool"), word_boundary)
.map(|_| true)
.parse(input)
}
fn vector_matching_op(input: &str) -> IResult<&str, VectorMatchingOp> {
(
alt((
value(VectorMatchingOp::On, tag_no_case("on")),
value(VectorMatchingOp::Ignoring, tag_no_case("ignoring")),
)),
word_boundary,
)
.map(|(op, _)| op)
.parse(input)
}
fn label_list(input: &str) -> IResult<&str, Vec<String>> {
delimited(
(char('('), ws_opt),
separated_list0(
delimited(ws_opt, char(','), ws_opt),
map(clause_label_name, |s| s.to_string()),
),
(ws_opt, char(')')),
)
.parse(input)
}
fn group_modifier(input: &str) -> IResult<&str, GroupModifier> {
(
alt((
value(GroupSide::Left, tag_no_case("group_left")),
value(GroupSide::Right, tag_no_case("group_right")),
)),
word_boundary,
ws_opt,
opt(label_list),
)
.map(|(side, _, _, labels)| GroupModifier {
side,
labels: labels.unwrap_or_default(),
})
.parse(input)
}
fn vector_matching(input: &str) -> IResult<&str, VectorMatching> {
(
vector_matching_op,
ws_opt,
label_list,
ws_opt,
opt(group_modifier),
)
.map(|(op, _, labels, _, group)| VectorMatching { op, labels, group })
.parse(input)
}
pub(crate) fn binary_modifier(input: &str) -> IResult<&str, BinaryModifier> {
let (rest, (_, return_bool, _, matching)) =
(ws_opt, opt(bool_modifier), ws_opt, opt(vector_matching)).parse(input)?;
if return_bool.is_none() && matching.is_none() {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)));
}
Ok((
rest,
BinaryModifier {
return_bool: return_bool.unwrap_or(false),
matching,
},
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_binary_op_arithmetic() {
assert_eq!(binary_op("+").unwrap().1, BinaryOp::Add);
assert_eq!(binary_op("-").unwrap().1, BinaryOp::Sub);
assert_eq!(binary_op("*").unwrap().1, BinaryOp::Mul);
assert_eq!(binary_op("/").unwrap().1, BinaryOp::Div);
assert_eq!(binary_op("%").unwrap().1, BinaryOp::Mod);
assert_eq!(binary_op("^").unwrap().1, BinaryOp::Pow);
}
#[test]
fn test_binary_op_comparison() {
assert_eq!(binary_op("==").unwrap().1, BinaryOp::Eq);
assert_eq!(binary_op("!=").unwrap().1, BinaryOp::Ne);
assert_eq!(binary_op("<").unwrap().1, BinaryOp::Lt);
assert_eq!(binary_op("<=").unwrap().1, BinaryOp::Le);
assert_eq!(binary_op(">").unwrap().1, BinaryOp::Gt);
assert_eq!(binary_op(">=").unwrap().1, BinaryOp::Ge);
}
#[test]
fn test_binary_op_keywords() {
assert_eq!(binary_op("and").unwrap().1, BinaryOp::And);
assert_eq!(binary_op("AND").unwrap().1, BinaryOp::And);
assert_eq!(binary_op("or").unwrap().1, BinaryOp::Or);
assert_eq!(binary_op("OR").unwrap().1, BinaryOp::Or);
assert_eq!(binary_op("unless").unwrap().1, BinaryOp::Unless);
assert_eq!(binary_op("UNLESS").unwrap().1, BinaryOp::Unless);
assert_eq!(binary_op("atan2").unwrap().1, BinaryOp::Atan2);
assert_eq!(binary_op("ATAN2").unwrap().1, BinaryOp::Atan2);
}
#[test]
fn test_binary_op_word_boundary() {
assert!(binary_op("andy").is_err());
assert!(binary_op("orange").is_err());
assert!(binary_op("atan2x").is_err());
}
#[test]
fn test_binary_op_with_remaining() {
let (rest, op) = binary_op("+ foo").unwrap();
assert_eq!(op, BinaryOp::Add);
assert_eq!(rest, " foo");
let (rest, op) = binary_op("and bar").unwrap();
assert_eq!(op, BinaryOp::And);
assert_eq!(rest, " bar");
}
#[test]
fn test_vector_matching_on() {
let (rest, vm) = vector_matching("on(job, instance)").unwrap();
assert!(rest.is_empty());
assert_eq!(vm.op, VectorMatchingOp::On);
assert_eq!(vm.labels, vec!["job", "instance"]);
assert!(vm.group.is_none());
}
#[test]
fn test_vector_matching_ignoring() {
let (rest, vm) = vector_matching("ignoring(instance)").unwrap();
assert!(rest.is_empty());
assert_eq!(vm.op, VectorMatchingOp::Ignoring);
assert_eq!(vm.labels, vec!["instance"]);
}
#[test]
fn test_vector_matching_empty() {
let (rest, vm) = vector_matching("on()").unwrap();
assert!(rest.is_empty());
assert_eq!(vm.op, VectorMatchingOp::On);
assert!(vm.labels.is_empty());
}
#[test]
fn test_vector_matching_with_group_left() {
let (rest, vm) = vector_matching("on(job) group_left").unwrap();
assert!(rest.is_empty());
assert_eq!(vm.op, VectorMatchingOp::On);
let group = vm.group.unwrap();
assert_eq!(group.side, GroupSide::Left);
assert!(group.labels.is_empty());
}
#[test]
fn test_vector_matching_with_group_right_labels() {
let (rest, vm) = vector_matching("ignoring(instance) group_right(job)").unwrap();
assert!(rest.is_empty());
assert_eq!(vm.op, VectorMatchingOp::Ignoring);
let group = vm.group.unwrap();
assert_eq!(group.side, GroupSide::Right);
assert_eq!(group.labels, vec!["job"]);
}
#[test]
fn test_vector_matching_case_insensitive() {
let (_, vm) = vector_matching("ON(job)").unwrap();
assert_eq!(vm.op, VectorMatchingOp::On);
let (_, vm) = vector_matching("IGNORING(job)").unwrap();
assert_eq!(vm.op, VectorMatchingOp::Ignoring);
let (_, vm) = vector_matching("on(job) GROUP_LEFT").unwrap();
assert!(vm.group.is_some());
}
#[test]
fn test_binary_modifier_bool_only() {
let (rest, m) = binary_modifier(" bool").unwrap();
assert!(rest.is_empty() || rest.chars().all(|c| c.is_whitespace()));
assert!(m.return_bool);
assert!(m.matching.is_none());
}
#[test]
fn test_binary_modifier_matching_only() {
let (rest, m) = binary_modifier(" on(job)").unwrap();
assert!(rest.is_empty());
assert!(!m.return_bool);
assert!(m.matching.is_some());
}
#[test]
fn test_binary_modifier_bool_and_matching() {
let (rest, m) = binary_modifier(" bool on(job)").unwrap();
assert!(rest.is_empty());
assert!(m.return_bool);
assert!(m.matching.is_some());
}
#[test]
fn test_binary_modifier_fails_on_empty() {
assert!(binary_modifier("foo").is_err());
}
#[test]
fn test_vector_matching_display() {
let vm = VectorMatching {
op: VectorMatchingOp::On,
labels: vec!["job".to_string()],
group: None,
};
assert_eq!(vm.to_string(), "on (job)");
let vm = VectorMatching {
op: VectorMatchingOp::Ignoring,
labels: vec!["job".to_string(), "instance".to_string()],
group: Some(GroupModifier {
side: GroupSide::Left,
labels: vec![],
}),
};
assert_eq!(vm.to_string(), "ignoring (job, instance) group_left");
}
#[test]
fn test_binary_modifier_display() {
let m = BinaryModifier {
return_bool: true,
matching: None,
};
assert_eq!(m.to_string(), "bool");
let m = BinaryModifier {
return_bool: false,
matching: Some(VectorMatching {
op: VectorMatchingOp::On,
labels: vec!["job".to_string()],
group: None,
}),
};
assert_eq!(m.to_string(), "on (job)");
}
}