use crate::ast::Predicate;
pub const MAX_DSL_DEPTH: usize = 32;
pub fn parse(expr: &str) -> Result<Predicate, ParseError> {
let trimmed = expr.trim();
parse_expr(trimmed, 0).map(|(p, _)| p)
}
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
#[error("unexpected end of input at: {0}")]
UnexpectedEof(String),
#[error("unexpected character '{0}' at position {1}")]
UnexpectedChar(char, usize),
#[error("unknown function: {0}")]
UnknownFunction(String),
#[error("argument count mismatch for {0}")]
ArgCount(String),
#[error("number parse error: {0}")]
NumberParse(String),
#[error("DSL recursion depth {depth} exceeds max {max}")]
DepthExceeded { depth: usize, max: usize },
}
fn parse_expr(s: &str, depth: usize) -> Result<(Predicate, &str), ParseError> {
if depth >= MAX_DSL_DEPTH {
return Err(ParseError::DepthExceeded {
depth,
max: MAX_DSL_DEPTH,
});
}
let s = s.trim();
let (name, rest) = parse_ident(s)?;
let rest = rest.trim_start();
let rest = eat(rest, '(')?;
let rest = rest.trim_start();
match name.as_str() {
"min_count" => {
let (attr, rest) = parse_string(rest)?;
let rest = eat(rest.trim_start(), ',')?;
let (count_str, rest) = parse_number(rest.trim_start())?;
let (_, rest) = parse_until_close(rest.trim_start())?;
let count: usize = count_str
.parse()
.map_err(|_| ParseError::NumberParse(count_str))?;
Ok((
Predicate::MinCount {
attribute: attr,
count,
},
rest,
))
}
"min_distinct" => {
let (attr, rest) = parse_string(rest)?;
let rest = eat(rest.trim_start(), ',')?;
let (count_str, rest) = parse_number(rest.trim_start())?;
let (_, rest) = parse_until_close(rest.trim_start())?;
let count: usize = count_str
.parse()
.map_err(|_| ParseError::NumberParse(count_str))?;
Ok((
Predicate::MinDistinct {
attribute: attr,
count,
},
rest,
))
}
"none" => {
let (attr, rest) = parse_string(rest)?;
let rest = parse_until_close(rest.trim_start())?.1;
Ok((Predicate::None { attribute: attr }, rest))
}
"any" => {
let (attr, rest) = parse_string(rest)?;
let rest = parse_until_close(rest.trim_start())?.1;
Ok((Predicate::Any { attribute: attr }, rest))
}
"all" => {
let (attr, rest) = parse_string(rest)?;
let rest = parse_until_close(rest.trim_start())?.1;
Ok((Predicate::All { attribute: attr }, rest))
}
"and" | "or" => {
let mut preds = Vec::new();
let mut s = rest;
loop {
let s2 = s.trim_start();
if s2.starts_with(')') {
break;
}
let (p, rest) = parse_expr(s2, depth + 1)?;
preds.push(p);
s = rest.trim_start();
if s.starts_with(',') {
s = &s[1..];
} else if s.starts_with(')') {
break;
} else {
return Err(ParseError::UnexpectedEof(s.into()));
}
}
let rest = eat(s, ')')?;
if name == "and" {
Ok((Predicate::And(preds), rest))
} else {
Ok((Predicate::Or(preds), rest))
}
}
"not" => {
let (inner, rest) = parse_expr(rest, depth + 1)?;
let rest = eat(rest.trim_start(), ')')?;
Ok((Predicate::Not(Box::new(inner)), rest))
}
other => Err(ParseError::UnknownFunction(other.into())),
}
}
fn parse_ident(s: &str) -> Result<(String, &str), ParseError> {
let mut chars = s.char_indices();
let mut end = 0;
for (i, c) in chars.by_ref() {
if c.is_alphanumeric() || c == '_' {
end = i + c.len_utf8();
} else {
break;
}
}
if end == 0 {
return Err(ParseError::UnexpectedEof(s.into()));
}
Ok((s[..end].to_string(), &s[end..]))
}
fn parse_string(s: &str) -> Result<(String, &str), ParseError> {
let s = eat(s, '"')?;
let end = s
.find('"')
.ok_or_else(|| ParseError::UnexpectedEof(s.into()))?;
Ok((s[..end].to_string(), &s[end + 1..]))
}
fn parse_number(s: &str) -> Result<(String, &str), ParseError> {
let end = s
.char_indices()
.take_while(|(_, c)| c.is_ascii_digit())
.last()
.map(|(i, c)| i + c.len_utf8())
.unwrap_or(0);
if end == 0 {
return Err(ParseError::NumberParse(s.into()));
}
Ok((s[..end].to_string(), &s[end..]))
}
fn eat(s: &str, ch: char) -> Result<&str, ParseError> {
let s = s.trim_start();
if s.starts_with(ch) {
Ok(&s[ch.len_utf8()..])
} else {
Err(ParseError::UnexpectedChar(
s.chars().next().unwrap_or(' '),
0,
))
}
}
fn parse_until_close(s: &str) -> Result<(String, &str), ParseError> {
let s = eat(s, ')')?;
Ok((String::new(), s))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_min_count() {
let p = parse(r#"min_count("role:director", 5)"#).unwrap();
match p {
Predicate::MinCount { attribute, count } => {
assert_eq!(attribute, "role:director");
assert_eq!(count, 5);
}
_ => panic!("wrong variant"),
}
}
#[test]
fn parses_any() {
let p = parse(r#"any("expertise")"#).unwrap();
assert!(matches!(p, Predicate::Any { .. }));
}
}
#[cfg(test)]
mod depth_tests {
use super::*;
#[test]
fn shallow_nesting_parses() {
let mut expr = String::from("any(\"x\")");
for _ in 0..5 {
expr = format!("not({expr})");
}
parse(&expr).expect("5-level not() should parse");
}
#[test]
fn deep_nesting_rejected() {
let mut expr = String::from("any(\"x\")");
for _ in 0..64 {
expr = format!("not({expr})");
}
let err = parse(&expr).expect_err("64-level not() should be rejected");
assert!(matches!(err, ParseError::DepthExceeded { .. }));
}
}