1use crate::WfeError;
2use crate::models::condition::{ComparisonOp, FieldComparison, StepCondition};
3
4pub 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), Err(EvalError::Wfe(e)) => Err(e),
16 }
17}
18
19#[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
78fn 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 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 if let Some(s) = resolved.as_str()
211 && let Some(substr) = expected.as_str()
212 {
213 return Ok(s.contains(substr));
214 }
215
216 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 #[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 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 #[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 #[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 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 #[test]
623 fn nested_all_any_not() {
624 let data = json!({"a": 1, "b": 2, "c": 3});
625 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 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 #[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}