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 = !value.is_null();
43 return expected.as_bool().unwrap_or(false) == is_present;
44 }
45 if let Some(expected) = rule.get("IsNull") {
46 let is_null = value.is_null();
47 return expected.as_bool().unwrap_or(false) == is_null;
48 }
49 if let Some(expected) = rule.get("IsNumeric") {
50 let is_numeric = value.is_number();
51 return expected.as_bool().unwrap_or(false) == is_numeric;
52 }
53 if let Some(expected) = rule.get("IsString") {
54 let is_string = value.is_string();
55 return expected.as_bool().unwrap_or(false) == is_string;
56 }
57 if let Some(expected) = rule.get("IsBoolean") {
58 let is_boolean = value.is_boolean();
59 return expected.as_bool().unwrap_or(false) == is_boolean;
60 }
61 if let Some(expected) = rule.get("IsTimestamp") {
62 let is_ts = value
63 .as_str()
64 .map(|s| chrono::DateTime::parse_from_rfc3339(s).is_ok())
65 .unwrap_or(false);
66 return expected.as_bool().unwrap_or(false) == is_ts;
67 }
68
69 if let Some(expected) = rule["StringEquals"].as_str() {
71 return value.as_str() == Some(expected);
72 }
73 if let Some(path) = rule["StringEqualsPath"].as_str() {
74 let other = resolve_path(input, path);
75 return value.as_str().is_some() && value.as_str() == other.as_str();
76 }
77 if let Some(expected) = rule["StringLessThan"].as_str() {
78 return value.as_str().is_some_and(|v| v < expected);
79 }
80 if let Some(expected) = rule["StringGreaterThan"].as_str() {
81 return value.as_str().is_some_and(|v| v > expected);
82 }
83 if let Some(expected) = rule["StringLessThanEquals"].as_str() {
84 return value.as_str().is_some_and(|v| v <= expected);
85 }
86 if let Some(expected) = rule["StringGreaterThanEquals"].as_str() {
87 return value.as_str().is_some_and(|v| v >= expected);
88 }
89 if let Some(pattern) = rule["StringMatches"].as_str() {
90 return value.as_str().is_some_and(|v| string_matches(v, pattern));
91 }
92
93 if let Some(expected) = rule["NumericEquals"].as_f64() {
95 return value.as_f64() == Some(expected);
96 }
97 if let Some(path) = rule["NumericEqualsPath"].as_str() {
98 let other = resolve_path(input, path);
99 return value.as_f64().is_some() && value.as_f64() == other.as_f64();
100 }
101 if let Some(expected) = rule["NumericLessThan"].as_f64() {
102 return value.as_f64().is_some_and(|v| v < expected);
103 }
104 if let Some(expected) = rule["NumericGreaterThan"].as_f64() {
105 return value.as_f64().is_some_and(|v| v > expected);
106 }
107 if let Some(expected) = rule["NumericLessThanEquals"].as_f64() {
108 return value.as_f64().is_some_and(|v| v <= expected);
109 }
110 if let Some(expected) = rule["NumericGreaterThanEquals"].as_f64() {
111 return value.as_f64().is_some_and(|v| v >= expected);
112 }
113
114 if let Some(expected) = rule["BooleanEquals"].as_bool() {
116 return value.as_bool() == Some(expected);
117 }
118 if let Some(path) = rule["BooleanEqualsPath"].as_str() {
119 let other = resolve_path(input, path);
120 return value.as_bool().is_some() && value.as_bool() == other.as_bool();
121 }
122
123 if let Some(expected) = rule["TimestampEquals"].as_str() {
125 return compare_timestamps(&value, expected, |a, b| a == b);
126 }
127 if let Some(expected) = rule["TimestampLessThan"].as_str() {
128 return compare_timestamps(&value, expected, |a, b| a < b);
129 }
130 if let Some(expected) = rule["TimestampGreaterThan"].as_str() {
131 return compare_timestamps(&value, expected, |a, b| a > b);
132 }
133 if let Some(expected) = rule["TimestampLessThanEquals"].as_str() {
134 return compare_timestamps(&value, expected, |a, b| a <= b);
135 }
136 if let Some(expected) = rule["TimestampGreaterThanEquals"].as_str() {
137 return compare_timestamps(&value, expected, |a, b| a >= b);
138 }
139
140 false
141}
142
143fn compare_timestamps<F>(value: &Value, expected: &str, cmp: F) -> bool
145where
146 F: Fn(chrono::DateTime<chrono::FixedOffset>, chrono::DateTime<chrono::FixedOffset>) -> bool,
147{
148 let val_str = match value.as_str() {
149 Some(s) => s,
150 None => return false,
151 };
152 let val_ts = match chrono::DateTime::parse_from_rfc3339(val_str) {
153 Ok(t) => t,
154 Err(_) => return false,
155 };
156 let exp_ts = match chrono::DateTime::parse_from_rfc3339(expected) {
157 Ok(t) => t,
158 Err(_) => return false,
159 };
160 cmp(val_ts, exp_ts)
161}
162
163fn string_matches(value: &str, pattern: &str) -> bool {
166 let mut pattern_chars: Vec<char> = pattern.chars().collect();
167 let value_chars: Vec<char> = value.chars().collect();
168
169 let mut segments: Vec<PatternSegment> = Vec::new();
171 let mut current = String::new();
172 let mut i = 0;
173 while i < pattern_chars.len() {
174 if pattern_chars[i] == '\\' && i + 1 < pattern_chars.len() && pattern_chars[i + 1] == '*' {
175 current.push('*');
176 i += 2;
177 } else if pattern_chars[i] == '*' {
178 if !current.is_empty() {
179 segments.push(PatternSegment::Literal(current.clone()));
180 current.clear();
181 }
182 segments.push(PatternSegment::Wildcard);
183 i += 1;
184 } else {
185 current.push(pattern_chars[i]);
186 i += 1;
187 }
188 }
189 if !current.is_empty() {
190 segments.push(PatternSegment::Literal(current));
191 }
192
193 pattern_chars = Vec::new();
195 for seg in &segments {
196 match seg {
197 PatternSegment::Literal(s) => {
198 for c in s.chars() {
199 pattern_chars.push(c);
200 }
201 }
202 PatternSegment::Wildcard => {
203 pattern_chars.push('\0'); }
205 }
206 }
207
208 let m = value_chars.len();
210 let n = pattern_chars.len();
211 let mut dp = vec![vec![false; n + 1]; m + 1];
212 dp[0][0] = true;
213
214 for j in 1..=n {
216 if pattern_chars[j - 1] == '\0' {
217 dp[0][j] = dp[0][j - 1];
218 }
219 }
220
221 for i in 1..=m {
222 for j in 1..=n {
223 if pattern_chars[j - 1] == '\0' {
224 dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
225 } else if pattern_chars[j - 1] == value_chars[i - 1] {
226 dp[i][j] = dp[i - 1][j - 1];
227 }
228 }
229 }
230
231 dp[m][n]
232}
233
234enum PatternSegment {
235 Literal(String),
236 Wildcard,
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use serde_json::json;
243
244 #[test]
245 fn test_string_equals() {
246 let rule = json!({
247 "Variable": "$.status",
248 "StringEquals": "active",
249 "Next": "Active"
250 });
251 let input = json!({"status": "active"});
252 assert!(evaluate_rule(&rule, &input));
253
254 let input = json!({"status": "inactive"});
255 assert!(!evaluate_rule(&rule, &input));
256 }
257
258 #[test]
259 fn test_numeric_greater_than() {
260 let rule = json!({
261 "Variable": "$.count",
262 "NumericGreaterThan": 10,
263 "Next": "High"
264 });
265 let input = json!({"count": 15});
266 assert!(evaluate_rule(&rule, &input));
267
268 let input = json!({"count": 5});
269 assert!(!evaluate_rule(&rule, &input));
270 }
271
272 #[test]
273 fn test_boolean_equals() {
274 let rule = json!({
275 "Variable": "$.enabled",
276 "BooleanEquals": true,
277 "Next": "Enabled"
278 });
279 let input = json!({"enabled": true});
280 assert!(evaluate_rule(&rule, &input));
281
282 let input = json!({"enabled": false});
283 assert!(!evaluate_rule(&rule, &input));
284 }
285
286 #[test]
287 fn test_and_operator() {
288 let rule = json!({
289 "And": [
290 {"Variable": "$.a", "NumericGreaterThan": 0},
291 {"Variable": "$.b", "NumericLessThan": 100}
292 ],
293 "Next": "Both"
294 });
295 let input = json!({"a": 5, "b": 50});
296 assert!(evaluate_rule(&rule, &input));
297
298 let input = json!({"a": -1, "b": 50});
299 assert!(!evaluate_rule(&rule, &input));
300 }
301
302 #[test]
303 fn test_or_operator() {
304 let rule = json!({
305 "Or": [
306 {"Variable": "$.status", "StringEquals": "active"},
307 {"Variable": "$.status", "StringEquals": "pending"}
308 ],
309 "Next": "Valid"
310 });
311 let input = json!({"status": "active"});
312 assert!(evaluate_rule(&rule, &input));
313
314 let input = json!({"status": "closed"});
315 assert!(!evaluate_rule(&rule, &input));
316 }
317
318 #[test]
319 fn test_not_operator() {
320 let rule = json!({
321 "Not": {
322 "Variable": "$.status",
323 "StringEquals": "closed"
324 },
325 "Next": "Open"
326 });
327 let input = json!({"status": "active"});
328 assert!(evaluate_rule(&rule, &input));
329
330 let input = json!({"status": "closed"});
331 assert!(!evaluate_rule(&rule, &input));
332 }
333
334 #[test]
335 fn test_is_present() {
336 let rule = json!({
337 "Variable": "$.optional",
338 "IsPresent": true,
339 "Next": "HasField"
340 });
341 let input = json!({"optional": "value"});
342 assert!(evaluate_rule(&rule, &input));
343
344 let input = json!({"other": "value"});
345 assert!(!evaluate_rule(&rule, &input));
346 }
347
348 #[test]
349 fn test_is_null() {
350 let rule = json!({
351 "Variable": "$.field",
352 "IsNull": true,
353 "Next": "Null"
354 });
355 let input = json!({"field": null});
356 assert!(evaluate_rule(&rule, &input));
357
358 let input = json!({"field": "value"});
359 assert!(!evaluate_rule(&rule, &input));
360 }
361
362 #[test]
363 fn test_is_numeric() {
364 let rule = json!({
365 "Variable": "$.value",
366 "IsNumeric": true,
367 "Next": "Number"
368 });
369 let input = json!({"value": 42});
370 assert!(evaluate_rule(&rule, &input));
371
372 let input = json!({"value": "not a number"});
373 assert!(!evaluate_rule(&rule, &input));
374 }
375
376 #[test]
377 fn test_string_matches() {
378 assert!(string_matches("hello world", "hello*"));
379 assert!(string_matches("hello world", "*world"));
380 assert!(string_matches("hello world", "hello*world"));
381 assert!(string_matches("hello world", "*"));
382 assert!(!string_matches("hello world", "goodbye*"));
383 assert!(string_matches("log-2024-01-15.txt", "log-*.txt"));
384 }
385
386 #[test]
387 fn test_evaluate_choice_with_default() {
388 let state_def = json!({
389 "Type": "Choice",
390 "Choices": [
391 {
392 "Variable": "$.status",
393 "StringEquals": "active",
394 "Next": "ActivePath"
395 }
396 ],
397 "Default": "DefaultPath"
398 });
399 let input = json!({"status": "unknown"});
400 assert_eq!(
401 evaluate_choice(&state_def, &input),
402 Some("DefaultPath".to_string())
403 );
404 }
405
406 #[test]
407 fn test_evaluate_choice_matching() {
408 let state_def = json!({
409 "Type": "Choice",
410 "Choices": [
411 {
412 "Variable": "$.value",
413 "NumericGreaterThan": 100,
414 "Next": "High"
415 },
416 {
417 "Variable": "$.value",
418 "NumericLessThanEquals": 100,
419 "Next": "Low"
420 }
421 ],
422 "Default": "Unknown"
423 });
424 let input = json!({"value": 150});
425 assert_eq!(
426 evaluate_choice(&state_def, &input),
427 Some("High".to_string())
428 );
429
430 let input = json!({"value": 50});
431 assert_eq!(evaluate_choice(&state_def, &input), Some("Low".to_string()));
432 }
433
434 #[test]
435 fn test_evaluate_choice_no_match_no_default() {
436 let state_def = json!({
437 "Type": "Choice",
438 "Choices": [
439 {
440 "Variable": "$.status",
441 "StringEquals": "active",
442 "Next": "Active"
443 }
444 ]
445 });
446 let input = json!({"status": "closed"});
447 assert_eq!(evaluate_choice(&state_def, &input), None);
448 }
449
450 #[test]
451 fn test_numeric_equals_path() {
452 let rule = json!({
453 "Variable": "$.a",
454 "NumericEqualsPath": "$.b",
455 "Next": "Equal"
456 });
457 let input = json!({"a": 42, "b": 42});
458 assert!(evaluate_rule(&rule, &input));
459
460 let input = json!({"a": 42, "b": 99});
461 assert!(!evaluate_rule(&rule, &input));
462 }
463
464 #[test]
465 fn test_timestamp_comparisons() {
466 let rule = json!({
467 "Variable": "$.ts",
468 "TimestampLessThan": "2024-06-01T00:00:00Z",
469 "Next": "Before"
470 });
471 let input = json!({"ts": "2024-01-15T12:00:00Z"});
472 assert!(evaluate_rule(&rule, &input));
473
474 let input = json!({"ts": "2024-12-01T00:00:00Z"});
475 assert!(!evaluate_rule(&rule, &input));
476 }
477
478 #[test]
479 fn test_string_less_than() {
480 let rule = json!({
481 "Variable": "$.name",
482 "StringLessThan": "beta",
483 "Next": "Before"
484 });
485 let input = json!({"name": "alpha"});
486 assert!(evaluate_rule(&rule, &input));
487
488 let input = json!({"name": "gamma"});
489 assert!(!evaluate_rule(&rule, &input));
490 }
491}