Skip to main content

pick/selector/
filter.rs

1use super::extract::extract;
2use super::types::*;
3use crate::error::PickError;
4use regex::Regex;
5use serde_json::Value;
6
7/// Evaluate a filter expression against a JSON value.
8/// Returns `true` if the value passes the filter.
9pub 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
40/// Equality: coerce types where sensible.
41fn 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        // Cross-type: never equal
50        _ => false,
51    }
52}
53
54/// Ordering: only meaningful for same-type numeric or string comparisons.
55fn 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
63/// Regex match: lhs must be a string, rhs must be a string (pattern).
64fn 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
73/// Truthiness: consistent with jq semantics.
74/// `false` and `null` are falsy; everything else is truthy.
75fn 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    // ── value_eq ──
85
86    #[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    // ── value_cmp ──
130
131    #[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    // ── regex ──
161
162    #[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    // ── truthiness ──
211
212    #[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    // ── evaluate (integration) ──
230
231    #[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        // Empty path = identity (the value itself)
340        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        // Missing key during filter = extract fails = treat as falsy
376        // Actually extract returns Err, so evaluate returns Err too.
377        // Let's handle this: for truthy, missing = false
378        assert!(evaluate(&val, &expr).is_err() || !evaluate(&val, &expr).unwrap());
379    }
380
381    // ══════════════════════════════════════════════
382    // Additional coverage tests
383    // ══════════════════════════════════════════════
384
385    // ── value_eq edge cases ──
386
387    #[test]
388    fn eq_float_precision() {
389        // 0.1 + 0.2 ≈ 0.30000000000000004 — should be close to 0.3
390        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    // ── value_cmp edge cases ──
444
445    #[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        // Cross-type comparison returns None
480        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    // ── regex edge cases ──
498
499    #[test]
500    fn regex_empty_pattern() {
501        // Empty pattern matches everything
502        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        // Dot matches any char
527        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        // Pattern must be a string
536        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    // ── truthiness edge cases ──
583
584    #[test]
585    fn truthy_zero_is_truthy() {
586        // jq: 0 is truthy (only false and null are falsy)
587        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    // ── evaluate composite expressions ──
611
612    #[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        // not(not true) = true
651        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        // Comparing string with number → always false for >, <, >=, <=
752        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        // Non-zero number is truthy
786        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        // 0 is truthy in jq semantics
794        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        // "" is truthy in jq semantics
802        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}