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