1use super::extract::extract;
2use super::types::*;
3use crate::error::PickError;
4use regex::Regex;
5use serde_json::Value;
6
7pub fn evaluate(value: &Value, expr: &FilterExpr) -> Result<bool, PickError> {
10 match expr {
11 FilterExpr::Condition(cond) => evaluate_condition(value, cond),
12 FilterExpr::Truthy(path) => {
13 let results = extract(value, path)?;
14 Ok(results.first().is_some_and(is_truthy))
15 }
16 FilterExpr::And(left, right) => Ok(evaluate(value, left)? && evaluate(value, right)?),
17 FilterExpr::Or(left, right) => Ok(evaluate(value, left)? || evaluate(value, right)?),
18 FilterExpr::Not(inner) => Ok(!evaluate(value, inner)?),
19 }
20}
21
22fn evaluate_condition(value: &Value, cond: &Condition) -> Result<bool, PickError> {
23 let results = extract(value, &cond.path)?;
24 let lhs = results.first().unwrap_or(&Value::Null);
25 Ok(compare(lhs, &cond.op, &cond.value))
26}
27
28fn compare(lhs: &Value, op: &CompareOp, rhs: &LiteralValue) -> bool {
29 match op {
30 CompareOp::Eq => value_eq(lhs, rhs),
31 CompareOp::Ne => !value_eq(lhs, rhs),
32 CompareOp::Gt => value_cmp(lhs, rhs).is_some_and(|o| o == std::cmp::Ordering::Greater),
33 CompareOp::Lt => value_cmp(lhs, rhs).is_some_and(|o| o == std::cmp::Ordering::Less),
34 CompareOp::Gte => value_cmp(lhs, rhs).is_some_and(|o| o != std::cmp::Ordering::Less),
35 CompareOp::Lte => value_cmp(lhs, rhs).is_some_and(|o| o != std::cmp::Ordering::Greater),
36 CompareOp::Match => value_regex_match(lhs, rhs),
37 }
38}
39
40fn value_eq(lhs: &Value, rhs: &LiteralValue) -> bool {
42 match (lhs, rhs) {
43 (Value::String(a), LiteralValue::String(b)) => a == b,
44 (Value::Bool(a), LiteralValue::Bool(b)) => a == b,
45 (Value::Null, LiteralValue::Null) => true,
46 (Value::Number(a), LiteralValue::Number(b)) => {
47 a.as_f64().is_some_and(|af| (af - b).abs() < f64::EPSILON)
48 }
49 _ => false,
51 }
52}
53
54fn value_cmp(lhs: &Value, rhs: &LiteralValue) -> Option<std::cmp::Ordering> {
56 match (lhs, rhs) {
57 (Value::Number(a), LiteralValue::Number(b)) => a.as_f64().and_then(|af| af.partial_cmp(b)),
58 (Value::String(a), LiteralValue::String(b)) => Some(a.as_str().cmp(b.as_str())),
59 _ => None,
60 }
61}
62
63fn value_regex_match(lhs: &Value, rhs: &LiteralValue) -> bool {
65 match (lhs, rhs) {
66 (Value::String(text), LiteralValue::String(pattern)) => {
67 Regex::new(pattern).is_ok_and(|re| re.is_match(text))
68 }
69 _ => false,
70 }
71}
72
73fn is_truthy(value: &Value) -> bool {
76 !matches!(value, Value::Null | Value::Bool(false))
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use serde_json::json;
83
84 #[test]
87 fn eq_strings() {
88 assert!(value_eq(
89 &json!("hello"),
90 &LiteralValue::String("hello".into())
91 ));
92 }
93
94 #[test]
95 fn eq_strings_differ() {
96 assert!(!value_eq(
97 &json!("hello"),
98 &LiteralValue::String("world".into())
99 ));
100 }
101
102 #[test]
103 fn eq_numbers() {
104 assert!(value_eq(&json!(42), &LiteralValue::Number(42.0)));
105 }
106
107 #[test]
108 fn eq_numbers_differ() {
109 assert!(!value_eq(&json!(42), &LiteralValue::Number(43.0)));
110 }
111
112 #[test]
113 fn eq_booleans() {
114 assert!(value_eq(&json!(true), &LiteralValue::Bool(true)));
115 assert!(!value_eq(&json!(true), &LiteralValue::Bool(false)));
116 }
117
118 #[test]
119 fn eq_nulls() {
120 assert!(value_eq(&json!(null), &LiteralValue::Null));
121 }
122
123 #[test]
124 fn eq_cross_type() {
125 assert!(!value_eq(&json!("42"), &LiteralValue::Number(42.0)));
126 assert!(!value_eq(&json!(1), &LiteralValue::Bool(true)));
127 }
128
129 #[test]
132 fn cmp_numbers() {
133 assert_eq!(
134 value_cmp(&json!(10), &LiteralValue::Number(5.0)),
135 Some(std::cmp::Ordering::Greater)
136 );
137 assert_eq!(
138 value_cmp(&json!(5), &LiteralValue::Number(10.0)),
139 Some(std::cmp::Ordering::Less)
140 );
141 assert_eq!(
142 value_cmp(&json!(5), &LiteralValue::Number(5.0)),
143 Some(std::cmp::Ordering::Equal)
144 );
145 }
146
147 #[test]
148 fn cmp_strings() {
149 assert_eq!(
150 value_cmp(&json!("banana"), &LiteralValue::String("apple".into())),
151 Some(std::cmp::Ordering::Greater)
152 );
153 }
154
155 #[test]
156 fn cmp_cross_type_none() {
157 assert_eq!(value_cmp(&json!("hello"), &LiteralValue::Number(5.0)), None);
158 }
159
160 #[test]
163 fn regex_match_simple() {
164 assert!(value_regex_match(
165 &json!("hello"),
166 &LiteralValue::String("^hel".into())
167 ));
168 }
169
170 #[test]
171 fn regex_no_match() {
172 assert!(!value_regex_match(
173 &json!("hello"),
174 &LiteralValue::String("^world".into())
175 ));
176 }
177
178 #[test]
179 fn regex_non_string_lhs() {
180 assert!(!value_regex_match(
181 &json!(42),
182 &LiteralValue::String("42".into())
183 ));
184 }
185
186 #[test]
187 fn regex_invalid_pattern() {
188 assert!(!value_regex_match(
189 &json!("hello"),
190 &LiteralValue::String("[invalid".into())
191 ));
192 }
193
194 #[test]
195 fn regex_case_sensitive() {
196 assert!(!value_regex_match(
197 &json!("Hello"),
198 &LiteralValue::String("^hello$".into())
199 ));
200 }
201
202 #[test]
203 fn regex_case_insensitive_flag() {
204 assert!(value_regex_match(
205 &json!("Hello"),
206 &LiteralValue::String("(?i)^hello$".into())
207 ));
208 }
209
210 #[test]
213 fn truthy_values() {
214 assert!(is_truthy(&json!(true)));
215 assert!(is_truthy(&json!(1)));
216 assert!(is_truthy(&json!(0)));
217 assert!(is_truthy(&json!("hello")));
218 assert!(is_truthy(&json!("")));
219 assert!(is_truthy(&json!([])));
220 assert!(is_truthy(&json!({})));
221 }
222
223 #[test]
224 fn falsy_values() {
225 assert!(!is_truthy(&json!(false)));
226 assert!(!is_truthy(&json!(null)));
227 }
228
229 #[test]
232 fn evaluate_simple_condition() {
233 let val = json!({"price": 150});
234 let expr = FilterExpr::Condition(Condition {
235 path: Selector::parse("price").unwrap(),
236 op: CompareOp::Gt,
237 value: LiteralValue::Number(100.0),
238 });
239 assert!(evaluate(&val, &expr).unwrap());
240 }
241
242 #[test]
243 fn evaluate_truthy_true() {
244 let val = json!({"active": true});
245 let expr = FilterExpr::Truthy(Selector::parse("active").unwrap());
246 assert!(evaluate(&val, &expr).unwrap());
247 }
248
249 #[test]
250 fn evaluate_truthy_false() {
251 let val = json!({"active": false});
252 let expr = FilterExpr::Truthy(Selector::parse("active").unwrap());
253 assert!(!evaluate(&val, &expr).unwrap());
254 }
255
256 #[test]
257 fn evaluate_truthy_null() {
258 let val = json!({"x": null});
259 let expr = FilterExpr::Truthy(Selector::parse("x").unwrap());
260 assert!(!evaluate(&val, &expr).unwrap());
261 }
262
263 #[test]
264 fn evaluate_not() {
265 let val = json!({"active": false});
266 let expr = FilterExpr::Not(Box::new(FilterExpr::Truthy(
267 Selector::parse("active").unwrap(),
268 )));
269 assert!(evaluate(&val, &expr).unwrap());
270 }
271
272 #[test]
273 fn evaluate_and_both_true() {
274 let val = json!({"a": 10, "b": 20});
275 let expr = FilterExpr::And(
276 Box::new(FilterExpr::Condition(Condition {
277 path: Selector::parse("a").unwrap(),
278 op: CompareOp::Gt,
279 value: LiteralValue::Number(5.0),
280 })),
281 Box::new(FilterExpr::Condition(Condition {
282 path: Selector::parse("b").unwrap(),
283 op: CompareOp::Gt,
284 value: LiteralValue::Number(15.0),
285 })),
286 );
287 assert!(evaluate(&val, &expr).unwrap());
288 }
289
290 #[test]
291 fn evaluate_and_one_false() {
292 let val = json!({"a": 10, "b": 5});
293 let expr = FilterExpr::And(
294 Box::new(FilterExpr::Condition(Condition {
295 path: Selector::parse("a").unwrap(),
296 op: CompareOp::Gt,
297 value: LiteralValue::Number(5.0),
298 })),
299 Box::new(FilterExpr::Condition(Condition {
300 path: Selector::parse("b").unwrap(),
301 op: CompareOp::Gt,
302 value: LiteralValue::Number(15.0),
303 })),
304 );
305 assert!(!evaluate(&val, &expr).unwrap());
306 }
307
308 #[test]
309 fn evaluate_or_one_true() {
310 let val = json!({"a": 10, "b": 5});
311 let expr = FilterExpr::Or(
312 Box::new(FilterExpr::Condition(Condition {
313 path: Selector::parse("a").unwrap(),
314 op: CompareOp::Gt,
315 value: LiteralValue::Number(100.0),
316 })),
317 Box::new(FilterExpr::Condition(Condition {
318 path: Selector::parse("b").unwrap(),
319 op: CompareOp::Gt,
320 value: LiteralValue::Number(1.0),
321 })),
322 );
323 assert!(evaluate(&val, &expr).unwrap());
324 }
325
326 #[test]
327 fn evaluate_nested_path() {
328 let val = json!({"user": {"age": 25}});
329 let expr = FilterExpr::Condition(Condition {
330 path: Selector::parse("user.age").unwrap(),
331 op: CompareOp::Gte,
332 value: LiteralValue::Number(18.0),
333 });
334 assert!(evaluate(&val, &expr).unwrap());
335 }
336
337 #[test]
338 fn evaluate_identity_comparison() {
339 let val = json!(42);
341 let expr = FilterExpr::Condition(Condition {
342 path: Selector { segments: vec![] },
343 op: CompareOp::Gt,
344 value: LiteralValue::Number(10.0),
345 });
346 assert!(evaluate(&val, &expr).unwrap());
347 }
348
349 #[test]
350 fn evaluate_string_comparison() {
351 let val = json!({"name": "banana"});
352 let expr = FilterExpr::Condition(Condition {
353 path: Selector::parse("name").unwrap(),
354 op: CompareOp::Gt,
355 value: LiteralValue::String("apple".into()),
356 });
357 assert!(evaluate(&val, &expr).unwrap());
358 }
359
360 #[test]
361 fn evaluate_regex() {
362 let val = json!({"email": "user@example.com"});
363 let expr = FilterExpr::Condition(Condition {
364 path: Selector::parse("email").unwrap(),
365 op: CompareOp::Match,
366 value: LiteralValue::String("@example\\.com$".into()),
367 });
368 assert!(evaluate(&val, &expr).unwrap());
369 }
370
371 #[test]
372 fn evaluate_missing_key_is_falsy() {
373 let val = json!({"a": 1});
374 let expr = FilterExpr::Truthy(Selector::parse("missing").unwrap());
375 assert!(evaluate(&val, &expr).is_err() || !evaluate(&val, &expr).unwrap());
379 }
380
381 #[test]
388 fn eq_float_precision() {
389 assert!(value_eq(&json!(0.3), &LiteralValue::Number(0.3)));
391 }
392
393 #[test]
394 fn eq_integer_as_float() {
395 assert!(value_eq(&json!(42), &LiteralValue::Number(42.0)));
396 }
397
398 #[test]
399 fn eq_zero() {
400 assert!(value_eq(&json!(0), &LiteralValue::Number(0.0)));
401 }
402
403 #[test]
404 fn eq_negative_number() {
405 assert!(value_eq(&json!(-5), &LiteralValue::Number(-5.0)));
406 }
407
408 #[test]
409 fn eq_string_empty() {
410 assert!(value_eq(&json!(""), &LiteralValue::String("".into())));
411 }
412
413 #[test]
414 fn eq_null_vs_false() {
415 assert!(!value_eq(&json!(null), &LiteralValue::Bool(false)));
416 }
417
418 #[test]
419 fn eq_null_vs_zero() {
420 assert!(!value_eq(&json!(null), &LiteralValue::Number(0.0)));
421 }
422
423 #[test]
424 fn eq_null_vs_empty_string() {
425 assert!(!value_eq(&json!(null), &LiteralValue::String("".into())));
426 }
427
428 #[test]
429 fn eq_bool_false_vs_false() {
430 assert!(value_eq(&json!(false), &LiteralValue::Bool(false)));
431 }
432
433 #[test]
434 fn eq_number_vs_string() {
435 assert!(!value_eq(&json!(42), &LiteralValue::String("42".into())));
436 }
437
438 #[test]
439 fn eq_bool_vs_number() {
440 assert!(!value_eq(&json!(true), &LiteralValue::Number(1.0)));
441 }
442
443 #[test]
446 fn cmp_numbers_equal() {
447 assert_eq!(
448 value_cmp(&json!(10), &LiteralValue::Number(10.0)),
449 Some(std::cmp::Ordering::Equal)
450 );
451 }
452
453 #[test]
454 fn cmp_negative_numbers() {
455 assert_eq!(
456 value_cmp(&json!(-5), &LiteralValue::Number(-3.0)),
457 Some(std::cmp::Ordering::Less)
458 );
459 }
460
461 #[test]
462 fn cmp_strings_equal() {
463 assert_eq!(
464 value_cmp(&json!("abc"), &LiteralValue::String("abc".into())),
465 Some(std::cmp::Ordering::Equal)
466 );
467 }
468
469 #[test]
470 fn cmp_empty_strings() {
471 assert_eq!(
472 value_cmp(&json!(""), &LiteralValue::String("".into())),
473 Some(std::cmp::Ordering::Equal)
474 );
475 }
476
477 #[test]
478 fn cmp_bool_vs_number() {
479 assert_eq!(value_cmp(&json!(true), &LiteralValue::Number(1.0)), None);
481 }
482
483 #[test]
484 fn cmp_null_vs_anything() {
485 assert_eq!(value_cmp(&json!(null), &LiteralValue::Number(0.0)), None);
486 assert_eq!(value_cmp(&json!(null), &LiteralValue::Null), None);
487 }
488
489 #[test]
490 fn cmp_float_numbers() {
491 assert_eq!(
492 value_cmp(&json!(3.14), &LiteralValue::Number(2.71)),
493 Some(std::cmp::Ordering::Greater)
494 );
495 }
496
497 #[test]
500 fn regex_empty_pattern() {
501 assert!(value_regex_match(
503 &json!("anything"),
504 &LiteralValue::String("".into())
505 ));
506 }
507
508 #[test]
509 fn regex_full_match() {
510 assert!(value_regex_match(
511 &json!("hello"),
512 &LiteralValue::String("^hello$".into())
513 ));
514 }
515
516 #[test]
517 fn regex_partial_match() {
518 assert!(value_regex_match(
519 &json!("hello world"),
520 &LiteralValue::String("world".into())
521 ));
522 }
523
524 #[test]
525 fn regex_special_chars_in_pattern() {
526 assert!(value_regex_match(
528 &json!("a.b"),
529 &LiteralValue::String("a.b".into())
530 ));
531 }
532
533 #[test]
534 fn regex_non_string_rhs() {
535 assert!(!value_regex_match(
537 &json!("hello"),
538 &LiteralValue::Number(42.0)
539 ));
540 }
541
542 #[test]
543 fn regex_null_lhs() {
544 assert!(!value_regex_match(
545 &json!(null),
546 &LiteralValue::String(".*".into())
547 ));
548 }
549
550 #[test]
551 fn regex_bool_lhs() {
552 assert!(!value_regex_match(
553 &json!(true),
554 &LiteralValue::String("true".into())
555 ));
556 }
557
558 #[test]
559 fn regex_unicode_pattern() {
560 assert!(value_regex_match(
561 &json!("hello 🌍"),
562 &LiteralValue::String("🌍".into())
563 ));
564 }
565
566 #[test]
567 fn regex_digit_class() {
568 assert!(value_regex_match(
569 &json!("abc123"),
570 &LiteralValue::String("\\d+".into())
571 ));
572 }
573
574 #[test]
575 fn regex_word_boundary() {
576 assert!(value_regex_match(
577 &json!("hello world"),
578 &LiteralValue::String("\\bworld\\b".into())
579 ));
580 }
581
582 #[test]
585 fn truthy_zero_is_truthy() {
586 assert!(is_truthy(&json!(0)));
588 }
589
590 #[test]
591 fn truthy_empty_string_is_truthy() {
592 assert!(is_truthy(&json!("")));
593 }
594
595 #[test]
596 fn truthy_empty_array_is_truthy() {
597 assert!(is_truthy(&json!([])));
598 }
599
600 #[test]
601 fn truthy_empty_object_is_truthy() {
602 assert!(is_truthy(&json!({})));
603 }
604
605 #[test]
606 fn truthy_negative_number_is_truthy() {
607 assert!(is_truthy(&json!(-1)));
608 }
609
610 #[test]
613 fn evaluate_or_both_false() {
614 let val = json!({"a": 0, "b": 0});
615 let expr = FilterExpr::Or(
616 Box::new(FilterExpr::Condition(Condition {
617 path: Selector::parse("a").unwrap(),
618 op: CompareOp::Gt,
619 value: LiteralValue::Number(100.0),
620 })),
621 Box::new(FilterExpr::Condition(Condition {
622 path: Selector::parse("b").unwrap(),
623 op: CompareOp::Gt,
624 value: LiteralValue::Number(100.0),
625 })),
626 );
627 assert!(!evaluate(&val, &expr).unwrap());
628 }
629
630 #[test]
631 fn evaluate_and_both_false() {
632 let val = json!({"a": 0, "b": 0});
633 let expr = FilterExpr::And(
634 Box::new(FilterExpr::Condition(Condition {
635 path: Selector::parse("a").unwrap(),
636 op: CompareOp::Gt,
637 value: LiteralValue::Number(100.0),
638 })),
639 Box::new(FilterExpr::Condition(Condition {
640 path: Selector::parse("b").unwrap(),
641 op: CompareOp::Gt,
642 value: LiteralValue::Number(100.0),
643 })),
644 );
645 assert!(!evaluate(&val, &expr).unwrap());
646 }
647
648 #[test]
649 fn evaluate_not_of_not() {
650 let val = json!({"active": true});
652 let expr = FilterExpr::Not(Box::new(FilterExpr::Not(Box::new(FilterExpr::Truthy(
653 Selector::parse("active").unwrap(),
654 )))));
655 assert!(evaluate(&val, &expr).unwrap());
656 }
657
658 #[test]
659 fn evaluate_triple_and() {
660 let val = json!({"a": 10, "b": 20, "c": 30});
661 let expr = FilterExpr::And(
662 Box::new(FilterExpr::And(
663 Box::new(FilterExpr::Condition(Condition {
664 path: Selector::parse("a").unwrap(),
665 op: CompareOp::Gt,
666 value: LiteralValue::Number(5.0),
667 })),
668 Box::new(FilterExpr::Condition(Condition {
669 path: Selector::parse("b").unwrap(),
670 op: CompareOp::Gt,
671 value: LiteralValue::Number(15.0),
672 })),
673 )),
674 Box::new(FilterExpr::Condition(Condition {
675 path: Selector::parse("c").unwrap(),
676 op: CompareOp::Gt,
677 value: LiteralValue::Number(25.0),
678 })),
679 );
680 assert!(evaluate(&val, &expr).unwrap());
681 }
682
683 #[test]
684 fn evaluate_gte_equal_values() {
685 let val = json!({"x": 10});
686 let expr = FilterExpr::Condition(Condition {
687 path: Selector::parse("x").unwrap(),
688 op: CompareOp::Gte,
689 value: LiteralValue::Number(10.0),
690 });
691 assert!(evaluate(&val, &expr).unwrap());
692 }
693
694 #[test]
695 fn evaluate_lte_equal_values() {
696 let val = json!({"x": 10});
697 let expr = FilterExpr::Condition(Condition {
698 path: Selector::parse("x").unwrap(),
699 op: CompareOp::Lte,
700 value: LiteralValue::Number(10.0),
701 });
702 assert!(evaluate(&val, &expr).unwrap());
703 }
704
705 #[test]
706 fn evaluate_gt_equal_is_false() {
707 let val = json!({"x": 10});
708 let expr = FilterExpr::Condition(Condition {
709 path: Selector::parse("x").unwrap(),
710 op: CompareOp::Gt,
711 value: LiteralValue::Number(10.0),
712 });
713 assert!(!evaluate(&val, &expr).unwrap());
714 }
715
716 #[test]
717 fn evaluate_lt_equal_is_false() {
718 let val = json!({"x": 10});
719 let expr = FilterExpr::Condition(Condition {
720 path: Selector::parse("x").unwrap(),
721 op: CompareOp::Lt,
722 value: LiteralValue::Number(10.0),
723 });
724 assert!(!evaluate(&val, &expr).unwrap());
725 }
726
727 #[test]
728 fn evaluate_ne_same_string() {
729 let val = json!({"name": "Alice"});
730 let expr = FilterExpr::Condition(Condition {
731 path: Selector::parse("name").unwrap(),
732 op: CompareOp::Ne,
733 value: LiteralValue::String("Alice".into()),
734 });
735 assert!(!evaluate(&val, &expr).unwrap());
736 }
737
738 #[test]
739 fn evaluate_ne_different_string() {
740 let val = json!({"name": "Alice"});
741 let expr = FilterExpr::Condition(Condition {
742 path: Selector::parse("name").unwrap(),
743 op: CompareOp::Ne,
744 value: LiteralValue::String("Bob".into()),
745 });
746 assert!(evaluate(&val, &expr).unwrap());
747 }
748
749 #[test]
750 fn evaluate_cross_type_comparison_false() {
751 let val = json!({"name": "Alice"});
753 let expr = FilterExpr::Condition(Condition {
754 path: Selector::parse("name").unwrap(),
755 op: CompareOp::Gt,
756 value: LiteralValue::Number(0.0),
757 });
758 assert!(!evaluate(&val, &expr).unwrap());
759 }
760
761 #[test]
762 fn evaluate_regex_in_filter() {
763 let val = json!({"email": "test@example.com"});
764 let expr = FilterExpr::Condition(Condition {
765 path: Selector::parse("email").unwrap(),
766 op: CompareOp::Match,
767 value: LiteralValue::String("^[a-z]+@".into()),
768 });
769 assert!(evaluate(&val, &expr).unwrap());
770 }
771
772 #[test]
773 fn evaluate_regex_no_match_in_filter() {
774 let val = json!({"email": "test@example.com"});
775 let expr = FilterExpr::Condition(Condition {
776 path: Selector::parse("email").unwrap(),
777 op: CompareOp::Match,
778 value: LiteralValue::String("^[0-9]+$".into()),
779 });
780 assert!(!evaluate(&val, &expr).unwrap());
781 }
782
783 #[test]
784 fn evaluate_truthy_with_number() {
785 let val = json!({"count": 42});
787 let expr = FilterExpr::Truthy(Selector::parse("count").unwrap());
788 assert!(evaluate(&val, &expr).unwrap());
789 }
790
791 #[test]
792 fn evaluate_truthy_with_zero() {
793 let val = json!({"count": 0});
795 let expr = FilterExpr::Truthy(Selector::parse("count").unwrap());
796 assert!(evaluate(&val, &expr).unwrap());
797 }
798
799 #[test]
800 fn evaluate_truthy_with_empty_string() {
801 let val = json!({"name": ""});
803 let expr = FilterExpr::Truthy(Selector::parse("name").unwrap());
804 assert!(evaluate(&val, &expr).unwrap());
805 }
806
807 #[test]
808 fn evaluate_truthy_with_empty_array() {
809 let val = json!({"items": []});
810 let expr = FilterExpr::Truthy(Selector::parse("items").unwrap());
811 assert!(evaluate(&val, &expr).unwrap());
812 }
813}