use serde_json::{Value, json};
use crate::error::{FraiseQLError, Result};
#[derive(Debug, Clone)]
pub struct EloExpressionEvaluator {
expression: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EloValidationResult {
pub valid: bool,
pub error: Option<String>,
}
impl EloExpressionEvaluator {
pub const fn new(expression: String) -> Self {
Self { expression }
}
pub fn evaluate(&self, context: &Value) -> Result<EloValidationResult> {
self.evaluate_expression(&self.expression, context)
}
fn evaluate_expression(&self, expr: &str, context: &Value) -> Result<EloValidationResult> {
let trimmed = expr.trim();
let expr = if trimmed.starts_with('(') && trimmed.ends_with(')') {
if self.are_matching_parens(trimmed) {
&trimmed[1..trimmed.len() - 1]
} else {
trimmed
}
} else {
trimmed
};
let expr = expr.trim();
if let Some(or_idx) = self.find_operator_outside_parens(expr, "||") {
let left = &expr[..or_idx];
let right = &expr[or_idx + 2..];
let left_result = self.evaluate_expression(left, context)?;
if left_result.valid {
return Ok(EloValidationResult {
valid: true,
error: None,
});
}
let right_result = self.evaluate_expression(right, context)?;
return Ok(right_result);
}
if let Some(and_idx) = self.find_operator_outside_parens(expr, "&&") {
let left = &expr[..and_idx];
let right = &expr[and_idx + 2..];
let left_result = self.evaluate_expression(left, context)?;
if !left_result.valid {
return Ok(left_result);
}
let right_result = self.evaluate_expression(right, context)?;
return Ok(right_result);
}
if let Some(inner) = expr.strip_prefix('!') {
let inner_result = self.evaluate_expression(inner.trim(), context)?;
return Ok(EloValidationResult {
valid: !inner_result.valid,
error: if inner_result.valid {
Some("Negation failed".to_string())
} else {
None
},
});
}
for op in &["==", "!=", "<=", ">=", "<", ">"] {
if let Some(op_idx) = self.find_operator_outside_parens(expr, op) {
let left = &expr[..op_idx].trim();
let right = &expr[op_idx + op.len()..].trim();
return self.evaluate_comparison(left, op, right, context);
}
}
if expr.contains('(') && expr.ends_with(')') {
return self.evaluate_function_call(expr, context);
}
self.evaluate_value(expr, context)
}
fn evaluate_comparison(
&self,
left: &str,
op: &str,
right: &str,
context: &Value,
) -> Result<EloValidationResult> {
let left_val = self.get_value(left, context)?;
let right_val = self.get_value(right, context)?;
let valid = match op {
"==" => left_val == right_val,
"!=" => left_val != right_val,
"<" => self.compare_values(&left_val, &right_val) == Some(std::cmp::Ordering::Less),
"<=" => {
matches!(
self.compare_values(&left_val, &right_val),
Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
)
},
">" => self.compare_values(&left_val, &right_val) == Some(std::cmp::Ordering::Greater),
">=" => {
matches!(
self.compare_values(&left_val, &right_val),
Some(std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
)
},
_ => false,
};
Ok(EloValidationResult {
valid,
error: if valid {
None
} else {
Some(format!("Comparison failed: {} {} {}", left_val, op, right_val))
},
})
}
fn parse_function_args(&self, args_str: &str) -> Vec<String> {
let mut args = Vec::new();
let mut current_arg = String::new();
let mut in_string = false;
for ch in args_str.chars() {
match ch {
'"' => {
in_string = !in_string;
current_arg.push(ch);
},
',' if !in_string => {
args.push(current_arg.trim().to_string());
current_arg = String::new();
},
_ => {
current_arg.push(ch);
},
}
}
if !current_arg.is_empty() {
args.push(current_arg.trim().to_string());
}
args
}
fn evaluate_function_call(&self, expr: &str, context: &Value) -> Result<EloValidationResult> {
if let Some(paren_idx) = expr.find('(') {
let func_name = &expr[..paren_idx].trim();
let args_str = &expr[paren_idx + 1..expr.len() - 1];
match *func_name {
"today" => {
Ok(EloValidationResult {
valid: true,
error: None,
})
},
"now" => {
Ok(EloValidationResult {
valid: true,
error: None,
})
},
"matches" => {
let parts = self.parse_function_args(args_str);
if parts.len() != 2 {
return Err(FraiseQLError::Validation {
message: "matches() requires 2 arguments".to_string(),
path: None,
});
}
let field_val = self.get_value(&parts[0], context)?;
let pattern = self.get_value(&parts[1], context)?;
if let (Value::String(s), Value::String(p)) = (&field_val, &pattern) {
match regex::Regex::new(p) {
Ok(re) => {
let valid = re.is_match(s);
Ok(EloValidationResult {
valid,
error: if valid {
None
} else {
Some(format!("'{}' does not match pattern '{}'", s, p))
},
})
},
Err(_) => Err(FraiseQLError::Validation {
message: format!("Invalid regex pattern: {}", p),
path: None,
}),
}
} else {
Err(FraiseQLError::Validation {
message: "matches() requires string arguments".to_string(),
path: None,
})
}
},
"contains" => {
let parts = self.parse_function_args(args_str);
if parts.len() != 2 {
return Err(FraiseQLError::Validation {
message: "contains() requires 2 arguments".to_string(),
path: None,
});
}
let field_val = self.get_value(&parts[0], context)?;
let needle = self.get_value(&parts[1], context)?;
if let (Value::String(s), Value::String(n)) = (&field_val, &needle) {
let valid = s.contains(n);
Ok(EloValidationResult {
valid,
error: if valid {
None
} else {
Some(format!("'{}' does not contain '{}'", s, n))
},
})
} else {
Err(FraiseQLError::Validation {
message: "contains() requires string arguments".to_string(),
path: None,
})
}
},
"length" => {
let field_val = self.get_value(args_str, context)?;
if let Value::String(_s) = field_val {
Ok(EloValidationResult {
valid: true,
error: None,
})
} else {
Err(FraiseQLError::Validation {
message: "length() requires a string argument".to_string(),
path: None,
})
}
},
"age" => {
let _field_val = self.get_value(args_str, context)?;
Ok(EloValidationResult {
valid: true,
error: None,
})
},
_ => Err(FraiseQLError::Validation {
message: format!("Unknown function: {}", func_name),
path: None,
}),
}
} else {
Err(FraiseQLError::Validation {
message: "Invalid function call".to_string(),
path: None,
})
}
}
fn evaluate_value(&self, expr: &str, context: &Value) -> Result<EloValidationResult> {
let _val = self.get_value(expr, context)?;
Ok(EloValidationResult {
valid: true,
error: None,
})
}
fn are_matching_parens(&self, expr: &str) -> bool {
if !expr.starts_with('(') || !expr.ends_with(')') {
return false;
}
let mut count = 0;
let mut in_string = false;
let mut escape = false;
for (i, ch) in expr.chars().enumerate() {
if escape {
escape = false;
continue;
}
if ch == '\\' {
escape = true;
continue;
}
if ch == '"' && !in_string {
in_string = true;
continue;
}
if ch == '"' && in_string {
in_string = false;
continue;
}
if in_string {
continue;
}
match ch {
'(' => count += 1,
')' => {
count -= 1;
if count == 0 && i < expr.len() - 1 {
return false;
}
},
_ => {},
}
}
count == 0
}
fn find_operator_outside_parens(&self, expr: &str, op: &str) -> Option<usize> {
let mut paren_count = 0;
let mut in_string = false;
let chars: Vec<char> = expr.chars().collect();
for i in (0..chars.len()).rev() {
let ch = chars[i];
if ch == '"' {
in_string = !in_string;
continue;
}
if in_string {
continue;
}
if ch == ')' {
paren_count += 1;
continue;
}
if ch == '(' {
paren_count -= 1;
continue;
}
if paren_count == 0 {
let remaining: String = chars[i..].iter().collect();
if remaining.starts_with(op) {
return Some(i);
}
}
}
None
}
fn get_value(&self, expr: &str, context: &Value) -> Result<Value> {
let trimmed = expr.trim();
if (trimmed.starts_with('"') && trimmed.ends_with('"'))
|| (trimmed.starts_with('\'') && trimmed.ends_with('\''))
{
let unquoted = &trimmed[1..trimmed.len() - 1];
let unescaped = unquoted
.replace("\\\\", "\x00") .replace("\\n", "\n")
.replace("\\t", "\t")
.replace("\\r", "\r")
.replace("\\\"", "\"")
.replace("\\'", "'")
.replace('\x00', "\\"); return Ok(Value::String(unescaped));
}
if let Ok(i) = trimmed.parse::<i64>() {
return Ok(Value::Number(i.into()));
}
if let Ok(f) = trimmed.parse::<f64>() {
return Ok(json!(f));
}
if trimmed == "true" {
return Ok(Value::Bool(true));
}
if trimmed == "false" {
return Ok(Value::Bool(false));
}
if trimmed == "null" {
return Ok(Value::Null);
}
if let Some(value) = self.access_field(trimmed, context) {
return Ok(value);
}
Err(FraiseQLError::Validation {
message: format!("Cannot resolve value: {}", trimmed),
path: None,
})
}
fn access_field(&self, path: &str, context: &Value) -> Option<Value> {
let parts: Vec<&str> = path.split('.').collect();
let mut current = context.clone();
for part in parts {
current = current.get(part)?.clone();
}
Some(current)
}
fn compare_values(&self, left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
match (left, right) {
(Value::Number(l), Value::Number(r)) => {
let l_f64 = l.as_f64()?;
let r_f64 = r.as_f64()?;
Some(l_f64.partial_cmp(&r_f64)?)
},
(Value::String(l), Value::String(r)) => Some(l.cmp(r)),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
fn create_test_user() -> Value {
json!({
"email": "user@example.com",
"age": 25,
"verified": true,
"birthDate": "2000-01-15",
"role": "user"
})
}
#[test]
fn test_simple_greater_than() {
let eval = EloExpressionEvaluator::new("age > 18".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_simple_greater_than_fails() {
let eval = EloExpressionEvaluator::new("age > 30".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(!result.valid);
}
#[test]
fn test_greater_or_equal() {
let eval = EloExpressionEvaluator::new("age >= 25".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_less_than() {
let eval = EloExpressionEvaluator::new("age < 30".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_less_or_equal() {
let eval = EloExpressionEvaluator::new("age <= 25".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_equality() {
let eval = EloExpressionEvaluator::new("role == \"user\"".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_inequality() {
let eval = EloExpressionEvaluator::new("role != \"admin\"".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_and_both_true() {
let eval = EloExpressionEvaluator::new("age > 18 && verified == true".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_and_first_false() {
let eval = EloExpressionEvaluator::new("age < 18 && verified == true".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(!result.valid);
}
#[test]
fn test_and_second_false() {
let eval = EloExpressionEvaluator::new("age > 18 && verified == false".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(!result.valid);
}
#[test]
fn test_or_both_true() {
let eval = EloExpressionEvaluator::new("age > 18 || role == \"admin\"".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_or_first_true() {
let eval = EloExpressionEvaluator::new("age > 18 || role == \"guest\"".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_or_second_true() {
let eval = EloExpressionEvaluator::new("age > 30 || role == \"user\"".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_or_both_false() {
let eval = EloExpressionEvaluator::new("age > 30 || role == \"admin\"".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(!result.valid);
}
#[test]
fn test_negation() {
let eval = EloExpressionEvaluator::new("!(role == \"admin\")".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_negation_of_true() {
let eval = EloExpressionEvaluator::new("!(verified == true)".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(!result.valid);
}
#[test]
fn test_matches_function() {
let eval = EloExpressionEvaluator::new(
"matches(email, \"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}$\")".to_string(),
);
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_matches_function_fails() {
let eval = EloExpressionEvaluator::new("matches(email, \"^[0-9]+$\")".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(!result.valid);
}
#[test]
fn test_contains_function() {
let eval = EloExpressionEvaluator::new("contains(email, \"example.com\")".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_contains_function_fails() {
let eval = EloExpressionEvaluator::new("contains(email, \"gmail.com\")".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(!result.valid);
}
#[test]
fn test_complex_and_or() {
let eval = EloExpressionEvaluator::new(
"age > 18 && (role == \"user\" || role == \"admin\")".to_string(),
);
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_complex_with_matches() {
let eval = EloExpressionEvaluator::new(
"age >= 18 && matches(email, \"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}$\")"
.to_string(),
);
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_complex_with_negation() {
let eval = EloExpressionEvaluator::new(
"!(role == \"banned\") && age > 18 && verified == true".to_string(),
);
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_field_access_string() {
let eval = EloExpressionEvaluator::new("email == \"user@example.com\"".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_field_access_number() {
let eval = EloExpressionEvaluator::new("age == 25".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_field_access_boolean() {
let eval = EloExpressionEvaluator::new("verified == true".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_unknown_function_error() {
let eval = EloExpressionEvaluator::new("unknown_func(email)".to_string());
let user = create_test_user();
let result = eval.evaluate(&user);
assert!(
matches!(result, Err(FraiseQLError::Validation { .. })),
"unknown function should return Validation error, got: {result:?}"
);
}
#[test]
fn test_invalid_regex_error() {
let eval = EloExpressionEvaluator::new("matches(email, \"[\")".to_string());
let user = create_test_user();
let result = eval.evaluate(&user);
assert!(
matches!(result, Err(FraiseQLError::Validation { .. })),
"invalid regex in matches() should return Validation error, got: {result:?}"
);
}
#[test]
fn test_wrong_argument_count_error() {
let eval = EloExpressionEvaluator::new("matches(email)".to_string());
let user = create_test_user();
let result = eval.evaluate(&user);
assert!(
matches!(result, Err(FraiseQLError::Validation { .. })),
"wrong argument count for matches() should return Validation error, got: {result:?}"
);
}
#[test]
fn test_whitespace_handling() {
let eval = EloExpressionEvaluator::new(" age > 18 ".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_multiple_operators_precedence() {
let eval =
EloExpressionEvaluator::new("age > 20 && age < 30 && role == \"user\"".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_string_literal_quotes() {
let eval = EloExpressionEvaluator::new("role == 'user'".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_number_literals() {
let eval = EloExpressionEvaluator::new("age > 20".to_string());
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_email_validation_pattern() {
let eval = EloExpressionEvaluator::new(
"matches(email, \"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}$\")".to_string(),
);
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_user_creation_rules() {
let eval = EloExpressionEvaluator::new(
"age >= 18 && verified == true && role != \"banned\"".to_string(),
);
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(result.valid);
}
#[test]
fn test_admin_access_rules() {
let eval = EloExpressionEvaluator::new(
"(role == \"admin\" || role == \"moderator\") && verified == true".to_string(),
);
let user = create_test_user();
let result = eval.evaluate(&user).unwrap();
assert!(!result.valid); }
}