use serde_json::Value;
use crate::io_processing::resolve_path;
pub fn evaluate_choice(state_def: &Value, input: &Value) -> Option<String> {
if let Some(choices) = state_def["Choices"].as_array() {
for choice in choices {
if evaluate_rule(choice, input) {
return choice["Next"].as_str().map(|s| s.to_string());
}
}
}
state_def["Default"].as_str().map(|s| s.to_string())
}
fn evaluate_rule(rule: &Value, input: &Value) -> bool {
if let Some(result) = evaluate_logical(rule, input) {
return result;
}
let variable = match rule["Variable"].as_str() {
Some(v) => v,
None => return false,
};
let value = resolve_path(input, variable);
if let Some(result) = evaluate_presence_or_type(rule, input, variable, &value) {
return result;
}
if let Some(result) = evaluate_string_comparison(rule, input, &value) {
return result;
}
if let Some(result) = evaluate_numeric_comparison(rule, input, &value) {
return result;
}
if let Some(result) = evaluate_boolean_comparison(rule, input, &value) {
return result;
}
if let Some(result) = evaluate_timestamp_comparison(rule, &value) {
return result;
}
false
}
fn evaluate_logical(rule: &Value, input: &Value) -> Option<bool> {
if let Some(and_rules) = rule["And"].as_array() {
return Some(and_rules.iter().all(|r| evaluate_rule(r, input)));
}
if let Some(or_rules) = rule["Or"].as_array() {
return Some(or_rules.iter().any(|r| evaluate_rule(r, input)));
}
if rule.get("Not").is_some() {
return Some(!evaluate_rule(&rule["Not"], input));
}
None
}
fn evaluate_presence_or_type(
rule: &Value,
input: &Value,
variable: &str,
value: &Value,
) -> Option<bool> {
if let Some(expected) = rule.get("IsPresent") {
let is_present = field_exists_in_input(input, variable);
return Some(expected.as_bool().unwrap_or(false) == is_present);
}
if let Some(expected) = rule.get("IsNull") {
return Some(expected.as_bool().unwrap_or(false) == value.is_null());
}
if let Some(expected) = rule.get("IsNumeric") {
return Some(expected.as_bool().unwrap_or(false) == value.is_number());
}
if let Some(expected) = rule.get("IsString") {
return Some(expected.as_bool().unwrap_or(false) == value.is_string());
}
if let Some(expected) = rule.get("IsBoolean") {
return Some(expected.as_bool().unwrap_or(false) == value.is_boolean());
}
if let Some(expected) = rule.get("IsTimestamp") {
let is_ts = value
.as_str()
.map(|s| chrono::DateTime::parse_from_rfc3339(s).is_ok())
.unwrap_or(false);
return Some(expected.as_bool().unwrap_or(false) == is_ts);
}
None
}
fn evaluate_string_comparison(rule: &Value, input: &Value, value: &Value) -> Option<bool> {
if let Some(expected) = rule["StringEquals"].as_str() {
return Some(value.as_str() == Some(expected));
}
if let Some(path) = rule["StringEqualsPath"].as_str() {
let other = resolve_path(input, path);
return Some(value.as_str().is_some() && value.as_str() == other.as_str());
}
if let Some(expected) = rule["StringLessThan"].as_str() {
return Some(value.as_str().is_some_and(|v| v < expected));
}
if let Some(expected) = rule["StringGreaterThan"].as_str() {
return Some(value.as_str().is_some_and(|v| v > expected));
}
if let Some(expected) = rule["StringLessThanEquals"].as_str() {
return Some(value.as_str().is_some_and(|v| v <= expected));
}
if let Some(expected) = rule["StringGreaterThanEquals"].as_str() {
return Some(value.as_str().is_some_and(|v| v >= expected));
}
if let Some(pattern) = rule["StringMatches"].as_str() {
return Some(value.as_str().is_some_and(|v| string_matches(v, pattern)));
}
None
}
fn evaluate_numeric_comparison(rule: &Value, input: &Value, value: &Value) -> Option<bool> {
if let Some(expected) = rule["NumericEquals"].as_f64() {
return Some(value.as_f64() == Some(expected));
}
if let Some(path) = rule["NumericEqualsPath"].as_str() {
let other = resolve_path(input, path);
return Some(value.as_f64().is_some() && value.as_f64() == other.as_f64());
}
if let Some(expected) = rule["NumericLessThan"].as_f64() {
return Some(value.as_f64().is_some_and(|v| v < expected));
}
if let Some(expected) = rule["NumericGreaterThan"].as_f64() {
return Some(value.as_f64().is_some_and(|v| v > expected));
}
if let Some(expected) = rule["NumericLessThanEquals"].as_f64() {
return Some(value.as_f64().is_some_and(|v| v <= expected));
}
if let Some(expected) = rule["NumericGreaterThanEquals"].as_f64() {
return Some(value.as_f64().is_some_and(|v| v >= expected));
}
None
}
fn evaluate_boolean_comparison(rule: &Value, input: &Value, value: &Value) -> Option<bool> {
if let Some(expected) = rule["BooleanEquals"].as_bool() {
return Some(value.as_bool() == Some(expected));
}
if let Some(path) = rule["BooleanEqualsPath"].as_str() {
let other = resolve_path(input, path);
return Some(value.as_bool().is_some() && value.as_bool() == other.as_bool());
}
None
}
fn evaluate_timestamp_comparison(rule: &Value, value: &Value) -> Option<bool> {
if let Some(expected) = rule["TimestampEquals"].as_str() {
return Some(compare_timestamps(value, expected, |a, b| a == b));
}
if let Some(expected) = rule["TimestampLessThan"].as_str() {
return Some(compare_timestamps(value, expected, |a, b| a < b));
}
if let Some(expected) = rule["TimestampGreaterThan"].as_str() {
return Some(compare_timestamps(value, expected, |a, b| a > b));
}
if let Some(expected) = rule["TimestampLessThanEquals"].as_str() {
return Some(compare_timestamps(value, expected, |a, b| a <= b));
}
if let Some(expected) = rule["TimestampGreaterThanEquals"].as_str() {
return Some(compare_timestamps(value, expected, |a, b| a >= b));
}
None
}
fn compare_timestamps<F>(value: &Value, expected: &str, cmp: F) -> bool
where
F: Fn(chrono::DateTime<chrono::FixedOffset>, chrono::DateTime<chrono::FixedOffset>) -> bool,
{
let val_str = match value.as_str() {
Some(s) => s,
None => return false,
};
let val_ts = match chrono::DateTime::parse_from_rfc3339(val_str) {
Ok(t) => t,
Err(_) => return false,
};
let exp_ts = match chrono::DateTime::parse_from_rfc3339(expected) {
Ok(t) => t,
Err(_) => return false,
};
cmp(val_ts, exp_ts)
}
fn string_matches(value: &str, pattern: &str) -> bool {
let compiled = compile_glob_pattern(pattern);
glob_dp_match(&value.chars().collect::<Vec<_>>(), &compiled)
}
fn compile_glob_pattern(pattern: &str) -> Vec<GlobToken> {
let mut out = Vec::new();
let chars: Vec<char> = pattern.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '\\' && i + 1 < chars.len() && chars[i + 1] == '*' {
out.push(GlobToken::Char('*'));
i += 2;
} else if chars[i] == '*' {
out.push(GlobToken::Wildcard);
i += 1;
} else {
out.push(GlobToken::Char(chars[i]));
i += 1;
}
}
out
}
fn glob_dp_match(value: &[char], pattern: &[GlobToken]) -> bool {
let m = value.len();
let n = pattern.len();
let mut dp = vec![vec![false; n + 1]; m + 1];
dp[0][0] = true;
for j in 1..=n {
if matches!(pattern[j - 1], GlobToken::Wildcard) {
dp[0][j] = dp[0][j - 1];
}
}
for i in 1..=m {
for j in 1..=n {
match pattern[j - 1] {
GlobToken::Wildcard => {
dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
}
GlobToken::Char(c) if c == value[i - 1] => {
dp[i][j] = dp[i - 1][j - 1];
}
GlobToken::Char(_) => {}
}
}
}
dp[m][n]
}
#[derive(Clone, Copy)]
enum GlobToken {
Char(char),
Wildcard,
}
fn field_exists_in_input(root: &Value, path: &str) -> bool {
if path == "$" {
return true;
}
let path = path.strip_prefix("$.").unwrap_or(path);
let parts: Vec<&str> = path.split('.').collect();
let mut current = root;
for (i, part) in parts.iter().enumerate() {
let is_last = i == parts.len() - 1;
if let Some(bracket_pos) = part.find('[') {
let field_name = &part[..bracket_pos];
if !part.ends_with(']') {
return false; }
let close_bracket = part.len() - 1;
if close_bracket <= bracket_pos {
return false;
}
let idx_str = &part[bracket_pos + 1..close_bracket];
match current.get(field_name) {
Some(arr) => {
if let Ok(idx) = idx_str.parse::<usize>() {
if is_last {
return arr.as_array().is_some_and(|a| idx < a.len());
}
match arr.get(idx) {
Some(v) => current = v,
None => return false,
}
} else {
return false;
}
}
None => return false,
}
} else if is_last {
return match current.as_object() {
Some(obj) => obj.contains_key(*part),
None => false,
};
} else {
match current.get(*part) {
Some(v) => current = v,
None => return false,
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_string_equals() {
let rule = json!({
"Variable": "$.status",
"StringEquals": "active",
"Next": "Active"
});
let input = json!({"status": "active"});
assert!(evaluate_rule(&rule, &input));
let input = json!({"status": "inactive"});
assert!(!evaluate_rule(&rule, &input));
}
#[test]
fn test_numeric_greater_than() {
let rule = json!({
"Variable": "$.count",
"NumericGreaterThan": 10,
"Next": "High"
});
let input = json!({"count": 15});
assert!(evaluate_rule(&rule, &input));
let input = json!({"count": 5});
assert!(!evaluate_rule(&rule, &input));
}
#[test]
fn test_boolean_equals() {
let rule = json!({
"Variable": "$.enabled",
"BooleanEquals": true,
"Next": "Enabled"
});
let input = json!({"enabled": true});
assert!(evaluate_rule(&rule, &input));
let input = json!({"enabled": false});
assert!(!evaluate_rule(&rule, &input));
}
#[test]
fn test_and_operator() {
let rule = json!({
"And": [
{"Variable": "$.a", "NumericGreaterThan": 0},
{"Variable": "$.b", "NumericLessThan": 100}
],
"Next": "Both"
});
let input = json!({"a": 5, "b": 50});
assert!(evaluate_rule(&rule, &input));
let input = json!({"a": -1, "b": 50});
assert!(!evaluate_rule(&rule, &input));
}
#[test]
fn test_or_operator() {
let rule = json!({
"Or": [
{"Variable": "$.status", "StringEquals": "active"},
{"Variable": "$.status", "StringEquals": "pending"}
],
"Next": "Valid"
});
let input = json!({"status": "active"});
assert!(evaluate_rule(&rule, &input));
let input = json!({"status": "closed"});
assert!(!evaluate_rule(&rule, &input));
}
#[test]
fn test_not_operator() {
let rule = json!({
"Not": {
"Variable": "$.status",
"StringEquals": "closed"
},
"Next": "Open"
});
let input = json!({"status": "active"});
assert!(evaluate_rule(&rule, &input));
let input = json!({"status": "closed"});
assert!(!evaluate_rule(&rule, &input));
}
#[test]
fn test_is_present() {
let rule = json!({
"Variable": "$.optional",
"IsPresent": true,
"Next": "HasField"
});
let input = json!({"optional": "value"});
assert!(evaluate_rule(&rule, &input));
let input = json!({"other": "value"});
assert!(!evaluate_rule(&rule, &input));
}
#[test]
fn test_is_present_with_array_index() {
let rule = json!({
"Variable": "$.items[0]",
"IsPresent": true,
"Next": "HasItem"
});
let input = json!({"items": [10, 20, 30]});
assert!(evaluate_rule(&rule, &input));
let input = json!({"items": []});
assert!(!evaluate_rule(&rule, &input));
}
#[test]
fn test_is_present_with_null_value() {
let rule = json!({
"Variable": "$.optional",
"IsPresent": true,
"Next": "HasField"
});
let input = json!({"optional": null});
assert!(evaluate_rule(&rule, &input));
}
#[test]
fn test_is_null() {
let rule = json!({
"Variable": "$.field",
"IsNull": true,
"Next": "Null"
});
let input = json!({"field": null});
assert!(evaluate_rule(&rule, &input));
let input = json!({"field": "value"});
assert!(!evaluate_rule(&rule, &input));
}
#[test]
fn test_is_numeric() {
let rule = json!({
"Variable": "$.value",
"IsNumeric": true,
"Next": "Number"
});
let input = json!({"value": 42});
assert!(evaluate_rule(&rule, &input));
let input = json!({"value": "not a number"});
assert!(!evaluate_rule(&rule, &input));
}
#[test]
fn test_string_matches() {
assert!(string_matches("hello world", "hello*"));
assert!(string_matches("hello world", "*world"));
assert!(string_matches("hello world", "hello*world"));
assert!(string_matches("hello world", "*"));
assert!(!string_matches("hello world", "goodbye*"));
assert!(string_matches("log-2024-01-15.txt", "log-*.txt"));
}
#[test]
fn test_evaluate_choice_with_default() {
let state_def = json!({
"Type": "Choice",
"Choices": [
{
"Variable": "$.status",
"StringEquals": "active",
"Next": "ActivePath"
}
],
"Default": "DefaultPath"
});
let input = json!({"status": "unknown"});
assert_eq!(
evaluate_choice(&state_def, &input),
Some("DefaultPath".to_string())
);
}
#[test]
fn test_evaluate_choice_matching() {
let state_def = json!({
"Type": "Choice",
"Choices": [
{
"Variable": "$.value",
"NumericGreaterThan": 100,
"Next": "High"
},
{
"Variable": "$.value",
"NumericLessThanEquals": 100,
"Next": "Low"
}
],
"Default": "Unknown"
});
let input = json!({"value": 150});
assert_eq!(
evaluate_choice(&state_def, &input),
Some("High".to_string())
);
let input = json!({"value": 50});
assert_eq!(evaluate_choice(&state_def, &input), Some("Low".to_string()));
}
#[test]
fn test_evaluate_choice_no_match_no_default() {
let state_def = json!({
"Type": "Choice",
"Choices": [
{
"Variable": "$.status",
"StringEquals": "active",
"Next": "Active"
}
]
});
let input = json!({"status": "closed"});
assert_eq!(evaluate_choice(&state_def, &input), None);
}
#[test]
fn test_numeric_equals_path() {
let rule = json!({
"Variable": "$.a",
"NumericEqualsPath": "$.b",
"Next": "Equal"
});
let input = json!({"a": 42, "b": 42});
assert!(evaluate_rule(&rule, &input));
let input = json!({"a": 42, "b": 99});
assert!(!evaluate_rule(&rule, &input));
}
#[test]
fn test_timestamp_comparisons() {
let rule = json!({
"Variable": "$.ts",
"TimestampLessThan": "2024-06-01T00:00:00Z",
"Next": "Before"
});
let input = json!({"ts": "2024-01-15T12:00:00Z"});
assert!(evaluate_rule(&rule, &input));
let input = json!({"ts": "2024-12-01T00:00:00Z"});
assert!(!evaluate_rule(&rule, &input));
}
#[test]
fn test_string_less_than() {
let rule = json!({
"Variable": "$.name",
"StringLessThan": "beta",
"Next": "Before"
});
let input = json!({"name": "alpha"});
assert!(evaluate_rule(&rule, &input));
let input = json!({"name": "gamma"});
assert!(!evaluate_rule(&rule, &input));
}
}