1use std::cmp::Ordering;
59use std::fmt;
60use std::str::FromStr;
61
62use serde::{Deserialize, Serialize};
63use serde_json::{Number, Value};
64use thiserror::Error;
65
66pub const MAX_EXPRESSION_LEN: usize = 4096;
77
78pub const MAX_EXPRESSION_DEPTH: usize = 64;
93
94pub const EXPRESSION_ROOTS: [&str; 5] = ["output", "payload", "labels", "metadata", "steps"];
104
105#[derive(Debug, Clone, PartialEq, Eq, Error)]
118pub enum ExpressionError {
119 #[error("expression is empty")]
121 Empty,
122 #[error("expression is longer than 4096 bytes")]
124 TooLong,
125 #[error("expression is nested deeper than 64 levels")]
127 TooDeep,
128 #[error("unexpected character {ch:?} at position {pos}")]
130 UnexpectedChar {
131 pos: usize,
133 ch: char,
135 },
136 #[error("unterminated string starting at position {pos}")]
138 UnterminatedString {
139 pos: usize,
141 },
142 #[error("invalid number at position {pos}")]
144 InvalidNumber {
145 pos: usize,
147 },
148 #[error("unexpected token {found:?} at position {pos}")]
150 UnexpectedToken {
151 pos: usize,
153 found: String,
155 },
156 #[error("unexpected end of expression")]
158 UnexpectedEnd,
159 #[error("unknown root {0:?}, expected one of output, payload, labels, metadata, steps")]
161 UnknownRoot(String),
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
183#[serde(try_from = "String", into = "String")]
184pub struct Expression {
185 source: String,
186 ast: Node,
187}
188
189impl Expression {
190 pub fn parse(src: &str) -> Result<Self, ExpressionError> {
209 if src.trim().is_empty() {
210 return Err(ExpressionError::Empty);
211 }
212 if src.len() > MAX_EXPRESSION_LEN {
213 return Err(ExpressionError::TooLong);
214 }
215 let tokens = tokenize(src)?;
216 let mut parser = Parser {
217 tokens,
218 pos: 0,
219 depth: 0,
220 };
221 let ast = parser.parse_or()?;
222 if let Some(token) = parser.peek() {
223 return Err(token.unexpected());
224 }
225 Ok(Self {
226 source: src.to_string(),
227 ast,
228 })
229 }
230
231 pub fn evaluate(&self, ctx: &Value) -> bool {
248 self.ast.eval(ctx)
249 }
250
251 pub fn source(&self) -> &str {
263 &self.source
264 }
265}
266
267impl PartialEq for Expression {
268 fn eq(&self, other: &Self) -> bool {
269 self.source == other.source
270 }
271}
272
273impl Eq for Expression {}
274
275impl fmt::Display for Expression {
276 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277 f.write_str(&self.source)
278 }
279}
280
281impl FromStr for Expression {
282 type Err = ExpressionError;
283
284 fn from_str(s: &str) -> Result<Self, Self::Err> {
285 Self::parse(s)
286 }
287}
288
289impl TryFrom<String> for Expression {
290 type Error = ExpressionError;
291
292 fn try_from(value: String) -> Result<Self, Self::Error> {
293 Self::parse(&value)
294 }
295}
296
297impl From<Expression> for String {
298 fn from(expr: Expression) -> Self {
299 expr.source
300 }
301}
302
303#[derive(Debug, Clone)]
308enum Node {
309 Or(Vec<Node>),
310 And(Vec<Node>),
311 Not(Box<Node>),
312 Compare(Operand, CmpOp, Operand),
313 Truthy(Operand),
314}
315
316#[derive(Debug, Clone, Copy)]
317enum CmpOp {
318 Eq,
319 Ne,
320 Gt,
321 Ge,
322 Lt,
323 Le,
324}
325
326#[derive(Debug, Clone)]
327enum Operand {
328 Path(Vec<Segment>),
329 Literal(Value),
330}
331
332#[derive(Debug, Clone)]
333enum Segment {
334 Key(String),
335 Index(usize),
336}
337
338static NULL: Value = Value::Null;
339
340impl Node {
341 fn eval(&self, ctx: &Value) -> bool {
342 match self {
343 Node::Or(nodes) => nodes.iter().any(|n| n.eval(ctx)),
344 Node::And(nodes) => nodes.iter().all(|n| n.eval(ctx)),
345 Node::Not(node) => !node.eval(ctx),
346 Node::Compare(left, op, right) => {
347 let (a, b) = (left.resolve(ctx), right.resolve(ctx));
348 match op {
349 CmpOp::Eq => loose_eq(a, b),
350 CmpOp::Ne => !loose_eq(a, b),
351 CmpOp::Gt => loose_cmp(a, b).is_some_and(Ordering::is_gt),
352 CmpOp::Ge => loose_cmp(a, b).is_some_and(Ordering::is_ge),
353 CmpOp::Lt => loose_cmp(a, b).is_some_and(Ordering::is_lt),
354 CmpOp::Le => loose_cmp(a, b).is_some_and(Ordering::is_le),
355 }
356 }
357 Node::Truthy(operand) => truthy(operand.resolve(ctx)),
358 }
359 }
360}
361
362impl Operand {
363 fn resolve<'a>(&'a self, ctx: &'a Value) -> &'a Value {
364 match self {
365 Operand::Literal(value) => value,
366 Operand::Path(segments) => {
367 let mut current = ctx;
368 for segment in segments {
369 let next = match segment {
370 Segment::Key(key) => current.as_object().and_then(|o| o.get(key)),
371 Segment::Index(idx) => current.as_array().and_then(|a| a.get(*idx)),
372 };
373 match next {
374 Some(value) => current = value,
375 None => return &NULL,
376 }
377 }
378 current
379 }
380 }
381 }
382}
383
384fn as_number(value: &Value) -> Option<f64> {
386 match value {
387 Value::Number(n) => n.as_f64(),
388 Value::String(s) => s.trim().parse::<f64>().ok(),
389 _ => None,
390 }
391}
392
393fn numeric_pair(a: &Value, b: &Value) -> Option<(f64, f64)> {
396 if !a.is_number() && !b.is_number() {
397 return None;
398 }
399 Some((as_number(a)?, as_number(b)?))
400}
401
402fn loose_eq(a: &Value, b: &Value) -> bool {
403 if a.is_number() || b.is_number() {
404 return numeric_pair(a, b).is_some_and(|(x, y)| x == y);
405 }
406 a == b
407}
408
409fn loose_cmp(a: &Value, b: &Value) -> Option<Ordering> {
410 if let Some((x, y)) = numeric_pair(a, b) {
411 return x.partial_cmp(&y);
412 }
413 match (a, b) {
414 (Value::String(x), Value::String(y)) => Some(x.cmp(y)),
415 _ => None,
416 }
417}
418
419fn truthy(value: &Value) -> bool {
420 match value {
421 Value::Null => false,
422 Value::Bool(b) => *b,
423 Value::Number(n) => n.as_f64().is_some_and(|f| f != 0.0),
424 Value::String(s) => !s.is_empty(),
425 Value::Array(a) => !a.is_empty(),
426 Value::Object(o) => !o.is_empty(),
427 }
428}
429
430#[derive(Debug, Clone, PartialEq)]
435enum Tok {
436 Ident(String),
437 Str(String),
438 Num(Number),
439 LParen,
440 RParen,
441 LBracket,
442 RBracket,
443 Dot,
444 AndAnd,
445 OrOr,
446 Bang,
447 Eq,
448 Ne,
449 Gt,
450 Ge,
451 Lt,
452 Le,
453}
454
455impl fmt::Display for Tok {
456 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457 match self {
458 Tok::Ident(s) => f.write_str(s),
459 Tok::Str(s) => write!(f, "{s:?}"),
460 Tok::Num(n) => write!(f, "{n}"),
461 Tok::LParen => f.write_str("("),
462 Tok::RParen => f.write_str(")"),
463 Tok::LBracket => f.write_str("["),
464 Tok::RBracket => f.write_str("]"),
465 Tok::Dot => f.write_str("."),
466 Tok::AndAnd => f.write_str("&&"),
467 Tok::OrOr => f.write_str("||"),
468 Tok::Bang => f.write_str("!"),
469 Tok::Eq => f.write_str("=="),
470 Tok::Ne => f.write_str("!="),
471 Tok::Gt => f.write_str(">"),
472 Tok::Ge => f.write_str(">="),
473 Tok::Lt => f.write_str("<"),
474 Tok::Le => f.write_str("<="),
475 }
476 }
477}
478
479#[derive(Debug, Clone)]
480struct Token {
481 kind: Tok,
482 pos: usize,
483}
484
485impl Token {
486 fn unexpected(&self) -> ExpressionError {
487 ExpressionError::UnexpectedToken {
488 pos: self.pos,
489 found: self.kind.to_string(),
490 }
491 }
492}
493
494fn is_ident_start(c: char) -> bool {
495 c.is_ascii_alphabetic() || c == '_'
496}
497
498fn is_ident_continue(c: char) -> bool {
499 c.is_ascii_alphanumeric() || c == '_' || c == '-'
500}
501
502fn tokenize(src: &str) -> Result<Vec<Token>, ExpressionError> {
503 let chars: Vec<(usize, char)> = src.char_indices().collect();
504 let mut tokens = Vec::new();
505 let mut i = 0;
506
507 while let Some(&(pos, c)) = chars.get(i) {
508 if c.is_whitespace() {
509 i += 1;
510 continue;
511 }
512
513 let next = chars.get(i + 1).map(|&(_, n)| n);
514 let (kind, len) = match (c, next) {
515 ('(', _) => (Tok::LParen, 1),
516 (')', _) => (Tok::RParen, 1),
517 ('[', _) => (Tok::LBracket, 1),
518 (']', _) => (Tok::RBracket, 1),
519 ('.', _) => (Tok::Dot, 1),
520 ('&', Some('&')) => (Tok::AndAnd, 2),
521 ('|', Some('|')) => (Tok::OrOr, 2),
522 ('=', Some('=')) => (Tok::Eq, 2),
523 ('!', Some('=')) => (Tok::Ne, 2),
524 ('!', _) => (Tok::Bang, 1),
525 ('>', Some('=')) => (Tok::Ge, 2),
526 ('>', _) => (Tok::Gt, 1),
527 ('<', Some('=')) => (Tok::Le, 2),
528 ('<', _) => (Tok::Lt, 1),
529 ('"' | '\'', _) => {
530 let (value, consumed) = lex_string(&chars, i)?;
531 (Tok::Str(value), consumed)
532 }
533 (c, _) if c.is_ascii_digit() || c == '-' => {
534 let (number, consumed) = lex_number(src, &chars, i)?;
535 (Tok::Num(number), consumed)
536 }
537 (c, _) if is_ident_start(c) => {
538 let mut j = i + 1;
539 while chars.get(j).is_some_and(|&(_, n)| is_ident_continue(n)) {
540 j += 1;
541 }
542 let end = chars.get(j).map_or(src.len(), |&(p, _)| p);
543 (Tok::Ident(src[pos..end].to_string()), j - i)
544 }
545 (ch, _) => return Err(ExpressionError::UnexpectedChar { pos, ch }),
546 };
547
548 tokens.push(Token { kind, pos });
549 i += len;
550 }
551
552 Ok(tokens)
553}
554
555fn lex_string(chars: &[(usize, char)], start: usize) -> Result<(String, usize), ExpressionError> {
558 let (open_pos, quote) = chars[start];
559 let mut value = String::new();
560 let mut j = start + 1;
561
562 while let Some(&(pos, c)) = chars.get(j) {
563 if c == quote {
564 return Ok((value, j - start + 1));
565 }
566 if c != '\\' {
567 value.push(c);
568 j += 1;
569 continue;
570 }
571 let Some(&(_, escaped)) = chars.get(j + 1) else {
572 break;
573 };
574 match escaped {
575 '"' | '\'' | '\\' => value.push(escaped),
576 'n' => value.push('\n'),
577 other => return Err(ExpressionError::UnexpectedChar { pos, ch: other }),
578 }
579 j += 2;
580 }
581
582 Err(ExpressionError::UnterminatedString { pos: open_pos })
583}
584
585fn lex_number(
589 src: &str,
590 chars: &[(usize, char)],
591 start: usize,
592) -> Result<(Number, usize), ExpressionError> {
593 let pos = chars[start].0;
594 let invalid = ExpressionError::InvalidNumber { pos };
595 let is_digit = |j: usize| chars.get(j).is_some_and(|&(_, c)| c.is_ascii_digit());
596
597 let mut j = start;
598 if chars[j].1 == '-' {
599 j += 1;
600 }
601 if !is_digit(j) {
602 return Err(invalid);
603 }
604 while is_digit(j) {
605 j += 1;
606 }
607 let mut is_float = false;
608 if chars.get(j).is_some_and(|&(_, c)| c == '.') && is_digit(j + 1) {
609 is_float = true;
610 j += 1;
611 while is_digit(j) {
612 j += 1;
613 }
614 }
615 if chars.get(j).is_some_and(|&(_, c)| is_ident_start(c)) {
617 return Err(invalid);
618 }
619
620 let end = chars.get(j).map_or(src.len(), |&(p, _)| p);
621 let text = &src[pos..end];
622 let parsed = if is_float {
623 text.parse::<f64>().ok().and_then(Number::from_f64)
624 } else {
625 text.parse::<i64>().ok().map(Number::from)
626 };
627 parsed.map(|n| (n, j - start)).ok_or(invalid)
628}
629
630struct Parser {
635 tokens: Vec<Token>,
636 pos: usize,
637 depth: usize,
638}
639
640impl Parser {
641 fn peek(&self) -> Option<&Token> {
642 self.tokens.get(self.pos)
643 }
644
645 fn peek_kind(&self) -> Option<&Tok> {
646 self.peek().map(|t| &t.kind)
647 }
648
649 fn advance(&mut self) -> Result<Token, ExpressionError> {
650 let token = self
651 .tokens
652 .get(self.pos)
653 .cloned()
654 .ok_or(ExpressionError::UnexpectedEnd)?;
655 self.pos += 1;
656 Ok(token)
657 }
658
659 fn expect(&mut self, kind: &Tok) -> Result<(), ExpressionError> {
660 let token = self.advance()?;
661 if &token.kind == kind {
662 Ok(())
663 } else {
664 Err(token.unexpected())
665 }
666 }
667
668 fn enter(&mut self) -> Result<(), ExpressionError> {
669 self.depth += 1;
670 if self.depth > MAX_EXPRESSION_DEPTH {
671 return Err(ExpressionError::TooDeep);
672 }
673 Ok(())
674 }
675
676 fn leave(&mut self) {
677 self.depth -= 1;
678 }
679
680 fn parse_or(&mut self) -> Result<Node, ExpressionError> {
681 let mut nodes = vec![self.parse_and()?];
682 while self.peek_kind() == Some(&Tok::OrOr) {
683 self.pos += 1;
684 nodes.push(self.parse_and()?);
685 }
686 Ok(if nodes.len() == 1 {
687 nodes.remove(0)
688 } else {
689 Node::Or(nodes)
690 })
691 }
692
693 fn parse_and(&mut self) -> Result<Node, ExpressionError> {
694 let mut nodes = vec![self.parse_unary()?];
695 while self.peek_kind() == Some(&Tok::AndAnd) {
696 self.pos += 1;
697 nodes.push(self.parse_unary()?);
698 }
699 Ok(if nodes.len() == 1 {
700 nodes.remove(0)
701 } else {
702 Node::And(nodes)
703 })
704 }
705
706 fn parse_unary(&mut self) -> Result<Node, ExpressionError> {
707 if self.peek_kind() == Some(&Tok::Bang) {
708 self.pos += 1;
709 self.enter()?;
710 let inner = self.parse_unary()?;
711 self.leave();
712 return Ok(Node::Not(Box::new(inner)));
713 }
714 self.parse_primary()
715 }
716
717 fn parse_primary(&mut self) -> Result<Node, ExpressionError> {
718 if self.peek_kind() == Some(&Tok::LParen) {
719 self.pos += 1;
720 self.enter()?;
721 let inner = self.parse_or()?;
722 self.expect(&Tok::RParen)?;
723 self.leave();
724 return Ok(inner);
725 }
726
727 let left = self.parse_operand()?;
728 let op = match self.peek_kind() {
729 Some(Tok::Eq) => CmpOp::Eq,
730 Some(Tok::Ne) => CmpOp::Ne,
731 Some(Tok::Gt) => CmpOp::Gt,
732 Some(Tok::Ge) => CmpOp::Ge,
733 Some(Tok::Lt) => CmpOp::Lt,
734 Some(Tok::Le) => CmpOp::Le,
735 _ => return Ok(Node::Truthy(left)),
736 };
737 self.pos += 1;
738 let right = self.parse_operand()?;
739 Ok(Node::Compare(left, op, right))
740 }
741
742 fn parse_operand(&mut self) -> Result<Operand, ExpressionError> {
743 let token = self.advance()?;
744 match &token.kind {
745 Tok::Str(s) => Ok(Operand::Literal(Value::String(s.clone()))),
746 Tok::Num(n) => Ok(Operand::Literal(Value::Number(n.clone()))),
747 Tok::Ident(name) => match name.as_str() {
748 "true" => Ok(Operand::Literal(Value::Bool(true))),
749 "false" => Ok(Operand::Literal(Value::Bool(false))),
750 "null" => Ok(Operand::Literal(Value::Null)),
751 root if EXPRESSION_ROOTS.contains(&root) => {
752 let mut segments = vec![Segment::Key(root.to_string())];
753 self.parse_path_tail(&mut segments)?;
754 Ok(Operand::Path(segments))
755 }
756 other => Err(ExpressionError::UnknownRoot(other.to_string())),
757 },
758 _ => Err(token.unexpected()),
759 }
760 }
761
762 fn parse_path_tail(&mut self, segments: &mut Vec<Segment>) -> Result<(), ExpressionError> {
763 loop {
764 match self.peek_kind() {
765 Some(Tok::Dot) => {
766 self.pos += 1;
767 let token = self.advance()?;
768 match &token.kind {
769 Tok::Ident(name) => segments.push(Segment::Key(name.clone())),
770 _ => return Err(token.unexpected()),
771 }
772 }
773 Some(Tok::LBracket) => {
774 self.pos += 1;
775 let token = self.advance()?;
776 match &token.kind {
777 Tok::Str(key) => segments.push(Segment::Key(key.clone())),
778 Tok::Num(n) => match n.as_u64() {
779 Some(idx) => segments.push(Segment::Index(idx as usize)),
780 None => return Err(token.unexpected()),
781 },
782 _ => return Err(token.unexpected()),
783 }
784 self.expect(&Tok::RBracket)?;
785 }
786 _ => return Ok(()),
787 }
788 }
789 }
790}
791
792#[cfg(test)]
793mod tests {
794 use serde_json::{from_value, json, to_value};
795
796 use super::*;
797
798 fn eval(src: &str, ctx: &Value) -> bool {
799 Expression::parse(src)
800 .unwrap_or_else(|e| panic!("{src:?} should parse: {e}"))
801 .evaluate(ctx)
802 }
803
804 fn ctx() -> Value {
805 json!({
806 "output": {"amount": 15000, "ok": true},
807 "payload": {
808 "amount": 15000,
809 "ratio": 0.75,
810 "delta": -3,
811 "name": "café",
812 "items": ["first", "second"],
813 "empty": "",
814 "zero": 0,
815 "list": [],
816 "obj": {},
817 "flag": false,
818 },
819 "labels": {"env": "production", "priority": "5", "team": "ops"},
820 "metadata": {"attempt": 1, "workflow_name": "deploy"},
821 "steps": {
822 "risk_assessment": {"output": {"level": "high"}},
823 "risk-assessment": {"output": {"level": "high"}},
824 },
825 })
826 }
827
828 #[test]
831 fn every_comparison_operator() {
832 let c = ctx();
833 assert!(eval("payload.amount == 15000", &c));
834 assert!(eval("payload.amount != 1", &c));
835 assert!(eval("payload.amount > 10000", &c));
836 assert!(eval("payload.amount >= 15000", &c));
837 assert!(eval("payload.amount < 20000", &c));
838 assert!(eval("payload.amount <= 15000", &c));
839 assert!(!eval("payload.amount < 15000", &c));
840 assert!(!eval("payload.amount >= 15001", &c));
841 assert!(!eval("payload.amount <= 14999", &c));
842 }
843
844 #[test]
845 fn output_amount_threshold() {
846 assert!(eval("output.amount > 10000", &ctx()));
847 let small = json!({"output": {"amount": 10}});
848 assert!(!eval("output.amount > 10000", &small));
849 }
850
851 #[test]
852 fn label_equality() {
853 assert!(eval("labels.env == \"production\"", &ctx()));
854 assert!(!eval("labels.env == \"staging\"", &ctx()));
855 }
856
857 #[test]
858 fn step_output_path() {
859 let c = ctx();
860 assert!(eval("steps.risk_assessment.output.level == \"high\"", &c));
861 }
862
863 #[test]
864 fn bracket_path_with_dash() {
865 let src = "steps[\"risk-assessment\"].output.level == 'high'";
866 assert!(eval(src, &ctx()));
867 }
868
869 #[test]
870 fn bracket_path_with_unicode_and_spaces() {
871 let c = json!({"steps": {"évaluation des risques": {"output": {"level": "élevé"}}}});
872 let src = "steps[\"évaluation des risques\"].output.level == \"élevé\"";
873 assert!(eval(src, &c));
874 }
875
876 #[test]
877 fn array_index() {
878 assert!(eval("payload.items[0] == \"first\"", &ctx()));
879 assert!(eval("payload.items[1] == \"second\"", &ctx()));
880 assert!(!eval("payload.items[5]", &ctx()));
881 }
882
883 #[test]
886 fn and_or_not() {
887 let c = ctx();
888 assert!(eval("payload.amount > 1 && labels.env == 'production'", &c));
889 assert!(!eval("payload.amount > 1 && labels.env == 'staging'", &c));
890 assert!(eval("payload.amount < 1 || labels.env == 'production'", &c));
891 assert!(!eval("payload.amount < 1 || labels.env == 'staging'", &c));
892 assert!(eval("!(labels.env == 'staging')", &c));
893 assert!(eval("!!payload.amount", &c));
894 }
895
896 #[test]
897 fn and_binds_tighter_than_or() {
898 let c = ctx();
900 assert!(eval(
901 "labels.env == 'production' || labels.env == 'x' && labels.env == 'y'",
902 &c
903 ));
904 assert!(!eval(
905 "(labels.env == 'production' || labels.env == 'x') && labels.env == 'y'",
906 &c
907 ));
908 }
909
910 #[test]
911 fn not_applies_to_the_following_primary_only() {
912 let c = ctx();
913 assert!(eval("!payload.flag && payload.amount", &c));
915 }
916
917 #[test]
920 fn bare_path_truthiness() {
921 let c = ctx();
922 assert!(eval("output.ok", &c));
923 assert!(eval("payload.amount", &c));
924 assert!(eval("payload.items", &c));
925 assert!(eval("labels", &c));
926 assert!(!eval("payload.flag", &c));
927 assert!(!eval("payload.zero", &c));
928 assert!(!eval("payload.empty", &c));
929 assert!(!eval("payload.list", &c));
930 assert!(!eval("payload.obj", &c));
931 assert!(!eval("payload.missing", &c));
932 }
933
934 #[test]
935 fn literal_truthiness() {
936 let c = ctx();
937 assert!(eval("true", &c));
938 assert!(!eval("false", &c));
939 assert!(!eval("null", &c));
940 assert!(eval("1", &c));
941 assert!(!eval("''", &c));
942 }
943
944 #[test]
947 fn missing_path_is_null() {
948 let c = ctx();
949 assert!(!eval("payload.nope.deeper > 1", &c));
950 assert!(!eval("payload.nope == 1", &c));
951 assert!(eval("payload.nope == null", &c));
952 assert!(!eval("payload.amount.inner", &c));
953 assert!(!eval("output.amount > 1", &json!({"output": null})));
954 assert!(!eval("output.amount > 1", &json!({})));
955 }
956
957 #[test]
958 fn not_equal_on_missing_path_is_true() {
959 assert!(eval("payload.nope != 'x'", &ctx()));
960 }
961
962 #[test]
963 fn label_strings_coerce_to_numbers() {
964 let c = ctx();
965 assert!(eval("labels.priority > 3", &c));
966 assert!(eval("labels.priority == 5", &c));
967 assert!(eval("5.0 == labels.priority", &c));
968 assert!(!eval("labels.priority < 3", &c));
969 }
970
971 #[test]
972 fn type_mismatch_is_false() {
973 let c = ctx();
974 assert!(!eval("labels.env > 3", &c));
975 assert!(!eval("labels.env == 3", &c));
976 assert!(!eval("payload.items > 1", &c));
977 assert!(!eval("payload.flag < 1", &c));
978 assert!(eval("labels.env != 3", &c));
979 }
980
981 #[test]
982 fn strings_compare_lexicographically() {
983 let c = ctx();
984 assert!(eval("labels.team < 'zzz'", &c));
985 assert!(eval("labels.team >= 'ops'", &c));
986 assert!(!eval("labels.team > 'ops'", &c));
987 }
988
989 #[test]
992 fn floats_and_negatives() {
993 let c = ctx();
994 assert!(eval("payload.ratio == 0.75", &c));
995 assert!(eval("payload.ratio > 0.5", &c));
996 assert!(eval("payload.delta == -3", &c));
997 assert!(eval("payload.delta < -2.5", &c));
998 assert!(eval("payload.amount == 15000.0", &c));
999 }
1000
1001 #[test]
1002 fn single_quotes_escapes_and_unicode() {
1003 let c = json!({"payload": {"q": "it's \"quoted\"\\\n", "name": "café ☕"}});
1004 assert!(eval(r#"payload.q == 'it\'s "quoted"\\\n'"#, &c));
1005 assert!(eval(r#"payload.q == "it's \"quoted\"\\\n""#, &c));
1006 assert!(eval("payload.name == 'café ☕'", &c));
1007 }
1008
1009 #[test]
1010 fn whitespace_is_insignificant() {
1011 let src = " payload.amount>10000&&labels.env=='production' ";
1012 assert!(eval(src, &ctx()));
1013 }
1014
1015 #[test]
1018 fn empty_source_is_rejected() {
1019 assert_eq!(Expression::parse(""), Err(ExpressionError::Empty));
1020 assert_eq!(Expression::parse(" \n"), Err(ExpressionError::Empty));
1021 }
1022
1023 #[test]
1024 fn unknown_root_is_rejected() {
1025 assert_eq!(
1026 Expression::parse("foo.bar == 1"),
1027 Err(ExpressionError::UnknownRoot("foo".to_string()))
1028 );
1029 assert_eq!(
1030 Expression::parse("payload.a == other"),
1031 Err(ExpressionError::UnknownRoot("other".to_string()))
1032 );
1033 }
1034
1035 #[test]
1036 fn unterminated_string_is_rejected() {
1037 assert_eq!(
1038 Expression::parse("labels.env == \"prod"),
1039 Err(ExpressionError::UnterminatedString { pos: 14 })
1040 );
1041 assert_eq!(
1042 Expression::parse("labels.env == 'prod\\"),
1043 Err(ExpressionError::UnterminatedString { pos: 14 })
1044 );
1045 }
1046
1047 #[test]
1048 fn trailing_token_is_rejected() {
1049 assert_eq!(
1050 Expression::parse("payload.a == 1 2"),
1051 Err(ExpressionError::UnexpectedToken {
1052 pos: 15,
1053 found: "2".to_string()
1054 })
1055 );
1056 }
1057
1058 #[test]
1059 fn unexpected_char_is_rejected() {
1060 assert_eq!(
1061 Expression::parse("payload.a # 1"),
1062 Err(ExpressionError::UnexpectedChar { pos: 10, ch: '#' })
1063 );
1064 assert_eq!(
1065 Expression::parse("payload.a = 1"),
1066 Err(ExpressionError::UnexpectedChar { pos: 10, ch: '=' })
1067 );
1068 assert_eq!(
1069 Expression::parse("payload.a & payload.b"),
1070 Err(ExpressionError::UnexpectedChar { pos: 10, ch: '&' })
1071 );
1072 }
1073
1074 #[test]
1075 fn invalid_number_is_rejected() {
1076 assert_eq!(
1077 Expression::parse("payload.a > -"),
1078 Err(ExpressionError::InvalidNumber { pos: 12 })
1079 );
1080 assert_eq!(
1081 Expression::parse("payload.a > 12ab"),
1082 Err(ExpressionError::InvalidNumber { pos: 12 })
1083 );
1084 }
1085
1086 #[test]
1087 fn unexpected_end_is_rejected() {
1088 assert_eq!(
1089 Expression::parse("payload.a >"),
1090 Err(ExpressionError::UnexpectedEnd)
1091 );
1092 assert_eq!(
1093 Expression::parse("(payload.a"),
1094 Err(ExpressionError::UnexpectedEnd)
1095 );
1096 assert_eq!(
1097 Expression::parse("payload."),
1098 Err(ExpressionError::UnexpectedEnd)
1099 );
1100 }
1101
1102 #[test]
1103 fn misplaced_tokens_are_rejected() {
1104 assert!(matches!(
1105 Expression::parse("payload.a == == 1"),
1106 Err(ExpressionError::UnexpectedToken { .. })
1107 ));
1108 assert!(matches!(
1109 Expression::parse("payload[-1]"),
1110 Err(ExpressionError::UnexpectedToken { .. })
1111 ));
1112 assert!(matches!(
1113 Expression::parse("payload[1.5]"),
1114 Err(ExpressionError::UnexpectedToken { .. })
1115 ));
1116 assert!(matches!(
1117 Expression::parse(")"),
1118 Err(ExpressionError::UnexpectedToken { .. })
1119 ));
1120 assert!(matches!(
1121 Expression::parse("payload.a > 1 > 2"),
1122 Err(ExpressionError::UnexpectedToken { .. })
1123 ));
1124 }
1125
1126 #[test]
1127 fn nesting_up_to_the_limit_is_accepted() {
1128 let src = format!(
1129 "{}payload.a{}",
1130 "(".repeat(MAX_EXPRESSION_DEPTH),
1131 ")".repeat(MAX_EXPRESSION_DEPTH)
1132 );
1133 assert!(Expression::parse(&src).is_ok());
1134 let bangs = format!("{}payload.a", "!".repeat(MAX_EXPRESSION_DEPTH));
1135 assert!(Expression::parse(&bangs).is_ok());
1136 }
1137
1138 #[test]
1139 fn nesting_beyond_the_limit_is_rejected() {
1140 let src = format!(
1141 "{}payload.a{}",
1142 "(".repeat(MAX_EXPRESSION_DEPTH + 1),
1143 ")".repeat(MAX_EXPRESSION_DEPTH + 1)
1144 );
1145 assert_eq!(Expression::parse(&src), Err(ExpressionError::TooDeep));
1146 let bangs = format!("{}payload.a", "!".repeat(MAX_EXPRESSION_DEPTH + 1));
1147 assert_eq!(Expression::parse(&bangs), Err(ExpressionError::TooDeep));
1148 }
1149
1150 #[test]
1151 fn source_longer_than_the_limit_is_rejected() {
1152 let src = format!("payload.a == '{}'", "x".repeat(MAX_EXPRESSION_LEN));
1153 assert_eq!(Expression::parse(&src), Err(ExpressionError::TooLong));
1154 }
1155
1156 #[test]
1159 fn display_from_str_and_conversions() {
1160 let expr: Expression = "payload.a > 1".parse().expect("parse");
1161 assert_eq!(expr.to_string(), "payload.a > 1");
1162 assert_eq!(expr.source(), "payload.a > 1");
1163
1164 let from_string = Expression::try_from(expr.to_string()).expect("parse");
1165 assert_eq!(from_string, expr);
1166 assert_ne!(
1167 from_string,
1168 Expression::parse("payload.a >= 1").expect("parse")
1169 );
1170
1171 let back: String = expr.into();
1172 assert_eq!(back, "payload.a > 1");
1173 }
1174
1175 #[test]
1176 fn serde_roundtrip_as_a_plain_string() {
1177 let expr = Expression::parse("labels.env == 'production'").expect("parse");
1178 let json = to_value(&expr).expect("serialize");
1179 assert_eq!(json, json!("labels.env == 'production'"));
1180
1181 let back: Expression = from_value(json).expect("deserialize");
1182 assert_eq!(back, expr);
1183 assert!(back.evaluate(&ctx()));
1184 }
1185
1186 #[test]
1187 fn serde_rejects_an_invalid_expression() {
1188 let err = from_value::<Expression>(json!("foo.bar == 1")).expect_err("invalid expression");
1189 assert!(err.to_string().contains("unknown root"));
1190 assert!(from_value::<Expression>(json!("")).is_err());
1191 }
1192}