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
12pub 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#[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub struct QuantifierExpr {
56 pub kind: QuantifierKind,
57 pub path: PathExpr,
58 pub comparison: Comparison,
59}
60
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67pub enum Operand {
68 Value(ValueExpr),
69 Literal(Literal),
70}
71
72#[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#[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
156fn 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
258fn 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 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
445pub 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); 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 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 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 #[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 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 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 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 #[test]
674 fn arithmetic_precedence_mul_over_add() {
675 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 #[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 #[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); }
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 #[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 assert!(parse_condition(
820 "any(message.payload.items, message.payload.items[*].qty > 0 && true)"
821 )
822 .is_err());
823 }
824
825 #[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 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}