Skip to main content

wfe_core/executor/
condition.rs

1use crate::WfeError;
2use crate::models::condition::{ComparisonOp, FieldComparison, StepCondition};
3
4/// Evaluate a step condition against workflow data.
5///
6/// Returns `Ok(true)` if the step should run, `Ok(false)` if it should be skipped.
7/// Missing field paths return `Ok(false)` (cascade skip behavior).
8pub fn evaluate(
9    condition: &StepCondition,
10    workflow_data: &serde_json::Value,
11) -> Result<bool, WfeError> {
12    match evaluate_inner(condition, workflow_data) {
13        Ok(result) => Ok(result),
14        Err(EvalError::FieldNotPresent) => Ok(false), // cascade skip
15        Err(EvalError::Wfe(e)) => Err(e),
16    }
17}
18
19/// Internal error type that distinguishes missing-field from real errors.
20#[derive(Debug)]
21enum EvalError {
22    FieldNotPresent,
23    Wfe(WfeError),
24}
25
26impl From<WfeError> for EvalError {
27    fn from(e: WfeError) -> Self {
28        EvalError::Wfe(e)
29    }
30}
31
32fn evaluate_inner(condition: &StepCondition, data: &serde_json::Value) -> Result<bool, EvalError> {
33    match condition {
34        StepCondition::All(conditions) => {
35            for c in conditions {
36                if !evaluate_inner(c, data)? {
37                    return Ok(false);
38                }
39            }
40            Ok(true)
41        }
42        StepCondition::Any(conditions) => {
43            for c in conditions {
44                if evaluate_inner(c, data)? {
45                    return Ok(true);
46                }
47            }
48            Ok(false)
49        }
50        StepCondition::None(conditions) => {
51            for c in conditions {
52                if evaluate_inner(c, data)? {
53                    return Ok(false);
54                }
55            }
56            Ok(true)
57        }
58        StepCondition::OneOf(conditions) => {
59            let mut count = 0;
60            for c in conditions {
61                if evaluate_inner(c, data)? {
62                    count += 1;
63                    if count > 1 {
64                        return Ok(false);
65                    }
66                }
67            }
68            Ok(count == 1)
69        }
70        StepCondition::Not(inner) => {
71            let result = evaluate_inner(inner, data)?;
72            Ok(!result)
73        }
74        StepCondition::Comparison(comp) => evaluate_comparison(comp, data),
75    }
76}
77
78/// Resolve a dot-separated field path against JSON data.
79///
80/// Path starts with `.` which is stripped, then split by `.`.
81/// Segments that parse as `usize` are treated as array indices.
82fn resolve_field_path<'a>(
83    path: &str,
84    data: &'a serde_json::Value,
85) -> Result<&'a serde_json::Value, EvalError> {
86    let path = path.strip_prefix('.').unwrap_or(path);
87    if path.is_empty() {
88        return Ok(data);
89    }
90
91    let segments: Vec<&str> = path.split('.').collect();
92
93    // Try resolving the full path first (for nested data like {"outputs": {"x": 1}}).
94    // If the first segment is "outputs"/"inputs" and doesn't exist as a key,
95    // strip it and resolve flat (for workflow data where outputs merge flat).
96    if segments.len() >= 2
97        && (segments[0] == "outputs" || segments[0] == "inputs")
98        && data.get(segments[0]).is_none()
99    {
100        return walk_segments(&segments[1..], data);
101    }
102
103    walk_segments(&segments, data)
104}
105
106fn walk_segments<'a>(
107    segments: &[&str],
108    data: &'a serde_json::Value,
109) -> Result<&'a serde_json::Value, EvalError> {
110    let mut current = data;
111
112    for segment in segments {
113        if let Ok(idx) = segment.parse::<usize>() {
114            match current.as_array() {
115                Some(arr) => {
116                    current = arr.get(idx).ok_or(EvalError::FieldNotPresent)?;
117                }
118                None => {
119                    return Err(EvalError::FieldNotPresent);
120                }
121            }
122        } else {
123            match current.as_object() {
124                Some(obj) => {
125                    current = obj.get(*segment).ok_or(EvalError::FieldNotPresent)?;
126                }
127                None => {
128                    return Err(EvalError::FieldNotPresent);
129                }
130            }
131        }
132    }
133
134    Ok(current)
135}
136
137fn evaluate_comparison(
138    comp: &FieldComparison,
139    data: &serde_json::Value,
140) -> Result<bool, EvalError> {
141    let resolved = resolve_field_path(&comp.field, data)?;
142
143    match &comp.operator {
144        ComparisonOp::IsNull => Ok(resolved.is_null()),
145        ComparisonOp::IsNotNull => Ok(!resolved.is_null()),
146        ComparisonOp::Equals => {
147            let expected = comp.value.as_ref().ok_or_else(|| {
148                EvalError::Wfe(WfeError::StepExecution(
149                    "Equals operator requires a value".into(),
150                ))
151            })?;
152            Ok(resolved == expected)
153        }
154        ComparisonOp::NotEquals => {
155            let expected = comp.value.as_ref().ok_or_else(|| {
156                EvalError::Wfe(WfeError::StepExecution(
157                    "NotEquals operator requires a value".into(),
158                ))
159            })?;
160            Ok(resolved != expected)
161        }
162        ComparisonOp::Gt => compare_numeric(resolved, comp, |a, b| a > b),
163        ComparisonOp::Gte => compare_numeric(resolved, comp, |a, b| a >= b),
164        ComparisonOp::Lt => compare_numeric(resolved, comp, |a, b| a < b),
165        ComparisonOp::Lte => compare_numeric(resolved, comp, |a, b| a <= b),
166        ComparisonOp::Contains => evaluate_contains(resolved, comp),
167    }
168}
169
170fn compare_numeric(
171    resolved: &serde_json::Value,
172    comp: &FieldComparison,
173    cmp_fn: fn(f64, f64) -> bool,
174) -> Result<bool, EvalError> {
175    let expected = comp.value.as_ref().ok_or_else(|| {
176        EvalError::Wfe(WfeError::StepExecution(format!(
177            "{:?} operator requires a value",
178            comp.operator
179        )))
180    })?;
181
182    let a = resolved.as_f64().ok_or_else(|| {
183        EvalError::Wfe(WfeError::StepExecution(format!(
184            "cannot compare non-numeric field value: {}",
185            resolved
186        )))
187    })?;
188
189    let b = expected.as_f64().ok_or_else(|| {
190        EvalError::Wfe(WfeError::StepExecution(format!(
191            "cannot compare with non-numeric value: {}",
192            expected
193        )))
194    })?;
195
196    Ok(cmp_fn(a, b))
197}
198
199fn evaluate_contains(
200    resolved: &serde_json::Value,
201    comp: &FieldComparison,
202) -> Result<bool, EvalError> {
203    let expected = comp.value.as_ref().ok_or_else(|| {
204        EvalError::Wfe(WfeError::StepExecution(
205            "Contains operator requires a value".into(),
206        ))
207    })?;
208
209    // String contains substring.
210    if let Some(s) = resolved.as_str()
211        && let Some(substr) = expected.as_str()
212    {
213        return Ok(s.contains(substr));
214    }
215
216    // Array contains element.
217    if let Some(arr) = resolved.as_array() {
218        return Ok(arr.contains(expected));
219    }
220
221    Err(EvalError::Wfe(WfeError::StepExecution(format!(
222        "Contains requires a string or array field, got {}",
223        value_type_name(resolved)
224    ))))
225}
226
227fn value_type_name(value: &serde_json::Value) -> &'static str {
228    match value {
229        serde_json::Value::Null => "null",
230        serde_json::Value::Bool(_) => "bool",
231        serde_json::Value::Number(_) => "number",
232        serde_json::Value::String(_) => "string",
233        serde_json::Value::Array(_) => "array",
234        serde_json::Value::Object(_) => "object",
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use crate::models::condition::{ComparisonOp, FieldComparison, StepCondition};
242    use serde_json::json;
243
244    // -- resolve_field_path tests --
245
246    #[test]
247    fn resolve_simple_field() {
248        let data = json!({"name": "alice"});
249        let result = resolve_field_path(".name", &data).unwrap();
250        assert_eq!(result, &json!("alice"));
251    }
252
253    #[test]
254    fn resolve_nested_field() {
255        let data = json!({"outputs": {"status": "success"}});
256        let result = resolve_field_path(".outputs.status", &data).unwrap();
257        assert_eq!(result, &json!("success"));
258    }
259
260    #[test]
261    fn resolve_missing_field() {
262        let data = json!({"name": "alice"});
263        let result = resolve_field_path(".missing", &data);
264        assert!(matches!(result, Err(EvalError::FieldNotPresent)));
265    }
266
267    #[test]
268    fn resolve_array_index() {
269        let data = json!({"items": [10, 20, 30]});
270        let result = resolve_field_path(".items.1", &data).unwrap();
271        assert_eq!(result, &json!(20));
272    }
273
274    #[test]
275    fn resolve_array_index_out_of_bounds() {
276        let data = json!({"items": [10, 20]});
277        let result = resolve_field_path(".items.5", &data);
278        assert!(matches!(result, Err(EvalError::FieldNotPresent)));
279    }
280
281    #[test]
282    fn resolve_deeply_nested() {
283        let data = json!({"a": {"b": {"c": {"d": 42}}}});
284        let result = resolve_field_path(".a.b.c.d", &data).unwrap();
285        assert_eq!(result, &json!(42));
286    }
287
288    #[test]
289    fn resolve_empty_path_returns_root() {
290        let data = json!({"x": 1});
291        let result = resolve_field_path(".", &data).unwrap();
292        assert_eq!(result, &data);
293    }
294
295    #[test]
296    fn resolve_field_on_non_object() {
297        let data = json!({"x": 42});
298        let result = resolve_field_path(".x.y", &data);
299        assert!(matches!(result, Err(EvalError::FieldNotPresent)));
300    }
301
302    // -- Comparison operator tests --
303
304    fn comp(field: &str, op: ComparisonOp, value: Option<serde_json::Value>) -> StepCondition {
305        StepCondition::Comparison(FieldComparison {
306            field: field.to_string(),
307            operator: op,
308            value,
309        })
310    }
311
312    #[test]
313    fn equals_match() {
314        let data = json!({"status": "ok"});
315        let cond = comp(".status", ComparisonOp::Equals, Some(json!("ok")));
316        assert!(evaluate(&cond, &data).unwrap());
317    }
318
319    #[test]
320    fn equals_mismatch() {
321        let data = json!({"status": "fail"});
322        let cond = comp(".status", ComparisonOp::Equals, Some(json!("ok")));
323        assert!(!evaluate(&cond, &data).unwrap());
324    }
325
326    #[test]
327    fn equals_numeric() {
328        let data = json!({"count": 5});
329        let cond = comp(".count", ComparisonOp::Equals, Some(json!(5)));
330        assert!(evaluate(&cond, &data).unwrap());
331    }
332
333    #[test]
334    fn not_equals_match() {
335        let data = json!({"status": "fail"});
336        let cond = comp(".status", ComparisonOp::NotEquals, Some(json!("ok")));
337        assert!(evaluate(&cond, &data).unwrap());
338    }
339
340    #[test]
341    fn not_equals_mismatch() {
342        let data = json!({"status": "ok"});
343        let cond = comp(".status", ComparisonOp::NotEquals, Some(json!("ok")));
344        assert!(!evaluate(&cond, &data).unwrap());
345    }
346
347    #[test]
348    fn gt_match() {
349        let data = json!({"count": 10});
350        let cond = comp(".count", ComparisonOp::Gt, Some(json!(5)));
351        assert!(evaluate(&cond, &data).unwrap());
352    }
353
354    #[test]
355    fn gt_mismatch() {
356        let data = json!({"count": 3});
357        let cond = comp(".count", ComparisonOp::Gt, Some(json!(5)));
358        assert!(!evaluate(&cond, &data).unwrap());
359    }
360
361    #[test]
362    fn gt_equal_is_false() {
363        let data = json!({"count": 5});
364        let cond = comp(".count", ComparisonOp::Gt, Some(json!(5)));
365        assert!(!evaluate(&cond, &data).unwrap());
366    }
367
368    #[test]
369    fn gte_match() {
370        let data = json!({"count": 5});
371        let cond = comp(".count", ComparisonOp::Gte, Some(json!(5)));
372        assert!(evaluate(&cond, &data).unwrap());
373    }
374
375    #[test]
376    fn gte_mismatch() {
377        let data = json!({"count": 4});
378        let cond = comp(".count", ComparisonOp::Gte, Some(json!(5)));
379        assert!(!evaluate(&cond, &data).unwrap());
380    }
381
382    #[test]
383    fn lt_match() {
384        let data = json!({"count": 3});
385        let cond = comp(".count", ComparisonOp::Lt, Some(json!(5)));
386        assert!(evaluate(&cond, &data).unwrap());
387    }
388
389    #[test]
390    fn lt_mismatch() {
391        let data = json!({"count": 7});
392        let cond = comp(".count", ComparisonOp::Lt, Some(json!(5)));
393        assert!(!evaluate(&cond, &data).unwrap());
394    }
395
396    #[test]
397    fn lte_match() {
398        let data = json!({"count": 5});
399        let cond = comp(".count", ComparisonOp::Lte, Some(json!(5)));
400        assert!(evaluate(&cond, &data).unwrap());
401    }
402
403    #[test]
404    fn lte_mismatch() {
405        let data = json!({"count": 6});
406        let cond = comp(".count", ComparisonOp::Lte, Some(json!(5)));
407        assert!(!evaluate(&cond, &data).unwrap());
408    }
409
410    #[test]
411    fn contains_string_match() {
412        let data = json!({"msg": "hello world"});
413        let cond = comp(".msg", ComparisonOp::Contains, Some(json!("world")));
414        assert!(evaluate(&cond, &data).unwrap());
415    }
416
417    #[test]
418    fn contains_string_mismatch() {
419        let data = json!({"msg": "hello world"});
420        let cond = comp(".msg", ComparisonOp::Contains, Some(json!("xyz")));
421        assert!(!evaluate(&cond, &data).unwrap());
422    }
423
424    #[test]
425    fn contains_array_match() {
426        let data = json!({"tags": ["a", "b", "c"]});
427        let cond = comp(".tags", ComparisonOp::Contains, Some(json!("b")));
428        assert!(evaluate(&cond, &data).unwrap());
429    }
430
431    #[test]
432    fn contains_array_mismatch() {
433        let data = json!({"tags": ["a", "b", "c"]});
434        let cond = comp(".tags", ComparisonOp::Contains, Some(json!("z")));
435        assert!(!evaluate(&cond, &data).unwrap());
436    }
437
438    #[test]
439    fn is_null_true() {
440        let data = json!({"val": null});
441        let cond = comp(".val", ComparisonOp::IsNull, None);
442        assert!(evaluate(&cond, &data).unwrap());
443    }
444
445    #[test]
446    fn is_null_false() {
447        let data = json!({"val": 42});
448        let cond = comp(".val", ComparisonOp::IsNull, None);
449        assert!(!evaluate(&cond, &data).unwrap());
450    }
451
452    #[test]
453    fn is_not_null_true() {
454        let data = json!({"val": 42});
455        let cond = comp(".val", ComparisonOp::IsNotNull, None);
456        assert!(evaluate(&cond, &data).unwrap());
457    }
458
459    #[test]
460    fn is_not_null_false() {
461        let data = json!({"val": null});
462        let cond = comp(".val", ComparisonOp::IsNotNull, None);
463        assert!(!evaluate(&cond, &data).unwrap());
464    }
465
466    // -- Combinator tests --
467
468    #[test]
469    fn all_both_true() {
470        let data = json!({"a": 1, "b": 2});
471        let cond = StepCondition::All(vec![
472            comp(".a", ComparisonOp::Equals, Some(json!(1))),
473            comp(".b", ComparisonOp::Equals, Some(json!(2))),
474        ]);
475        assert!(evaluate(&cond, &data).unwrap());
476    }
477
478    #[test]
479    fn all_one_false() {
480        let data = json!({"a": 1, "b": 99});
481        let cond = StepCondition::All(vec![
482            comp(".a", ComparisonOp::Equals, Some(json!(1))),
483            comp(".b", ComparisonOp::Equals, Some(json!(2))),
484        ]);
485        assert!(!evaluate(&cond, &data).unwrap());
486    }
487
488    #[test]
489    fn all_empty_is_true() {
490        let data = json!({});
491        let cond = StepCondition::All(vec![]);
492        assert!(evaluate(&cond, &data).unwrap());
493    }
494
495    #[test]
496    fn any_one_true() {
497        let data = json!({"a": 1, "b": 99});
498        let cond = StepCondition::Any(vec![
499            comp(".a", ComparisonOp::Equals, Some(json!(1))),
500            comp(".b", ComparisonOp::Equals, Some(json!(2))),
501        ]);
502        assert!(evaluate(&cond, &data).unwrap());
503    }
504
505    #[test]
506    fn any_none_true() {
507        let data = json!({"a": 99, "b": 99});
508        let cond = StepCondition::Any(vec![
509            comp(".a", ComparisonOp::Equals, Some(json!(1))),
510            comp(".b", ComparisonOp::Equals, Some(json!(2))),
511        ]);
512        assert!(!evaluate(&cond, &data).unwrap());
513    }
514
515    #[test]
516    fn any_empty_is_false() {
517        let data = json!({});
518        let cond = StepCondition::Any(vec![]);
519        assert!(!evaluate(&cond, &data).unwrap());
520    }
521
522    #[test]
523    fn none_all_false() {
524        let data = json!({"a": 99, "b": 99});
525        let cond = StepCondition::None(vec![
526            comp(".a", ComparisonOp::Equals, Some(json!(1))),
527            comp(".b", ComparisonOp::Equals, Some(json!(2))),
528        ]);
529        assert!(evaluate(&cond, &data).unwrap());
530    }
531
532    #[test]
533    fn none_one_true() {
534        let data = json!({"a": 1, "b": 99});
535        let cond = StepCondition::None(vec![
536            comp(".a", ComparisonOp::Equals, Some(json!(1))),
537            comp(".b", ComparisonOp::Equals, Some(json!(2))),
538        ]);
539        assert!(!evaluate(&cond, &data).unwrap());
540    }
541
542    #[test]
543    fn none_empty_is_true() {
544        let data = json!({});
545        let cond = StepCondition::None(vec![]);
546        assert!(evaluate(&cond, &data).unwrap());
547    }
548
549    #[test]
550    fn one_of_exactly_one_true() {
551        let data = json!({"a": 1, "b": 99});
552        let cond = StepCondition::OneOf(vec![
553            comp(".a", ComparisonOp::Equals, Some(json!(1))),
554            comp(".b", ComparisonOp::Equals, Some(json!(2))),
555        ]);
556        assert!(evaluate(&cond, &data).unwrap());
557    }
558
559    #[test]
560    fn one_of_both_true() {
561        let data = json!({"a": 1, "b": 2});
562        let cond = StepCondition::OneOf(vec![
563            comp(".a", ComparisonOp::Equals, Some(json!(1))),
564            comp(".b", ComparisonOp::Equals, Some(json!(2))),
565        ]);
566        assert!(!evaluate(&cond, &data).unwrap());
567    }
568
569    #[test]
570    fn one_of_none_true() {
571        let data = json!({"a": 99, "b": 99});
572        let cond = StepCondition::OneOf(vec![
573            comp(".a", ComparisonOp::Equals, Some(json!(1))),
574            comp(".b", ComparisonOp::Equals, Some(json!(2))),
575        ]);
576        assert!(!evaluate(&cond, &data).unwrap());
577    }
578
579    #[test]
580    fn not_true_becomes_false() {
581        let data = json!({"a": 1});
582        let cond = StepCondition::Not(Box::new(comp(".a", ComparisonOp::Equals, Some(json!(1)))));
583        assert!(!evaluate(&cond, &data).unwrap());
584    }
585
586    #[test]
587    fn not_false_becomes_true() {
588        let data = json!({"a": 99});
589        let cond = StepCondition::Not(Box::new(comp(".a", ComparisonOp::Equals, Some(json!(1)))));
590        assert!(evaluate(&cond, &data).unwrap());
591    }
592
593    // -- Cascade skip tests --
594
595    #[test]
596    fn missing_field_returns_false_cascade_skip() {
597        let data = json!({"other": 1});
598        let cond = comp(".missing", ComparisonOp::Equals, Some(json!(1)));
599        // Missing field -> cascade skip -> Ok(false)
600        assert!(!evaluate(&cond, &data).unwrap());
601    }
602
603    #[test]
604    fn missing_nested_field_returns_false() {
605        let data = json!({"a": {"b": 1}});
606        let cond = comp(".a.c", ComparisonOp::Equals, Some(json!(1)));
607        assert!(!evaluate(&cond, &data).unwrap());
608    }
609
610    #[test]
611    fn missing_field_in_all_returns_false() {
612        let data = json!({"a": 1});
613        let cond = StepCondition::All(vec![
614            comp(".a", ComparisonOp::Equals, Some(json!(1))),
615            comp(".missing", ComparisonOp::Equals, Some(json!(2))),
616        ]);
617        assert!(!evaluate(&cond, &data).unwrap());
618    }
619
620    // -- Nested combinator tests --
621
622    #[test]
623    fn nested_all_any_not() {
624        let data = json!({"a": 1, "b": 2, "c": 3});
625        // All(Any(a==1, a==99), Not(c==99))
626        let cond = StepCondition::All(vec![
627            StepCondition::Any(vec![
628                comp(".a", ComparisonOp::Equals, Some(json!(1))),
629                comp(".a", ComparisonOp::Equals, Some(json!(99))),
630            ]),
631            StepCondition::Not(Box::new(comp(".c", ComparisonOp::Equals, Some(json!(99))))),
632        ]);
633        assert!(evaluate(&cond, &data).unwrap());
634    }
635
636    #[test]
637    fn nested_any_of_alls() {
638        let data = json!({"x": 10, "y": 20});
639        // Any(All(x>5, y>25), All(x>5, y>15))
640        let cond = StepCondition::Any(vec![
641            StepCondition::All(vec![
642                comp(".x", ComparisonOp::Gt, Some(json!(5))),
643                comp(".y", ComparisonOp::Gt, Some(json!(25))),
644            ]),
645            StepCondition::All(vec![
646                comp(".x", ComparisonOp::Gt, Some(json!(5))),
647                comp(".y", ComparisonOp::Gt, Some(json!(15))),
648            ]),
649        ]);
650        assert!(evaluate(&cond, &data).unwrap());
651    }
652
653    // -- Edge cases / error cases --
654
655    #[test]
656    fn gt_on_string_errors() {
657        let data = json!({"name": "alice"});
658        let cond = comp(".name", ComparisonOp::Gt, Some(json!(5)));
659        let result = evaluate(&cond, &data);
660        assert!(result.is_err());
661    }
662
663    #[test]
664    fn gt_with_string_value_errors() {
665        let data = json!({"count": 5});
666        let cond = comp(".count", ComparisonOp::Gt, Some(json!("not a number")));
667        let result = evaluate(&cond, &data);
668        assert!(result.is_err());
669    }
670
671    #[test]
672    fn contains_on_number_errors() {
673        let data = json!({"count": 42});
674        let cond = comp(".count", ComparisonOp::Contains, Some(json!("4")));
675        let result = evaluate(&cond, &data);
676        assert!(result.is_err());
677    }
678
679    #[test]
680    fn equals_without_value_errors() {
681        let data = json!({"a": 1});
682        let cond = comp(".a", ComparisonOp::Equals, None);
683        let result = evaluate(&cond, &data);
684        assert!(result.is_err());
685    }
686
687    #[test]
688    fn not_equals_without_value_errors() {
689        let data = json!({"a": 1});
690        let cond = comp(".a", ComparisonOp::NotEquals, None);
691        let result = evaluate(&cond, &data);
692        assert!(result.is_err());
693    }
694
695    #[test]
696    fn gt_without_value_errors() {
697        let data = json!({"a": 1});
698        let cond = comp(".a", ComparisonOp::Gt, None);
699        let result = evaluate(&cond, &data);
700        assert!(result.is_err());
701    }
702
703    #[test]
704    fn contains_without_value_errors() {
705        let data = json!({"msg": "hello"});
706        let cond = comp(".msg", ComparisonOp::Contains, None);
707        let result = evaluate(&cond, &data);
708        assert!(result.is_err());
709    }
710
711    #[test]
712    fn equals_bool_values() {
713        let data = json!({"active": true});
714        let cond = comp(".active", ComparisonOp::Equals, Some(json!(true)));
715        assert!(evaluate(&cond, &data).unwrap());
716    }
717
718    #[test]
719    fn equals_null_value() {
720        let data = json!({"val": null});
721        let cond = comp(".val", ComparisonOp::Equals, Some(json!(null)));
722        assert!(evaluate(&cond, &data).unwrap());
723    }
724
725    #[test]
726    fn float_comparison() {
727        let data = json!({"score": 3.14});
728        assert!(evaluate(&comp(".score", ComparisonOp::Gt, Some(json!(3.0))), &data).unwrap());
729        assert!(evaluate(&comp(".score", ComparisonOp::Lt, Some(json!(4.0))), &data).unwrap());
730        assert!(
731            !evaluate(
732                &comp(".score", ComparisonOp::Equals, Some(json!(3.0))),
733                &data
734            )
735            .unwrap()
736        );
737    }
738
739    #[test]
740    fn contains_array_numeric_element() {
741        let data = json!({"nums": [1, 2, 3]});
742        let cond = comp(".nums", ComparisonOp::Contains, Some(json!(2)));
743        assert!(evaluate(&cond, &data).unwrap());
744    }
745
746    #[test]
747    fn one_of_empty_is_false() {
748        let data = json!({});
749        let cond = StepCondition::OneOf(vec![]);
750        assert!(!evaluate(&cond, &data).unwrap());
751    }
752}