1use serde_json::Value;
2
3use crate::io_processing::resolve_path;
4
5pub fn evaluate_choice(state_def: &Value, input: &Value) -> Option<String> {
8 if let Some(choices) = state_def["Choices"].as_array() {
9 for choice in choices {
10 if evaluate_rule(choice, input) {
11 return choice["Next"].as_str().map(|s| s.to_string());
12 }
13 }
14 }
15
16 state_def["Default"].as_str().map(|s| s.to_string())
18}
19
20fn evaluate_rule(rule: &Value, input: &Value) -> bool {
22 if let Some(result) = evaluate_logical(rule, input) {
23 return result;
24 }
25
26 let variable = match rule["Variable"].as_str() {
27 Some(v) => v,
28 None => return false,
29 };
30 let value = resolve_path(input, variable);
31
32 if let Some(result) = evaluate_presence_or_type(rule, input, variable, &value) {
33 return result;
34 }
35 if let Some(result) = evaluate_string_comparison(rule, input, &value) {
36 return result;
37 }
38 if let Some(result) = evaluate_numeric_comparison(rule, input, &value) {
39 return result;
40 }
41 if let Some(result) = evaluate_boolean_comparison(rule, input, &value) {
42 return result;
43 }
44 if let Some(result) = evaluate_timestamp_comparison(rule, &value) {
45 return result;
46 }
47
48 false
49}
50
51fn evaluate_logical(rule: &Value, input: &Value) -> Option<bool> {
52 if let Some(and_rules) = rule["And"].as_array() {
53 return Some(and_rules.iter().all(|r| evaluate_rule(r, input)));
54 }
55 if let Some(or_rules) = rule["Or"].as_array() {
56 return Some(or_rules.iter().any(|r| evaluate_rule(r, input)));
57 }
58 if rule.get("Not").is_some() {
59 return Some(!evaluate_rule(&rule["Not"], input));
60 }
61 None
62}
63
64fn evaluate_presence_or_type(
65 rule: &Value,
66 input: &Value,
67 variable: &str,
68 value: &Value,
69) -> Option<bool> {
70 if let Some(expected) = rule.get("IsPresent") {
71 let is_present = field_exists_in_input(input, variable);
74 return Some(expected.as_bool().unwrap_or(false) == is_present);
75 }
76 if let Some(expected) = rule.get("IsNull") {
77 return Some(expected.as_bool().unwrap_or(false) == value.is_null());
78 }
79 if let Some(expected) = rule.get("IsNumeric") {
80 return Some(expected.as_bool().unwrap_or(false) == value.is_number());
81 }
82 if let Some(expected) = rule.get("IsString") {
83 return Some(expected.as_bool().unwrap_or(false) == value.is_string());
84 }
85 if let Some(expected) = rule.get("IsBoolean") {
86 return Some(expected.as_bool().unwrap_or(false) == value.is_boolean());
87 }
88 if let Some(expected) = rule.get("IsTimestamp") {
89 let is_ts = value
90 .as_str()
91 .map(|s| chrono::DateTime::parse_from_rfc3339(s).is_ok())
92 .unwrap_or(false);
93 return Some(expected.as_bool().unwrap_or(false) == is_ts);
94 }
95 None
96}
97
98fn evaluate_string_comparison(rule: &Value, input: &Value, value: &Value) -> Option<bool> {
99 if let Some(expected) = rule["StringEquals"].as_str() {
100 return Some(value.as_str() == Some(expected));
101 }
102 if let Some(path) = rule["StringEqualsPath"].as_str() {
103 let other = resolve_path(input, path);
104 return Some(value.as_str().is_some() && value.as_str() == other.as_str());
105 }
106 if let Some(expected) = rule["StringLessThan"].as_str() {
107 return Some(value.as_str().is_some_and(|v| v < expected));
108 }
109 if let Some(expected) = rule["StringGreaterThan"].as_str() {
110 return Some(value.as_str().is_some_and(|v| v > expected));
111 }
112 if let Some(expected) = rule["StringLessThanEquals"].as_str() {
113 return Some(value.as_str().is_some_and(|v| v <= expected));
114 }
115 if let Some(expected) = rule["StringGreaterThanEquals"].as_str() {
116 return Some(value.as_str().is_some_and(|v| v >= expected));
117 }
118 if let Some(pattern) = rule["StringMatches"].as_str() {
119 return Some(value.as_str().is_some_and(|v| string_matches(v, pattern)));
120 }
121 None
122}
123
124fn evaluate_numeric_comparison(rule: &Value, input: &Value, value: &Value) -> Option<bool> {
125 if let Some(expected) = rule["NumericEquals"].as_f64() {
126 return Some(value.as_f64() == Some(expected));
127 }
128 if let Some(path) = rule["NumericEqualsPath"].as_str() {
129 let other = resolve_path(input, path);
130 return Some(value.as_f64().is_some() && value.as_f64() == other.as_f64());
131 }
132 if let Some(expected) = rule["NumericLessThan"].as_f64() {
133 return Some(value.as_f64().is_some_and(|v| v < expected));
134 }
135 if let Some(expected) = rule["NumericGreaterThan"].as_f64() {
136 return Some(value.as_f64().is_some_and(|v| v > expected));
137 }
138 if let Some(expected) = rule["NumericLessThanEquals"].as_f64() {
139 return Some(value.as_f64().is_some_and(|v| v <= expected));
140 }
141 if let Some(expected) = rule["NumericGreaterThanEquals"].as_f64() {
142 return Some(value.as_f64().is_some_and(|v| v >= expected));
143 }
144 None
145}
146
147fn evaluate_boolean_comparison(rule: &Value, input: &Value, value: &Value) -> Option<bool> {
148 if let Some(expected) = rule["BooleanEquals"].as_bool() {
149 return Some(value.as_bool() == Some(expected));
150 }
151 if let Some(path) = rule["BooleanEqualsPath"].as_str() {
152 let other = resolve_path(input, path);
153 return Some(value.as_bool().is_some() && value.as_bool() == other.as_bool());
154 }
155 None
156}
157
158fn evaluate_timestamp_comparison(rule: &Value, value: &Value) -> Option<bool> {
159 if let Some(expected) = rule["TimestampEquals"].as_str() {
160 return Some(compare_timestamps(value, expected, |a, b| a == b));
161 }
162 if let Some(expected) = rule["TimestampLessThan"].as_str() {
163 return Some(compare_timestamps(value, expected, |a, b| a < b));
164 }
165 if let Some(expected) = rule["TimestampGreaterThan"].as_str() {
166 return Some(compare_timestamps(value, expected, |a, b| a > b));
167 }
168 if let Some(expected) = rule["TimestampLessThanEquals"].as_str() {
169 return Some(compare_timestamps(value, expected, |a, b| a <= b));
170 }
171 if let Some(expected) = rule["TimestampGreaterThanEquals"].as_str() {
172 return Some(compare_timestamps(value, expected, |a, b| a >= b));
173 }
174 None
175}
176
177fn compare_timestamps<F>(value: &Value, expected: &str, cmp: F) -> bool
179where
180 F: Fn(chrono::DateTime<chrono::FixedOffset>, chrono::DateTime<chrono::FixedOffset>) -> bool,
181{
182 let val_str = match value.as_str() {
183 Some(s) => s,
184 None => return false,
185 };
186 let val_ts = match chrono::DateTime::parse_from_rfc3339(val_str) {
187 Ok(t) => t,
188 Err(_) => return false,
189 };
190 let exp_ts = match chrono::DateTime::parse_from_rfc3339(expected) {
191 Ok(t) => t,
192 Err(_) => return false,
193 };
194 cmp(val_ts, exp_ts)
195}
196
197fn string_matches(value: &str, pattern: &str) -> bool {
200 let compiled = compile_glob_pattern(pattern);
201 glob_dp_match(&value.chars().collect::<Vec<_>>(), &compiled)
202}
203
204fn compile_glob_pattern(pattern: &str) -> Vec<GlobToken> {
208 let mut out = Vec::new();
209 let chars: Vec<char> = pattern.chars().collect();
210 let mut i = 0;
211 while i < chars.len() {
212 if chars[i] == '\\' && i + 1 < chars.len() && chars[i + 1] == '*' {
213 out.push(GlobToken::Char('*'));
214 i += 2;
215 } else if chars[i] == '*' {
216 out.push(GlobToken::Wildcard);
217 i += 1;
218 } else {
219 out.push(GlobToken::Char(chars[i]));
220 i += 1;
221 }
222 }
223 out
224}
225
226fn glob_dp_match(value: &[char], pattern: &[GlobToken]) -> bool {
228 let m = value.len();
229 let n = pattern.len();
230 let mut dp = vec![vec![false; n + 1]; m + 1];
231 dp[0][0] = true;
232
233 for j in 1..=n {
234 if matches!(pattern[j - 1], GlobToken::Wildcard) {
235 dp[0][j] = dp[0][j - 1];
236 }
237 }
238
239 for i in 1..=m {
240 for j in 1..=n {
241 match pattern[j - 1] {
242 GlobToken::Wildcard => {
243 dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
244 }
245 GlobToken::Char(c) if c == value[i - 1] => {
246 dp[i][j] = dp[i - 1][j - 1];
247 }
248 GlobToken::Char(_) => {}
249 }
250 }
251 }
252
253 dp[m][n]
254}
255
256#[derive(Clone, Copy)]
257enum GlobToken {
258 Char(char),
259 Wildcard,
260}
261
262fn field_exists_in_input(root: &Value, path: &str) -> bool {
267 if path == "$" {
268 return true;
269 }
270 let path = path.strip_prefix("$.").unwrap_or(path);
271 let parts: Vec<&str> = path.split('.').collect();
272 let mut current = root;
273
274 for (i, part) in parts.iter().enumerate() {
275 let is_last = i == parts.len() - 1;
276
277 if let Some(bracket_pos) = part.find('[') {
279 let field_name = &part[..bracket_pos];
280 if !part.ends_with(']') {
282 return false; }
284 let close_bracket = part.len() - 1;
285 if close_bracket <= bracket_pos {
286 return false;
287 }
288 let idx_str = &part[bracket_pos + 1..close_bracket];
289
290 match current.get(field_name) {
291 Some(arr) => {
292 if let Ok(idx) = idx_str.parse::<usize>() {
293 if is_last {
294 return arr.as_array().is_some_and(|a| idx < a.len());
295 }
296 match arr.get(idx) {
297 Some(v) => current = v,
298 None => return false,
299 }
300 } else {
301 return false;
302 }
303 }
304 None => return false,
305 }
306 } else if is_last {
307 return match current.as_object() {
308 Some(obj) => obj.contains_key(*part),
309 None => false,
310 };
311 } else {
312 match current.get(*part) {
313 Some(v) => current = v,
314 None => return false,
315 }
316 }
317 }
318 false
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324 use serde_json::json;
325
326 #[test]
327 fn test_string_equals() {
328 let rule = json!({
329 "Variable": "$.status",
330 "StringEquals": "active",
331 "Next": "Active"
332 });
333 let input = json!({"status": "active"});
334 assert!(evaluate_rule(&rule, &input));
335
336 let input = json!({"status": "inactive"});
337 assert!(!evaluate_rule(&rule, &input));
338 }
339
340 #[test]
341 fn test_numeric_greater_than() {
342 let rule = json!({
343 "Variable": "$.count",
344 "NumericGreaterThan": 10,
345 "Next": "High"
346 });
347 let input = json!({"count": 15});
348 assert!(evaluate_rule(&rule, &input));
349
350 let input = json!({"count": 5});
351 assert!(!evaluate_rule(&rule, &input));
352 }
353
354 #[test]
355 fn test_boolean_equals() {
356 let rule = json!({
357 "Variable": "$.enabled",
358 "BooleanEquals": true,
359 "Next": "Enabled"
360 });
361 let input = json!({"enabled": true});
362 assert!(evaluate_rule(&rule, &input));
363
364 let input = json!({"enabled": false});
365 assert!(!evaluate_rule(&rule, &input));
366 }
367
368 #[test]
369 fn test_and_operator() {
370 let rule = json!({
371 "And": [
372 {"Variable": "$.a", "NumericGreaterThan": 0},
373 {"Variable": "$.b", "NumericLessThan": 100}
374 ],
375 "Next": "Both"
376 });
377 let input = json!({"a": 5, "b": 50});
378 assert!(evaluate_rule(&rule, &input));
379
380 let input = json!({"a": -1, "b": 50});
381 assert!(!evaluate_rule(&rule, &input));
382 }
383
384 #[test]
385 fn test_or_operator() {
386 let rule = json!({
387 "Or": [
388 {"Variable": "$.status", "StringEquals": "active"},
389 {"Variable": "$.status", "StringEquals": "pending"}
390 ],
391 "Next": "Valid"
392 });
393 let input = json!({"status": "active"});
394 assert!(evaluate_rule(&rule, &input));
395
396 let input = json!({"status": "closed"});
397 assert!(!evaluate_rule(&rule, &input));
398 }
399
400 #[test]
401 fn test_not_operator() {
402 let rule = json!({
403 "Not": {
404 "Variable": "$.status",
405 "StringEquals": "closed"
406 },
407 "Next": "Open"
408 });
409 let input = json!({"status": "active"});
410 assert!(evaluate_rule(&rule, &input));
411
412 let input = json!({"status": "closed"});
413 assert!(!evaluate_rule(&rule, &input));
414 }
415
416 #[test]
417 fn test_is_present() {
418 let rule = json!({
419 "Variable": "$.optional",
420 "IsPresent": true,
421 "Next": "HasField"
422 });
423 let input = json!({"optional": "value"});
424 assert!(evaluate_rule(&rule, &input));
425
426 let input = json!({"other": "value"});
427 assert!(!evaluate_rule(&rule, &input));
428 }
429
430 #[test]
431 fn test_is_present_with_array_index() {
432 let rule = json!({
433 "Variable": "$.items[0]",
434 "IsPresent": true,
435 "Next": "HasItem"
436 });
437 let input = json!({"items": [10, 20, 30]});
438 assert!(evaluate_rule(&rule, &input));
439
440 let input = json!({"items": []});
441 assert!(!evaluate_rule(&rule, &input));
442 }
443
444 #[test]
445 fn test_is_present_with_null_value() {
446 let rule = json!({
448 "Variable": "$.optional",
449 "IsPresent": true,
450 "Next": "HasField"
451 });
452 let input = json!({"optional": null});
453 assert!(evaluate_rule(&rule, &input));
454 }
455
456 #[test]
457 fn test_is_null() {
458 let rule = json!({
459 "Variable": "$.field",
460 "IsNull": true,
461 "Next": "Null"
462 });
463 let input = json!({"field": null});
464 assert!(evaluate_rule(&rule, &input));
465
466 let input = json!({"field": "value"});
467 assert!(!evaluate_rule(&rule, &input));
468 }
469
470 #[test]
471 fn test_is_numeric() {
472 let rule = json!({
473 "Variable": "$.value",
474 "IsNumeric": true,
475 "Next": "Number"
476 });
477 let input = json!({"value": 42});
478 assert!(evaluate_rule(&rule, &input));
479
480 let input = json!({"value": "not a number"});
481 assert!(!evaluate_rule(&rule, &input));
482 }
483
484 #[test]
485 fn test_string_matches() {
486 assert!(string_matches("hello world", "hello*"));
487 assert!(string_matches("hello world", "*world"));
488 assert!(string_matches("hello world", "hello*world"));
489 assert!(string_matches("hello world", "*"));
490 assert!(!string_matches("hello world", "goodbye*"));
491 assert!(string_matches("log-2024-01-15.txt", "log-*.txt"));
492 }
493
494 #[test]
495 fn test_evaluate_choice_with_default() {
496 let state_def = json!({
497 "Type": "Choice",
498 "Choices": [
499 {
500 "Variable": "$.status",
501 "StringEquals": "active",
502 "Next": "ActivePath"
503 }
504 ],
505 "Default": "DefaultPath"
506 });
507 let input = json!({"status": "unknown"});
508 assert_eq!(
509 evaluate_choice(&state_def, &input),
510 Some("DefaultPath".to_string())
511 );
512 }
513
514 #[test]
515 fn test_evaluate_choice_matching() {
516 let state_def = json!({
517 "Type": "Choice",
518 "Choices": [
519 {
520 "Variable": "$.value",
521 "NumericGreaterThan": 100,
522 "Next": "High"
523 },
524 {
525 "Variable": "$.value",
526 "NumericLessThanEquals": 100,
527 "Next": "Low"
528 }
529 ],
530 "Default": "Unknown"
531 });
532 let input = json!({"value": 150});
533 assert_eq!(
534 evaluate_choice(&state_def, &input),
535 Some("High".to_string())
536 );
537
538 let input = json!({"value": 50});
539 assert_eq!(evaluate_choice(&state_def, &input), Some("Low".to_string()));
540 }
541
542 #[test]
543 fn test_evaluate_choice_no_match_no_default() {
544 let state_def = json!({
545 "Type": "Choice",
546 "Choices": [
547 {
548 "Variable": "$.status",
549 "StringEquals": "active",
550 "Next": "Active"
551 }
552 ]
553 });
554 let input = json!({"status": "closed"});
555 assert_eq!(evaluate_choice(&state_def, &input), None);
556 }
557
558 #[test]
559 fn test_numeric_equals_path() {
560 let rule = json!({
561 "Variable": "$.a",
562 "NumericEqualsPath": "$.b",
563 "Next": "Equal"
564 });
565 let input = json!({"a": 42, "b": 42});
566 assert!(evaluate_rule(&rule, &input));
567
568 let input = json!({"a": 42, "b": 99});
569 assert!(!evaluate_rule(&rule, &input));
570 }
571
572 #[test]
573 fn test_timestamp_comparisons() {
574 let rule = json!({
575 "Variable": "$.ts",
576 "TimestampLessThan": "2024-06-01T00:00:00Z",
577 "Next": "Before"
578 });
579 let input = json!({"ts": "2024-01-15T12:00:00Z"});
580 assert!(evaluate_rule(&rule, &input));
581
582 let input = json!({"ts": "2024-12-01T00:00:00Z"});
583 assert!(!evaluate_rule(&rule, &input));
584 }
585
586 #[test]
587 fn test_string_less_than() {
588 let rule = json!({
589 "Variable": "$.name",
590 "StringLessThan": "beta",
591 "Next": "Before"
592 });
593 let input = json!({"name": "alpha"});
594 assert!(evaluate_rule(&rule, &input));
595
596 let input = json!({"name": "gamma"});
597 assert!(!evaluate_rule(&rule, &input));
598 }
599}