1use std::cmp::Ordering;
90
91use serde_json::{Number, Value};
92
93pub const MAX_EXPRESSION_LEN: usize = 512;
99
100#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
107#[error("{message}")]
108pub struct ExprError {
109 message: String,
110}
111
112impl ExprError {
113 fn new(message: impl Into<String>) -> Self {
114 Self {
115 message: message.into(),
116 }
117 }
118
119 #[must_use]
121 pub fn message(&self) -> &str {
122 &self.message
123 }
124}
125
126#[derive(Clone, Debug, PartialEq)]
132pub struct Expr {
133 root: Bool,
134}
135
136impl Expr {
137 #[must_use]
145 pub fn eval(&self, value: &Value) -> bool {
146 eval_bool(&self.root, value)
147 }
148}
149
150#[derive(Clone, Debug, PartialEq)]
161pub struct Reference {
162 segments: Vec<Segment>,
163}
164
165impl Reference {
166 #[must_use]
173 pub fn resolve<'a>(&self, value: &'a Value) -> Option<&'a Value> {
174 resolve_path(&self.segments, value)
175 }
176}
177
178pub fn parse_reference(input: &str) -> Result<Reference, ExprError> {
190 let chars: Vec<char> = input.chars().collect();
191 if chars.len() > MAX_EXPRESSION_LEN {
192 return Err(ExprError::new(format!(
193 "reference is {} characters long, which exceeds the {MAX_EXPRESSION_LEN}-character limit",
194 chars.len()
195 )));
196 }
197
198 let tokens = lex(&chars)?;
199 let mut parser = Parser {
200 tokens: &tokens,
201 pos: 0,
202 end: chars.len(),
203 };
204 let operand = parser.parse_operand()?;
205 parser.expect_end()?;
206 match operand {
207 Operand::Path(segments) => Ok(Reference { segments }),
208 Operand::Literal(_) => Err(ExprError::new(
209 "a reference must be a path into the routed value, not a literal",
210 )),
211 }
212}
213
214#[derive(Clone, Debug, PartialEq)]
216enum Bool {
217 Or(Box<Bool>, Box<Bool>),
218 And(Box<Bool>, Box<Bool>),
219 Not(Box<Bool>),
220 Compare {
221 left: Operand,
222 op: CmpOp,
223 right: Operand,
224 },
225 Truthy(Operand),
228}
229
230#[derive(Clone, Debug, PartialEq)]
233enum Operand {
234 Literal(Value),
235 Path(Vec<Segment>),
236}
237
238#[derive(Clone, Debug, PartialEq)]
240enum Segment {
241 Key(String),
242 Index(usize),
243}
244
245#[derive(Clone, Copy, Debug, PartialEq)]
247enum CmpOp {
248 Eq,
249 Ne,
250 Lt,
251 Le,
252 Gt,
253 Ge,
254}
255
256pub fn parse(input: &str) -> Result<Expr, ExprError> {
272 let chars: Vec<char> = input.chars().collect();
273 if chars.len() > MAX_EXPRESSION_LEN {
274 return Err(ExprError::new(format!(
275 "expression is {} characters long, which exceeds the {MAX_EXPRESSION_LEN}-character limit",
276 chars.len()
277 )));
278 }
279
280 let tokens = lex(&chars)?;
281 let mut parser = Parser {
282 tokens: &tokens,
283 pos: 0,
284 end: chars.len(),
285 };
286 let root = parser.parse_or()?;
287 parser.expect_end()?;
288 Ok(Expr { root })
289}
290
291#[derive(Clone, Debug, PartialEq)]
293struct Spanned {
294 token: Token,
295 at: usize,
296}
297
298#[derive(Clone, Debug, PartialEq)]
300enum Token {
301 Ident(String),
302 Number(Number),
303 Str(String),
304 True,
305 False,
306 Null,
307 Dot,
308 LParen,
309 RParen,
310 Bang,
311 AndAnd,
312 OrOr,
313 EqEq,
314 NotEq,
315 Lt,
316 Le,
317 Gt,
318 Ge,
319}
320
321fn is_ident_start(c: char) -> bool {
322 c.is_ascii_alphabetic() || c == '_'
323}
324
325fn is_ident_continue(c: char) -> bool {
326 c.is_ascii_alphanumeric() || c == '_'
327}
328
329fn lex(chars: &[char]) -> Result<Vec<Spanned>, ExprError> {
332 let mut tokens = Vec::new();
333 let mut i = 0;
334 let len = chars.len();
335
336 while i < len {
337 let c = chars[i];
338 let at = i;
339
340 if c.is_whitespace() {
341 i += 1;
342 continue;
343 }
344
345 match c {
346 '(' => {
347 tokens.push(Spanned {
348 token: Token::LParen,
349 at,
350 });
351 i += 1;
352 }
353 ')' => {
354 tokens.push(Spanned {
355 token: Token::RParen,
356 at,
357 });
358 i += 1;
359 }
360 '.' => {
361 tokens.push(Spanned {
362 token: Token::Dot,
363 at,
364 });
365 i += 1;
366 }
367 '!' => {
368 if i + 1 < len && chars[i + 1] == '=' {
369 tokens.push(Spanned {
370 token: Token::NotEq,
371 at,
372 });
373 i += 2;
374 } else {
375 tokens.push(Spanned {
376 token: Token::Bang,
377 at,
378 });
379 i += 1;
380 }
381 }
382 '=' => {
383 if i + 1 < len && chars[i + 1] == '=' {
384 tokens.push(Spanned {
385 token: Token::EqEq,
386 at,
387 });
388 i += 2;
389 } else {
390 return Err(at_char(at, "a lone `=`; did you mean `==`?"));
391 }
392 }
393 '<' => {
394 if i + 1 < len && chars[i + 1] == '=' {
395 tokens.push(Spanned {
396 token: Token::Le,
397 at,
398 });
399 i += 2;
400 } else {
401 tokens.push(Spanned {
402 token: Token::Lt,
403 at,
404 });
405 i += 1;
406 }
407 }
408 '>' => {
409 if i + 1 < len && chars[i + 1] == '=' {
410 tokens.push(Spanned {
411 token: Token::Ge,
412 at,
413 });
414 i += 2;
415 } else {
416 tokens.push(Spanned {
417 token: Token::Gt,
418 at,
419 });
420 i += 1;
421 }
422 }
423 '&' => {
424 if i + 1 < len && chars[i + 1] == '&' {
425 tokens.push(Spanned {
426 token: Token::AndAnd,
427 at,
428 });
429 i += 2;
430 } else {
431 return Err(at_char(at, "a lone `&`; did you mean `&&`?"));
432 }
433 }
434 '|' => {
435 if i + 1 < len && chars[i + 1] == '|' {
436 tokens.push(Spanned {
437 token: Token::OrOr,
438 at,
439 });
440 i += 2;
441 } else {
442 return Err(at_char(at, "a lone `|`; did you mean `||`?"));
443 }
444 }
445 '"' => {
446 let (token, next) = lex_string(chars, i)?;
447 tokens.push(Spanned { token, at });
448 i = next;
449 }
450 _ if c.is_ascii_digit()
451 || (c == '-' && i + 1 < len && chars[i + 1].is_ascii_digit()) =>
452 {
453 let (token, next) = lex_number(chars, i)?;
454 tokens.push(Spanned { token, at });
455 i = next;
456 }
457 _ if is_ident_start(c) => {
458 let (token, next) = lex_ident(chars, i);
459 tokens.push(Spanned { token, at });
460 i = next;
461 }
462 _ => {
463 return Err(at_char(at, format!("an unexpected character `{c}`")));
464 }
465 }
466 }
467
468 Ok(tokens)
469}
470
471fn lex_string(chars: &[char], start: usize) -> Result<(Token, usize), ExprError> {
475 let len = chars.len();
476 let mut i = start + 1;
477 let mut value = String::new();
478
479 while i < len {
480 let c = chars[i];
481 if c == '"' {
482 return Ok((Token::Str(value), i + 1));
483 }
484 if c == '\\' {
485 i += 1;
486 if i >= len {
487 break;
488 }
489 match chars[i] {
490 '"' => value.push('"'),
491 '\\' => value.push('\\'),
492 '/' => value.push('/'),
493 'n' => value.push('\n'),
494 't' => value.push('\t'),
495 'r' => value.push('\r'),
496 other => {
497 return Err(at_char(
498 i,
499 format!("an unsupported string escape `\\{other}`"),
500 ));
501 }
502 }
503 i += 1;
504 } else {
505 value.push(c);
506 i += 1;
507 }
508 }
509
510 Err(at_char(start, "an unterminated string literal"))
511}
512
513fn lex_number(chars: &[char], start: usize) -> Result<(Token, usize), ExprError> {
516 let len = chars.len();
517 let mut i = start;
518 if chars[i] == '-' {
519 i += 1;
520 }
521 while i < len && chars[i].is_ascii_digit() {
522 i += 1;
523 }
524 if i + 1 < len && chars[i] == '.' && chars[i + 1].is_ascii_digit() {
528 i += 1;
529 while i < len && chars[i].is_ascii_digit() {
530 i += 1;
531 }
532 }
533
534 let text: String = chars[start..i].iter().collect();
535 let number: Number = serde_json::from_str(&text)
536 .map_err(|_| at_char(start, format!("a malformed number `{text}`")))?;
537 Ok((Token::Number(number), i))
538}
539
540fn lex_ident(chars: &[char], start: usize) -> (Token, usize) {
543 let len = chars.len();
544 let mut i = start;
545 while i < len && is_ident_continue(chars[i]) {
546 i += 1;
547 }
548 let text: String = chars[start..i].iter().collect();
549 let token = match text.as_str() {
550 "true" => Token::True,
551 "false" => Token::False,
552 "null" => Token::Null,
553 _ => Token::Ident(text),
554 };
555 (token, i)
556}
557
558fn at_char(position: usize, what: impl std::fmt::Display) -> ExprError {
559 ExprError::new(format!("found {what} at character {position}"))
560}
561
562struct Parser<'a> {
566 tokens: &'a [Spanned],
567 pos: usize,
568 end: usize,
571}
572
573impl Parser<'_> {
574 fn peek(&self) -> Option<&Token> {
575 self.tokens.get(self.pos).map(|s| &s.token)
576 }
577
578 fn position(&self) -> usize {
579 self.tokens.get(self.pos).map_or(self.end, |s| s.at)
580 }
581
582 fn advance(&mut self) {
583 self.pos += 1;
584 }
585
586 fn expect_end(&self) -> Result<(), ExprError> {
587 match self.peek() {
588 None => Ok(()),
589 Some(_) => Err(at_char(
590 self.position(),
591 "an unexpected trailing token; the expression already ended",
592 )),
593 }
594 }
595
596 fn parse_or(&mut self) -> Result<Bool, ExprError> {
597 let mut left = self.parse_and()?;
598 while matches!(self.peek(), Some(Token::OrOr)) {
599 self.advance();
600 let right = self.parse_and()?;
601 left = Bool::Or(Box::new(left), Box::new(right));
602 }
603 Ok(left)
604 }
605
606 fn parse_and(&mut self) -> Result<Bool, ExprError> {
607 let mut left = self.parse_unary()?;
608 while matches!(self.peek(), Some(Token::AndAnd)) {
609 self.advance();
610 let right = self.parse_unary()?;
611 left = Bool::And(Box::new(left), Box::new(right));
612 }
613 Ok(left)
614 }
615
616 fn parse_unary(&mut self) -> Result<Bool, ExprError> {
617 if matches!(self.peek(), Some(Token::Bang)) {
618 self.advance();
619 let inner = self.parse_unary()?;
620 Ok(Bool::Not(Box::new(inner)))
621 } else {
622 self.parse_atom()
623 }
624 }
625
626 fn parse_atom(&mut self) -> Result<Bool, ExprError> {
627 if matches!(self.peek(), Some(Token::LParen)) {
628 self.advance();
629 let inner = self.parse_or()?;
630 match self.peek() {
631 Some(Token::RParen) => {
632 self.advance();
633 Ok(inner)
634 }
635 _ => Err(at_char(self.position(), "a missing closing `)`")),
636 }
637 } else {
638 self.parse_comparison()
639 }
640 }
641
642 fn parse_comparison(&mut self) -> Result<Bool, ExprError> {
643 let left = self.parse_operand()?;
644 let op = match self.peek() {
645 Some(Token::EqEq) => CmpOp::Eq,
646 Some(Token::NotEq) => CmpOp::Ne,
647 Some(Token::Lt) => CmpOp::Lt,
648 Some(Token::Le) => CmpOp::Le,
649 Some(Token::Gt) => CmpOp::Gt,
650 Some(Token::Ge) => CmpOp::Ge,
651 _ => return Ok(Bool::Truthy(left)),
652 };
653 self.advance();
654 let right = self.parse_operand()?;
655 Ok(Bool::Compare { left, op, right })
656 }
657
658 fn parse_operand(&mut self) -> Result<Operand, ExprError> {
659 match self.peek() {
660 Some(Token::Number(n)) => {
661 let value = Operand::Literal(Value::Number(n.clone()));
662 self.advance();
663 Ok(value)
664 }
665 Some(Token::Str(s)) => {
666 let value = Operand::Literal(Value::String(s.clone()));
667 self.advance();
668 Ok(value)
669 }
670 Some(Token::True) => {
671 self.advance();
672 Ok(Operand::Literal(Value::Bool(true)))
673 }
674 Some(Token::False) => {
675 self.advance();
676 Ok(Operand::Literal(Value::Bool(false)))
677 }
678 Some(Token::Null) => {
679 self.advance();
680 Ok(Operand::Literal(Value::Null))
681 }
682 Some(Token::Ident(name)) => {
683 let name = name.clone();
684 self.advance();
685 self.parse_path(name)
686 }
687 _ => Err(at_char(
688 self.position(),
689 "a value or path where one was required",
690 )),
691 }
692 }
693
694 fn parse_path(&mut self, first: String) -> Result<Operand, ExprError> {
695 let mut segments = vec![Segment::Key(first)];
696 while matches!(self.peek(), Some(Token::Dot)) {
697 self.advance();
698 match self.peek() {
699 Some(Token::Ident(name)) => {
700 segments.push(Segment::Key(name.clone()));
701 self.advance();
702 }
703 Some(Token::Number(n)) => {
704 let index = n
705 .as_u64()
706 .and_then(|v| usize::try_from(v).ok())
707 .ok_or_else(|| {
708 at_char(
709 self.position(),
710 "a path segment that is not a non-negative integer index",
711 )
712 })?;
713 segments.push(Segment::Index(index));
714 self.advance();
715 }
716 _ => {
717 return Err(at_char(self.position(), "a missing path segment after `.`"));
718 }
719 }
720 }
721 Ok(Operand::Path(segments))
722 }
723}
724
725fn eval_bool(node: &Bool, root: &Value) -> bool {
730 match node {
731 Bool::Or(a, b) => eval_bool(a, root) || eval_bool(b, root),
732 Bool::And(a, b) => eval_bool(a, root) && eval_bool(b, root),
733 Bool::Not(a) => !eval_bool(a, root),
734 Bool::Compare { left, op, right } => eval_compare(left, *op, right, root),
735 Bool::Truthy(operand) => matches!(resolve(operand, root), Some(Value::Bool(true))),
736 }
737}
738
739fn resolve<'a>(operand: &'a Operand, root: &'a Value) -> Option<&'a Value> {
743 match operand {
744 Operand::Literal(value) => Some(value),
745 Operand::Path(segments) => resolve_path(segments, root),
746 }
747}
748
749fn resolve_path<'a>(segments: &[Segment], root: &'a Value) -> Option<&'a Value> {
755 let mut current = root;
756 for segment in segments {
757 current = match (current, segment) {
758 (Value::Object(map), Segment::Key(key)) => map.get(key)?,
759 (Value::Array(items), Segment::Index(index)) => items.get(*index)?,
760 _ => return None,
761 };
762 }
763 Some(current)
764}
765
766fn eval_compare(left: &Operand, op: CmpOp, right: &Operand, root: &Value) -> bool {
767 let (Some(l), Some(r)) = (resolve(left, root), resolve(right, root)) else {
770 return false;
771 };
772
773 match op {
774 CmpOp::Eq => values_equal(l, r),
775 CmpOp::Ne => !values_equal(l, r),
776 CmpOp::Lt => matches!(order(l, r), Some(Ordering::Less)),
777 CmpOp::Le => matches!(order(l, r), Some(Ordering::Less | Ordering::Equal)),
778 CmpOp::Gt => matches!(order(l, r), Some(Ordering::Greater)),
779 CmpOp::Ge => matches!(order(l, r), Some(Ordering::Greater | Ordering::Equal)),
780 }
781}
782
783fn values_equal(l: &Value, r: &Value) -> bool {
786 match (l, r) {
787 (Value::Number(a), Value::Number(b)) => number_cmp(a, b) == Some(Ordering::Equal),
788 _ => l == r,
789 }
790}
791
792fn order(l: &Value, r: &Value) -> Option<Ordering> {
795 match (l, r) {
796 (Value::Number(a), Value::Number(b)) => number_cmp(a, b),
797 (Value::String(a), Value::String(b)) => Some(a.cmp(b)),
798 _ => None,
799 }
800}
801
802fn number_cmp(a: &Number, b: &Number) -> Option<Ordering> {
805 if let (Some(x), Some(y)) = (as_i128(a), as_i128(b)) {
806 return Some(x.cmp(&y));
807 }
808 match (a.as_f64(), b.as_f64()) {
809 (Some(x), Some(y)) => x.partial_cmp(&y),
810 _ => None,
811 }
812}
813
814fn as_i128(n: &Number) -> Option<i128> {
816 n.as_u64()
817 .map(i128::from)
818 .or_else(|| n.as_i64().map(i128::from))
819}
820
821#[cfg(test)]
822mod tests {
823 use super::*;
824 use proptest::prelude::*;
825 use serde_json::json;
826
827 fn eval(expr: &str, value: &Value) -> bool {
828 parse(expr)
829 .unwrap_or_else(|e| panic!("`{expr}` should parse: {e}"))
830 .eval(value)
831 }
832
833 #[test]
836 fn sample_score_expression() {
837 assert!(eval("score > 0.8", &json!({"score": 0.9})));
838 assert!(!eval("score > 0.8", &json!({"score": 0.5})));
839 }
840
841 #[test]
844 fn every_comparison_operator() {
845 let v = json!({"n": 5});
846 assert!(eval("n == 5", &v));
847 assert!(!eval("n == 4", &v));
848 assert!(eval("n != 4", &v));
849 assert!(!eval("n != 5", &v));
850 assert!(eval("n < 6", &v));
851 assert!(!eval("n < 5", &v));
852 assert!(eval("n <= 5", &v));
853 assert!(eval("n > 4", &v));
854 assert!(!eval("n > 5", &v));
855 assert!(eval("n >= 5", &v));
856 }
857
858 #[test]
859 fn boolean_operators() {
860 let v = json!({"a": true, "b": false});
861 assert!(eval("a && !b", &v));
862 assert!(eval("a || b", &v));
863 assert!(!eval("!a", &v));
864 assert!(eval("!b", &v));
865 assert!(!eval("a && b", &v));
866 }
867
868 #[test]
871 fn and_binds_tighter_than_or() {
872 let v = json!({});
876 assert!(eval("true || false && false", &v));
877 assert!(!eval("(true || false) && false", &v));
878 }
879
880 #[test]
881 fn not_binds_tighter_than_and() {
882 let v = json!({"a": false, "b": true});
883 assert!(eval("!a && b", &v));
885 }
886
887 #[test]
888 fn comparison_binds_tighter_than_not() {
889 assert!(eval("!score > 0.8", &json!({"score": 0.5})));
891 assert!(!eval("!score > 0.8", &json!({"score": 0.9})));
892 }
893
894 #[test]
895 fn parentheses_group_boolean_expressions() {
896 let v = json!({"a": true, "b": false, "c": true});
897 assert!(eval("a && (b || c)", &v));
898 assert!(!eval("(a && b) || (b && c)", &v));
899 }
900
901 #[test]
902 fn comparisons_do_not_chain() {
903 assert!(parse("1 < 2 < 3").is_err());
904 }
905
906 #[test]
907 fn a_boolean_group_is_not_a_comparison_operand() {
908 assert!(parse("(a > b) > c").is_err());
909 }
910
911 #[test]
914 fn nested_and_indexed_paths() {
915 let v = json!({"output": {"score": 0.9}, "items": [{"score": 1}, {"score": 2}]});
916 assert!(eval("output.score > 0.8", &v));
917 assert!(eval("items.0.score == 1", &v));
918 assert!(eval("items.1.score == 2", &v));
919 }
920
921 #[test]
922 fn bare_path_is_truthy_only_for_boolean_true() {
923 assert!(eval("flag", &json!({"flag": true})));
924 assert!(!eval("flag", &json!({"flag": false})));
925 assert!(!eval("flag", &json!({"flag": 1})));
926 assert!(!eval("flag", &json!({"flag": "true"})));
927 assert!(!eval("flag", &json!({})));
928 }
929
930 #[test]
933 fn missing_path_makes_every_comparison_false() {
934 let v = json!({});
935 assert!(!eval("missing == 1", &v));
936 assert!(!eval("missing != 1", &v));
937 assert!(!eval("missing < 1", &v));
938 assert!(!eval("missing > 1", &v));
939 assert!(eval("!(missing > 1)", &v));
941 }
942
943 #[test]
944 fn missing_is_distinct_from_null() {
945 assert!(eval("x == null", &json!({"x": null})));
947 assert!(!eval("missing == null", &json!({})));
948 }
949
950 #[test]
951 fn descending_into_a_non_container_is_missing() {
952 let v = json!({"x": 5});
953 assert!(!eval("x.y == 1", &v));
954 assert!(!eval("x.0 == 1", &v));
955 }
956
957 #[test]
960 fn cross_type_equality_is_never_equal() {
961 assert!(!eval("x == 5", &json!({"x": "5"})));
962 assert!(eval("x != 5", &json!({"x": "5"})));
963 assert!(!eval("x == 1", &json!({"x": true})));
964 }
965
966 #[test]
967 fn cross_type_ordering_is_false() {
968 assert!(!eval("x < 5", &json!({"x": "5"})));
969 assert!(!eval("x > 5", &json!({"x": "5"})));
970 assert!(!eval("x < 5", &json!({"x": true})));
971 assert!(!eval("x < 5", &json!({"x": null})));
972 }
973
974 #[test]
975 fn strings_order_lexicographically() {
976 assert!(eval("x < \"b\"", &json!({"x": "a"})));
977 assert!(!eval("x < \"a\"", &json!({"x": "b"})));
978 assert!(eval("x == \"hi\"", &json!({"x": "hi"})));
979 }
980
981 #[test]
984 fn integer_and_float_equality() {
985 assert!(eval("x == 1", &json!({"x": 1.0})));
986 assert!(eval("x == 1.0", &json!({"x": 1})));
987 assert!(eval("x >= 1", &json!({"x": 1.0})));
988 }
989
990 #[test]
991 fn large_integers_compare_exactly() {
992 let big = json!({"a": 9_007_199_254_740_993_i64});
994 assert!(eval("a == 9007199254740993", &big));
995 assert!(!eval("a == 9007199254740992", &big));
996 }
997
998 #[test]
999 fn negative_and_signed_comparison() {
1000 assert!(eval("x < 0", &json!({"x": -3})));
1001 assert!(eval("x == -0.5", &json!({"x": -0.5})));
1002 assert!(eval("x > y", &json!({"x": 1, "y": -1})));
1003 }
1004
1005 #[test]
1008 fn the_length_cap_rejects_longer_input_and_names_it() {
1009 let ok = "a".repeat(MAX_EXPRESSION_LEN);
1010 assert!(parse(&ok).is_ok());
1011 let too_long = "a".repeat(MAX_EXPRESSION_LEN + 1);
1012 let err = parse(&too_long).expect_err("over the cap");
1013 assert!(
1014 err.message().contains(&MAX_EXPRESSION_LEN.to_string()),
1015 "names the cap: {err}"
1016 );
1017 }
1018
1019 #[test]
1020 fn assorted_syntax_errors() {
1021 for bad in [
1022 "",
1023 "&&",
1024 "a &&",
1025 "a && (b",
1026 "== 5",
1027 "a = 5",
1028 "a & b",
1029 "a | b",
1030 "1.2.3 == 1",
1031 "\"unterminated",
1032 "a.",
1033 "a.-1 == 1",
1034 ] {
1035 assert!(parse(bad).is_err(), "`{bad}` should be a parse error");
1036 }
1037 }
1038
1039 #[test]
1042 fn a_reference_resolves_a_top_level_and_nested_array() {
1043 let top = parse_reference("items").expect("`items` parses");
1044 assert_eq!(
1045 top.resolve(&json!({"items": [1, 2, 3]})),
1046 Some(&json!([1, 2, 3]))
1047 );
1048 let nested = parse_reference("output.items").expect("`output.items` parses");
1049 assert_eq!(
1050 nested.resolve(&json!({"output": {"items": ["a"]}})),
1051 Some(&json!(["a"]))
1052 );
1053 let indexed = parse_reference("results.0.items").expect("indexed path parses");
1054 assert_eq!(
1055 indexed.resolve(&json!({"results": [{"items": [true]}]})),
1056 Some(&json!([true]))
1057 );
1058 }
1059
1060 #[test]
1061 fn a_missing_reference_resolves_to_none() {
1062 let reference = parse_reference("items").expect("parses");
1063 assert_eq!(reference.resolve(&json!({})), None);
1064 assert_eq!(reference.resolve(&json!({"other": [1]})), None);
1065 let deep = parse_reference("x.items").expect("parses");
1067 assert_eq!(deep.resolve(&json!({"x": 5})), None);
1068 }
1069
1070 #[test]
1071 fn a_reference_may_name_any_json_value_not_only_arrays() {
1072 let reference = parse_reference("value").expect("parses");
1074 assert_eq!(reference.resolve(&json!({"value": 5})), Some(&json!(5)));
1075 assert_eq!(
1076 reference.resolve(&json!({"value": {"k": 1}})),
1077 Some(&json!({"k": 1}))
1078 );
1079 }
1080
1081 #[test]
1082 fn a_literal_or_malformed_reference_is_rejected() {
1083 assert!(
1084 parse_reference("5").is_err(),
1085 "a bare literal is not a path"
1086 );
1087 assert!(
1088 parse_reference("\"x\"").is_err(),
1089 "a string literal is not a path"
1090 );
1091 assert!(parse_reference("true").is_err(), "a keyword is not a path");
1092 assert!(
1093 parse_reference("items ==").is_err(),
1094 "trailing tokens rejected"
1095 );
1096 assert!(
1097 parse_reference("items.").is_err(),
1098 "a dangling dot is rejected"
1099 );
1100 assert!(
1101 parse_reference("").is_err(),
1102 "an empty reference is rejected"
1103 );
1104 }
1105
1106 proptest! {
1109 #[test]
1111 fn parsing_never_panics(input in ".*") {
1112 let _ = parse(&input);
1113 }
1114
1115 #[test]
1117 fn near_miss_parsing_never_panics(
1118 input in "[a-z0-9_. ()!&|<>=\"'.-]{0,80}"
1119 ) {
1120 let _ = parse(&input);
1121 }
1122
1123 #[test]
1126 fn cap_always_holds(input in "a{500,700}") {
1127 let result = parse(&input);
1128 if input.chars().count() > MAX_EXPRESSION_LEN {
1129 prop_assert!(result.is_err());
1130 }
1131 }
1132
1133 #[test]
1137 fn eval_never_panics(expr in expr_strategy(), value in json_strategy()) {
1138 if let Ok(parsed) = parse(&expr) {
1139 let _ = parsed.eval(&value);
1140 }
1141 }
1142 }
1143
1144 fn expr_strategy() -> impl Strategy<Value = String> {
1147 let leaf = prop_oneof![
1148 Just("score".to_string()),
1149 Just("output.score".to_string()),
1150 Just("items.0.score".to_string()),
1151 Just("flag".to_string()),
1152 Just("0.8".to_string()),
1153 Just("5".to_string()),
1154 Just("-1".to_string()),
1155 Just("true".to_string()),
1156 Just("null".to_string()),
1157 Just("\"hi\"".to_string()),
1158 ];
1159 let comparison = (leaf.clone(), "==|!=|<|<=|>|>=", leaf.clone())
1160 .prop_map(|(l, op, r)| format!("{l} {op} {r}"));
1161 let atom = prop_oneof![leaf, comparison];
1162 atom.prop_recursive(4, 32, 4, |inner| {
1163 prop_oneof![
1164 inner.clone().prop_map(|e| format!("!{e}")),
1165 inner.clone().prop_map(|e| format!("({e})")),
1166 (inner.clone(), inner.clone()).prop_map(|(a, b)| format!("{a} && {b}")),
1167 (inner.clone(), inner).prop_map(|(a, b)| format!("{a} || {b}")),
1168 ]
1169 })
1170 }
1171
1172 fn json_strategy() -> impl Strategy<Value = Value> {
1174 let leaf = prop_oneof![
1175 Just(Value::Null),
1176 any::<bool>().prop_map(Value::Bool),
1177 any::<i64>().prop_map(|n| json!(n)),
1178 any::<f64>()
1179 .prop_filter("finite", |f| f.is_finite())
1180 .prop_map(|f| json!(f)),
1181 ".*".prop_map(Value::String),
1182 ];
1183 leaf.prop_recursive(3, 16, 4, |inner| {
1184 prop_oneof![
1185 prop::collection::vec(inner.clone(), 0..4).prop_map(Value::Array),
1186 prop::collection::hash_map("[a-z]{1,5}", inner, 0..4)
1187 .prop_map(|m| Value::Object(m.into_iter().collect())),
1188 ]
1189 })
1190 }
1191}