Skip to main content

etdl_parser/
ecel.rs

1use nom::{
2    branch::alt,
3    bytes::complete::{tag, take_while1},
4    character::complete::{alpha1, alphanumeric1, char, digit1, multispace0},
5    combinator::{map, opt, recognize, value},
6    multi::{many0, separated_list0},
7    sequence::{delimited, pair, preceded},
8    IResult,
9};
10use serde::{Deserialize, Serialize};
11
12/// Conformance floor for the total operand count in one `condition-expr`
13/// (spec §6.2): a Conforming Parser MUST accept at least this many. This is
14/// the actual limit this implementation enforces (well above the floor);
15/// see `count_operands` in `etdl-compiler`'s typeck module (rule V-206).
16pub const MAX_CONDITION_OPERANDS: usize = 64;
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub enum Condition {
20    Default,
21    Expr(BoolExpr),
22}
23
24/// A boolean expression: any combination of `comparison`/`quantifier-expr`/
25/// `defined-expr` joined by `&&`, `||`, and unary `!` (spec §6.2). A bare
26/// `Comparison` (no combinator) is the pre-existing, common case.
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub enum BoolExpr {
29    And(Box<BoolExpr>, Box<BoolExpr>),
30    Or(Box<BoolExpr>, Box<BoolExpr>),
31    Not(Box<BoolExpr>),
32    Comparison(Comparison),
33    Quantifier(QuantifierExpr),
34    Defined(PathExpr),
35}
36
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
38pub struct Comparison {
39    pub left: Operand,
40    pub op: Comparator,
41    pub right: Operand,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45pub enum QuantifierKind {
46    Any,
47    All,
48}
49
50/// `quantifier "(" path-expr "," comparison ")"` (spec §6.2, §6.4). The
51/// inner `comparison` is deliberately a plain `Comparison`, not a
52/// `BoolExpr` — a quantifier's inner test MUST NOT contain `&&`/`||`/`!`
53/// (spec §6.4), so this can't nest arbitrary boolean expressions.
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub struct QuantifierExpr {
56    pub kind: QuantifierKind,
57    pub path: PathExpr,
58    pub comparison: Comparison,
59}
60
61/// An `operand` (spec §6.2): either a `value-expr` (a path, number,
62/// arithmetic expression, or built-in function call) or a non-numeric
63/// `literal` (string/bool/null/array). A bare numeric literal always
64/// parses as `Value(ValueExpr::Number(_))`, never `Literal(Literal::Number(_))`
65/// — `value-expr` is tried first in the grammar's ordered choice.
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67pub enum Operand {
68    Value(ValueExpr),
69    Literal(Literal),
70}
71
72/// `value-expr` (spec §6.2): a `path-expr`/`number`/`func-call`, optionally
73/// combined with `+`/`-`/`*`/`/`. A bare `Path`/`Number`, with no
74/// arithmetic operator, is the pre-existing, common case.
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
76pub enum ValueExpr {
77    Path(PathExpr),
78    Number(f64),
79    Call(FuncName, Box<ValueExpr>),
80    Add(Box<ValueExpr>, Box<ValueExpr>),
81    Sub(Box<ValueExpr>, Box<ValueExpr>),
82    Mul(Box<ValueExpr>, Box<ValueExpr>),
83    Div(Box<ValueExpr>, Box<ValueExpr>),
84}
85
86/// The fixed, non-extensible built-in function set (spec §6.5.1) — not a
87/// general function-call mechanism.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89pub enum FuncName {
90    Length,
91    Abs,
92    Lower,
93    Upper,
94}
95
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
97pub struct PathExpr {
98    pub segments: Vec<PathSegment>,
99}
100
101impl PathExpr {
102    pub fn new(segments: Vec<PathSegment>) -> Self {
103        PathExpr { segments }
104    }
105}
106
107#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
108pub enum PathSegment {
109    Field(String),
110    Wildcard,
111    Index(usize),
112    QuotedKey(String),
113}
114
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
116pub enum Comparator {
117    Eq,
118    Neq,
119    Gte,
120    Lte,
121    Gt,
122    Lt,
123    In,
124    Matches,
125}
126
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128pub enum Literal {
129    Number(f64),
130    String(String),
131    Bool(bool),
132    Null,
133    Array(Vec<Literal>),
134}
135
136pub fn parse_condition(input: &str) -> Result<Condition, String> {
137    if input.trim() == "default" {
138        return Ok(Condition::Default);
139    }
140
141    match parse_bool_expr(input) {
142        Ok((remaining, expr)) => {
143            if remaining.trim().is_empty() {
144                Ok(Condition::Expr(expr))
145            } else {
146                Err(format!(
147                    "trailing content in condition expression: '{}'",
148                    remaining
149                ))
150            }
151        }
152        Err(e) => Err(format!("failed to parse condition expression: {}", e)),
153    }
154}
155
156// --- bool-expr / bool-term / bool-factor / bool-atom (standard precedence
157// climbing: `!` binds tightest, then `&&`, then `||`; spec §6.2, §6.5). ---
158
159fn parse_bool_expr(input: &str) -> IResult<&str, BoolExpr> {
160    let (input, first) = parse_bool_term(input)?;
161    let (input, rest) = many0(preceded(
162        delimited(multispace0, tag("||"), multispace0),
163        parse_bool_term,
164    ))(input)?;
165    Ok((
166        input,
167        rest.into_iter()
168            .fold(first, |acc, next| BoolExpr::Or(Box::new(acc), Box::new(next))),
169    ))
170}
171
172fn parse_bool_term(input: &str) -> IResult<&str, BoolExpr> {
173    let (input, first) = parse_bool_factor(input)?;
174    let (input, rest) = many0(preceded(
175        delimited(multispace0, tag("&&"), multispace0),
176        parse_bool_factor,
177    ))(input)?;
178    Ok((
179        input,
180        rest.into_iter()
181            .fold(first, |acc, next| BoolExpr::And(Box::new(acc), Box::new(next))),
182    ))
183}
184
185fn parse_bool_factor(input: &str) -> IResult<&str, BoolExpr> {
186    alt((
187        map(
188            preceded(pair(char('!'), multispace0), parse_bool_atom),
189            |e| BoolExpr::Not(Box::new(e)),
190        ),
191        parse_bool_atom,
192    ))(input)
193}
194
195fn parse_bool_atom(input: &str) -> IResult<&str, BoolExpr> {
196    alt((
197        map(parse_quantifier_expr, BoolExpr::Quantifier),
198        map(parse_defined_expr, BoolExpr::Defined),
199        map(parse_comparison, BoolExpr::Comparison),
200        delimited(
201            pair(char('('), multispace0),
202            parse_bool_expr,
203            pair(multispace0, char(')')),
204        ),
205    ))(input)
206}
207
208fn parse_quantifier_expr(input: &str) -> IResult<&str, QuantifierExpr> {
209    let (input, kind) = alt((
210        value(QuantifierKind::Any, tag("any")),
211        value(QuantifierKind::All, tag("all")),
212    ))(input)?;
213    let (input, _) = char('(')(input)?;
214    let (input, _) = multispace0(input)?;
215    let (input, path) = parse_path_expr(input)?;
216    let (input, _) = multispace0(input)?;
217    let (input, _) = char(',')(input)?;
218    let (input, _) = multispace0(input)?;
219    let (input, comparison) = parse_comparison(input)?;
220    let (input, _) = multispace0(input)?;
221    let (input, _) = char(')')(input)?;
222    Ok((
223        input,
224        QuantifierExpr {
225            kind,
226            path,
227            comparison,
228        },
229    ))
230}
231
232fn parse_defined_expr(input: &str) -> IResult<&str, PathExpr> {
233    let (input, _) = tag("defined")(input)?;
234    let (input, _) = char('(')(input)?;
235    let (input, _) = multispace0(input)?;
236    let (input, path) = parse_path_expr(input)?;
237    let (input, _) = multispace0(input)?;
238    let (input, _) = char(')')(input)?;
239    Ok((input, path))
240}
241
242fn parse_comparison(input: &str) -> IResult<&str, Comparison> {
243    let (input, left) = parse_operand(input)?;
244    let (input, _) = multispace0(input)?;
245    let (input, op) = parse_comparator(input)?;
246    let (input, _) = multispace0(input)?;
247    let (input, right) = parse_operand(input)?;
248    Ok((input, Comparison { left, op, right }))
249}
250
251fn parse_operand(input: &str) -> IResult<&str, Operand> {
252    alt((
253        map(parse_value_expr, Operand::Value),
254        map(parse_literal, Operand::Literal),
255    ))(input)
256}
257
258// --- value-expr / value-term / value-atom (spec §6.2, §6.5: `*`/`/` bind
259// tighter than `+`/`-`). ---
260
261fn parse_value_expr(input: &str) -> IResult<&str, ValueExpr> {
262    let (input, first) = parse_value_term(input)?;
263    let (input, rest) = many0(pair(
264        delimited(multispace0, alt((char('+'), char('-'))), multispace0),
265        parse_value_term,
266    ))(input)?;
267    Ok((
268        input,
269        rest.into_iter().fold(first, |acc, (op, next)| match op {
270            '+' => ValueExpr::Add(Box::new(acc), Box::new(next)),
271            _ => ValueExpr::Sub(Box::new(acc), Box::new(next)),
272        }),
273    ))
274}
275
276fn parse_value_term(input: &str) -> IResult<&str, ValueExpr> {
277    let (input, first) = parse_value_atom(input)?;
278    let (input, rest) = many0(pair(
279        delimited(multispace0, alt((char('*'), char('/'))), multispace0),
280        parse_value_atom,
281    ))(input)?;
282    Ok((
283        input,
284        rest.into_iter().fold(first, |acc, (op, next)| match op {
285            '*' => ValueExpr::Mul(Box::new(acc), Box::new(next)),
286            _ => ValueExpr::Div(Box::new(acc), Box::new(next)),
287        }),
288    ))
289}
290
291fn parse_value_atom(input: &str) -> IResult<&str, ValueExpr> {
292    alt((
293        parse_func_call,
294        map(parse_path_expr, ValueExpr::Path),
295        map(parse_number, ValueExpr::Number),
296        delimited(
297            pair(char('('), multispace0),
298            parse_value_expr,
299            pair(multispace0, char(')')),
300        ),
301    ))(input)
302}
303
304fn parse_func_call(input: &str) -> IResult<&str, ValueExpr> {
305    let (input, name) = alt((
306        value(FuncName::Length, tag("length")),
307        value(FuncName::Abs, tag("abs")),
308        value(FuncName::Lower, tag("lower")),
309        value(FuncName::Upper, tag("upper")),
310    ))(input)?;
311    let (input, _) = char('(')(input)?;
312    let (input, _) = multispace0(input)?;
313    let (input, arg) = parse_value_expr(input)?;
314    let (input, _) = multispace0(input)?;
315    let (input, _) = char(')')(input)?;
316    Ok((input, ValueExpr::Call(name, Box::new(arg))))
317}
318
319fn parse_path_expr(input: &str) -> IResult<&str, PathExpr> {
320    let (input, root) = parse_root_var(input)?;
321    let (input, segments) = many0(parse_member_access)(input)?;
322    let mut all_segments = vec![PathSegment::Field(root)];
323    all_segments.extend(segments);
324    Ok((input, PathExpr::new(all_segments)))
325}
326
327fn parse_root_var(input: &str) -> IResult<&str, String> {
328    let (input, _) = tag("message")(input)?;
329    Ok((input, "message".to_string()))
330}
331
332fn parse_member_access(input: &str) -> IResult<&str, PathSegment> {
333    alt((parse_dot_access, parse_bracket_access))(input)
334}
335
336fn parse_dot_access(input: &str) -> IResult<&str, PathSegment> {
337    let (input, _) = char('.')(input)?;
338    let (input, ident) = parse_identifier(input)?;
339    Ok((input, PathSegment::Field(ident)))
340}
341
342fn parse_bracket_access(input: &str) -> IResult<&str, PathSegment> {
343    delimited(
344        char('['),
345        alt((
346            map(tag("*"), |_| PathSegment::Wildcard),
347            map(parse_index, PathSegment::Index),
348            map(parse_quoted_key, PathSegment::QuotedKey),
349        )),
350        char(']'),
351    )(input)
352}
353
354fn parse_identifier(input: &str) -> IResult<&str, String> {
355    map(
356        recognize(pair(alpha1, many0(alt((alphanumeric1, tag("_")))))),
357        |s: &str| s.to_string(),
358    )(input)
359}
360
361fn parse_index(input: &str) -> IResult<&str, usize> {
362    // `s.parse::<usize>()` panics on overflow (>= 20 digits); use a saturating
363    // fold so untrusted input can never crash the parser.
364    map(digit1, |s: &str| {
365        s.bytes().fold(0usize, |acc, b| {
366            acc.saturating_mul(10).saturating_add((b - b'0') as usize)
367        })
368    })(input)
369}
370
371fn parse_quoted_key(input: &str) -> IResult<&str, String> {
372    delimited(
373        char('"'),
374        map(
375            take_while1(|c: char| c != '"' && ('\x20'..='\x7e').contains(&c)),
376            |s: &str| s.to_string(),
377        ),
378        char('"'),
379    )(input)
380}
381
382fn parse_comparator(input: &str) -> IResult<&str, Comparator> {
383    alt((
384        value(Comparator::Eq, tag("==")),
385        value(Comparator::Neq, tag("!=")),
386        value(Comparator::Gte, tag(">=")),
387        value(Comparator::Lte, tag("<=")),
388        value(Comparator::Gt, tag(">")),
389        value(Comparator::Lt, tag("<")),
390        value(Comparator::In, tag("in")),
391        value(Comparator::Matches, tag("matches")),
392    ))(input)
393}
394
395fn parse_literal(input: &str) -> IResult<&str, Literal> {
396    alt((
397        value(Literal::Bool(true), tag("true")),
398        value(Literal::Bool(false), tag("false")),
399        value(Literal::Null, tag("null")),
400        map(parse_number, Literal::Number),
401        map(parse_string_literal, Literal::String),
402        map(parse_array_literal, Literal::Array),
403    ))(input)
404}
405
406fn parse_array_literal(input: &str) -> IResult<&str, Vec<Literal>> {
407    delimited(
408        char('['),
409        preceded(
410            multispace0,
411            separated_list0(
412                preceded(multispace0, char(',')),
413                preceded(multispace0, parse_literal),
414            ),
415        ),
416        preceded(multispace0, char(']')),
417    )(input)
418}
419
420fn parse_number(input: &str) -> IResult<&str, f64> {
421    let (input, sign) = opt(char('-'))(input)?;
422    let (input, int_part) = digit1(input)?;
423    let (input, frac_part) = opt(preceded(char('.'), digit1))(input)?;
424
425    let num_str = format!(
426        "{}{}{}",
427        sign.map(|_| "-").unwrap_or(""),
428        int_part,
429        frac_part.map(|f| format!(".{}", f)).unwrap_or_default()
430    );
431    let value = num_str.parse::<f64>().map_err(|_| {
432        nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
433    })?;
434    Ok((input, value))
435}
436
437fn parse_string_literal(input: &str) -> IResult<&str, String> {
438    delimited(
439        char('"'),
440        map(take_while1(|c: char| c != '"'), |s: &str| s.to_string()),
441        char('"'),
442    )(input)
443}
444
445/// Counts operands (every `Comparison` leaf's two operands, plus every
446/// arithmetic/function operand nested within them) in a `BoolExpr` tree, for
447/// rule V-206's conformance-floor check (spec §6.2).
448pub fn count_operands(expr: &BoolExpr) -> usize {
449    match expr {
450        BoolExpr::And(a, b) | BoolExpr::Or(a, b) => count_operands(a) + count_operands(b),
451        BoolExpr::Not(a) => count_operands(a),
452        BoolExpr::Comparison(cmp) => count_operand(&cmp.left) + count_operand(&cmp.right),
453        BoolExpr::Quantifier(q) => {
454            count_operand(&q.comparison.left) + count_operand(&q.comparison.right)
455        }
456        BoolExpr::Defined(_) => 1,
457    }
458}
459
460fn count_operand(operand: &Operand) -> usize {
461    match operand {
462        Operand::Value(v) => count_value_expr(v),
463        Operand::Literal(_) => 1,
464    }
465}
466
467fn count_value_expr(expr: &ValueExpr) -> usize {
468    match expr {
469        ValueExpr::Path(_) | ValueExpr::Number(_) => 1,
470        ValueExpr::Call(_, arg) => count_value_expr(arg),
471        ValueExpr::Add(a, b)
472        | ValueExpr::Sub(a, b)
473        | ValueExpr::Mul(a, b)
474        | ValueExpr::Div(a, b) => count_value_expr(a) + count_value_expr(b),
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    fn as_comparison(expr: &BoolExpr) -> &Comparison {
483        match expr {
484            BoolExpr::Comparison(c) => c,
485            _ => panic!("expected a bare comparison, got {:?}", expr),
486        }
487    }
488
489    #[test]
490    fn test_default_condition() {
491        assert_eq!(parse_condition("default").unwrap(), Condition::Default);
492    }
493
494    #[test]
495    fn test_simple_comparison() {
496        let cond = parse_condition("message.payload.status == \"ok\"").unwrap();
497        match cond {
498            Condition::Expr(expr) => {
499                let c = as_comparison(&expr);
500                assert_eq!(c.op, Comparator::Eq);
501                match &c.left {
502                    Operand::Value(ValueExpr::Path(p)) => assert_eq!(p.segments.len(), 3),
503                    _ => panic!("expected path"),
504                }
505                match &c.right {
506                    Operand::Literal(Literal::String(s)) => assert_eq!(s, "ok"),
507                    _ => panic!("expected string literal"),
508                }
509            }
510            _ => panic!("expected comparison"),
511        }
512    }
513
514    #[test]
515    fn test_wildcard_path() {
516        let cond = parse_condition("message.payload.items[*].qty > 0").unwrap();
517        match cond {
518            Condition::Expr(expr) => {
519                let c = as_comparison(&expr);
520                match &c.left {
521                    Operand::Value(ValueExpr::Path(p)) => {
522                        assert_eq!(p.segments.len(), 5); // message, payload, items, [*], qty
523                        assert_eq!(p.segments[3], PathSegment::Wildcard);
524                    }
525                    _ => panic!("expected path"),
526                }
527            }
528            _ => panic!("expected comparison"),
529        }
530    }
531
532    #[test]
533    fn test_in_operator() {
534        let cond = parse_condition("message.payload.type in [\"A\", \"B\"]").unwrap();
535        match cond {
536            Condition::Expr(expr) => assert_eq!(as_comparison(&expr).op, Comparator::In),
537            _ => panic!("expected comparison"),
538        }
539    }
540
541    #[test]
542    fn test_matches_operator() {
543        let cond = parse_condition("message.payload.email matches \"@\"").unwrap();
544        match cond {
545            Condition::Expr(expr) => assert_eq!(as_comparison(&expr).op, Comparator::Matches),
546            _ => panic!("expected comparison"),
547        }
548    }
549
550    #[test]
551    fn test_bracket_index() {
552        let cond = parse_condition("message.payload.items[0].name == \"test\"").unwrap();
553        match cond {
554            Condition::Expr(expr) => match &as_comparison(&expr).left {
555                Operand::Value(ValueExpr::Path(p)) => {
556                    assert_eq!(p.segments.len(), 5);
557                    assert_eq!(p.segments[3], PathSegment::Index(0));
558                }
559                _ => panic!("expected path"),
560            },
561            _ => panic!("expected comparison"),
562        }
563    }
564
565    #[test]
566    fn oversized_index_does_not_panic() {
567        // 40 digits overflows usize; must parse to usize::MAX, never panic.
568        let idx = "9999999999999999999999999999999999999999";
569        let cond =
570            parse_condition(&format!("message.payload.items[{}].name == \"test\"", idx)).unwrap();
571        match cond {
572            Condition::Expr(expr) => match &as_comparison(&expr).left {
573                Operand::Value(ValueExpr::Path(p)) => {
574                    assert_eq!(p.segments[3], PathSegment::Index(usize::MAX));
575                }
576                _ => panic!("expected path"),
577            },
578            _ => panic!("expected comparison"),
579        }
580    }
581
582    #[test]
583    fn trailing_content_is_error() {
584        // `&&` is now valid grammar — use content the grammar genuinely has
585        // no production for.
586        assert!(parse_condition("message.payload.ok == true } extra").is_err());
587    }
588
589    #[test]
590    fn default_is_parsed() {
591        assert!(matches!(parse_condition("default"), Ok(Condition::Default)));
592    }
593
594    #[test]
595    fn negated_numbers_parse() {
596        let cond = parse_condition("message.payload.temp < -5").unwrap();
597        match cond {
598            Condition::Expr(expr) => match &as_comparison(&expr).right {
599                Operand::Value(ValueExpr::Number(n)) => assert_eq!(*n, -5.0),
600                _ => panic!("expected negative number"),
601            },
602            _ => panic!("expected comparison"),
603        }
604    }
605
606    // --- new grammar: boolean combinators ---
607
608    #[test]
609    fn and_combinator_parses() {
610        let cond =
611            parse_condition("message.payload.a > 0 && message.payload.b > 0").unwrap();
612        match cond {
613            Condition::Expr(BoolExpr::And(_, _)) => {}
614            other => panic!("expected And, got {:?}", other),
615        }
616    }
617
618    #[test]
619    fn or_combinator_parses() {
620        let cond =
621            parse_condition("message.payload.a > 0 || message.payload.b > 0").unwrap();
622        match cond {
623            Condition::Expr(BoolExpr::Or(_, _)) => {}
624            other => panic!("expected Or, got {:?}", other),
625        }
626    }
627
628    #[test]
629    fn not_binds_tighter_than_and() {
630        // !a && b  =>  And(Not(a), b)
631        let cond =
632            parse_condition("!message.payload.a == true && message.payload.b == true").unwrap();
633        match cond {
634            Condition::Expr(BoolExpr::And(left, _)) => {
635                assert!(matches!(*left, BoolExpr::Not(_)));
636            }
637            other => panic!("expected And(Not(_), _), got {:?}", other),
638        }
639    }
640
641    #[test]
642    fn and_binds_tighter_than_or() {
643        // a || b && c  =>  Or(a, And(b, c))
644        let cond = parse_condition(
645            "message.payload.a == true || message.payload.b == true && message.payload.c == true",
646        )
647        .unwrap();
648        match cond {
649            Condition::Expr(BoolExpr::Or(_, right)) => {
650                assert!(matches!(*right, BoolExpr::And(_, _)));
651            }
652            other => panic!("expected Or(_, And(_, _)), got {:?}", other),
653        }
654    }
655
656    #[test]
657    fn parens_override_precedence() {
658        // (a || b) && c  =>  And(Or(a, b), c)
659        let cond = parse_condition(
660            "(message.payload.a == true || message.payload.b == true) && message.payload.c == true",
661        )
662        .unwrap();
663        match cond {
664            Condition::Expr(BoolExpr::And(left, _)) => {
665                assert!(matches!(*left, BoolExpr::Or(_, _)));
666            }
667            other => panic!("expected And(Or(_, _), _), got {:?}", other),
668        }
669    }
670
671    // --- new grammar: arithmetic ---
672
673    #[test]
674    fn arithmetic_precedence_mul_over_add() {
675        // a + b * c  =>  Add(a, Mul(b, c))
676        let cond = parse_condition("message.payload.a + message.payload.b * 2 > 0").unwrap();
677        match cond {
678            Condition::Expr(expr) => match &as_comparison(&expr).left {
679                Operand::Value(ValueExpr::Add(_, right)) => {
680                    assert!(matches!(**right, ValueExpr::Mul(_, _)));
681                }
682                other => panic!("expected Add(_, Mul(_, _)), got {:?}", other),
683            },
684            _ => panic!("expected comparison"),
685        }
686    }
687
688    #[test]
689    fn subtraction_parses() {
690        let cond = parse_condition("message.payload.subtotal - message.payload.fee > 0").unwrap();
691        match cond {
692            Condition::Expr(expr) => match &as_comparison(&expr).left {
693                Operand::Value(ValueExpr::Sub(_, _)) => {}
694                other => panic!("expected Sub, got {:?}", other),
695            },
696            _ => panic!("expected comparison"),
697        }
698    }
699
700    #[test]
701    fn division_parses() {
702        let cond = parse_condition("message.payload.a / message.payload.b > 0").unwrap();
703        match cond {
704            Condition::Expr(expr) => match &as_comparison(&expr).left {
705                Operand::Value(ValueExpr::Div(_, _)) => {}
706                other => panic!("expected Div, got {:?}", other),
707            },
708            _ => panic!("expected comparison"),
709        }
710    }
711
712    // --- new grammar: built-in functions ---
713
714    #[test]
715    fn length_function_parses() {
716        let cond = parse_condition("length(message.payload.items) > 0").unwrap();
717        match cond {
718            Condition::Expr(expr) => match &as_comparison(&expr).left {
719                Operand::Value(ValueExpr::Call(FuncName::Length, _)) => {}
720                other => panic!("expected Call(Length, _), got {:?}", other),
721            },
722            _ => panic!("expected comparison"),
723        }
724    }
725
726    #[test]
727    fn abs_function_parses() {
728        let cond = parse_condition("abs(message.payload.delta) < 1").unwrap();
729        match cond {
730            Condition::Expr(expr) => match &as_comparison(&expr).left {
731                Operand::Value(ValueExpr::Call(FuncName::Abs, _)) => {}
732                other => panic!("expected Call(Abs, _), got {:?}", other),
733            },
734            _ => panic!("expected comparison"),
735        }
736    }
737
738    #[test]
739    fn lower_and_upper_functions_parse() {
740        let lower = parse_condition("lower(message.payload.status) == \"paid\"").unwrap();
741        match lower {
742            Condition::Expr(expr) => match &as_comparison(&expr).left {
743                Operand::Value(ValueExpr::Call(FuncName::Lower, _)) => {}
744                other => panic!("expected Call(Lower, _), got {:?}", other),
745            },
746            _ => panic!("expected comparison"),
747        }
748        let upper = parse_condition("upper(message.payload.status) == \"PAID\"").unwrap();
749        match upper {
750            Condition::Expr(expr) => match &as_comparison(&expr).left {
751                Operand::Value(ValueExpr::Call(FuncName::Upper, _)) => {}
752                other => panic!("expected Call(Upper, _), got {:?}", other),
753            },
754            _ => panic!("expected comparison"),
755        }
756    }
757
758    #[test]
759    fn unknown_function_name_is_rejected() {
760        assert!(parse_condition("now(message.payload.x) > 0").is_err());
761    }
762
763    // --- new grammar: defined() ---
764
765    #[test]
766    fn defined_expr_parses() {
767        let cond = parse_condition("defined(message.payload.discountCode)").unwrap();
768        match cond {
769            Condition::Expr(BoolExpr::Defined(path)) => {
770                assert_eq!(path.segments.len(), 3); // message, payload, discountCode
771            }
772            other => panic!("expected Defined, got {:?}", other),
773        }
774    }
775
776    #[test]
777    fn defined_combines_with_and() {
778        let cond = parse_condition(
779            "defined(message.payload.discountCode) && message.payload.amount > 0",
780        )
781        .unwrap();
782        assert!(matches!(cond, Condition::Expr(BoolExpr::And(_, _))));
783    }
784
785    // --- new grammar: explicit quantifiers ---
786
787    #[test]
788    fn explicit_any_quantifier_parses() {
789        let cond = parse_condition(
790            "any(message.payload.items, message.payload.items[*].qty > 0)",
791        )
792        .unwrap();
793        match cond {
794            Condition::Expr(BoolExpr::Quantifier(q)) => {
795                assert_eq!(q.kind, QuantifierKind::Any);
796            }
797            other => panic!("expected Quantifier(Any), got {:?}", other),
798        }
799    }
800
801    #[test]
802    fn explicit_all_quantifier_parses() {
803        let cond = parse_condition(
804            "all(message.payload.items, message.payload.items[*].qty > 0)",
805        )
806        .unwrap();
807        match cond {
808            Condition::Expr(BoolExpr::Quantifier(q)) => {
809                assert_eq!(q.kind, QuantifierKind::All);
810            }
811            other => panic!("expected Quantifier(All), got {:?}", other),
812        }
813    }
814
815    #[test]
816    fn quantifier_inner_expression_rejects_combinators() {
817        // The inner test is exactly a `comparison`, not a `bool-expr` — it
818        // MUST NOT contain `&&`/`||`/`!` (spec §6.4).
819        assert!(parse_condition(
820            "any(message.payload.items, message.payload.items[*].qty > 0 && true)"
821        )
822        .is_err());
823    }
824
825    // --- operand-count ceiling (rule V-206 groundwork) ---
826
827    #[test]
828    fn count_operands_counts_comparison_leaves() {
829        let cond = parse_condition(
830            "message.payload.a > 0 && message.payload.b > 0 && message.payload.c > 0",
831        )
832        .unwrap();
833        match cond {
834            Condition::Expr(expr) => assert_eq!(count_operands(&expr), 6),
835            _ => panic!("expected expr"),
836        }
837    }
838
839    #[test]
840    fn count_operands_counts_nested_arithmetic() {
841        // a + b * c > 0  => operands: a, b, c, 0  (four leaves)
842        let cond = parse_condition("message.payload.a + message.payload.b * 2 > 0").unwrap();
843        match cond {
844            Condition::Expr(expr) => assert_eq!(count_operands(&expr), 4),
845            _ => panic!("expected expr"),
846        }
847    }
848}