use exprimo::Evaluator;
use std::collections::HashMap;
#[cfg(test)]
#[test]
fn test_basic_evaluate_with_context() {
let mut context = HashMap::new();
context.insert("a".to_string(), serde_json::Value::Bool(true));
context.insert("b".to_string(), serde_json::Value::Bool(false));
let evaluator = Evaluator::new(
context,
HashMap::new(), );
let expr1 = "a && b";
let expr2 = "a || b";
let expr3 = "a && !b";
let expr4 = "a || !b";
let expr5 = "a && b || a && !b";
let res1 = evaluator.evaluate(&expr1).unwrap();
let res2 = evaluator.evaluate(&expr2).unwrap();
let res3 = evaluator.evaluate(&expr3).unwrap();
let res4 = evaluator.evaluate(&expr4).unwrap();
let res5 = evaluator.evaluate(&expr5).unwrap();
assert_eq!(res1, false);
assert_eq!(res2, true);
assert_eq!(res3, true);
assert_eq!(res4, true);
assert_eq!(res5, true);
}
#[test]
fn test_basic_evaluate_with_nulls() {
let mut context = HashMap::new();
context.insert("a".to_string(), serde_json::Value::Null);
context.insert("b".to_string(), serde_json::Value::Bool(true));
let evaluator = Evaluator::new(
context,
HashMap::new(), );
let expr1 = "a && b";
let expr2 = "a || b";
let expr3 = "a && !b";
let expr4 = "a || !b";
let expr5 = "a && b || a && !b";
let res1 = evaluator.evaluate(&expr1).unwrap();
let res2 = evaluator.evaluate(&expr2).unwrap();
let res3 = evaluator.evaluate(&expr3).unwrap();
let res4 = evaluator.evaluate(&expr4).unwrap();
let res5 = evaluator.evaluate(&expr5).unwrap();
assert_eq!(res1, false);
assert_eq!(res2, true);
assert_eq!(res3, false);
assert_eq!(res4, false);
assert_eq!(res5, false);
}
#[test]
fn test_single_quotes_expressions() {
let mut context = HashMap::new();
context.insert(
"a".to_string(),
serde_json::Value::String("true".to_string()),
);
let evaluator = Evaluator::new(
context,
HashMap::new(), );
let expr1 = "a == 'true'";
let res1 = evaluator.evaluate(&expr1).unwrap();
assert_eq!(res1, true);
}
use exprimo::{CustomFuncError, CustomFunction, EvaluationError};
use serde_json::Value; use std::fmt::Debug;
use std::sync::Arc;
#[derive(Debug)] struct MyTestAdder;
impl CustomFunction for MyTestAdder {
fn call(&self, args: &[Value]) -> Result<Value, CustomFuncError> {
if args.len() != 2 {
return Err(CustomFuncError::ArityError {
expected: 2,
got: args.len(),
});
}
match (&args[0], &args[1]) {
(Value::Number(a), Value::Number(b)) => {
if let (Some(val_a), Some(val_b)) = (a.as_f64(), b.as_f64()) {
Ok(Value::Number(
serde_json::Number::from_f64(val_a + val_b).unwrap(),
))
} else {
Err(CustomFuncError::ArgumentError(
"Non-finite number provided".to_string(),
))
}
}
_ => Err(CustomFuncError::ArgumentError(
"Arguments must be numbers".to_string(),
)),
}
}
}
#[test]
fn test_parenthesized_expressions() {
let mut context = HashMap::new();
context.insert(
"a".to_string(),
serde_json::Value::Number(serde_json::Number::from_f64(1.0).unwrap()),
);
context.insert(
"b".to_string(),
serde_json::Value::Number(serde_json::Number::from_f64(2.0).unwrap()),
);
context.insert(
"c".to_string(),
serde_json::Value::Number(serde_json::Number::from_f64(3.0).unwrap()),
);
context.insert(
"d".to_string(),
serde_json::Value::Number(serde_json::Number::from_f64(4.0).unwrap()),
);
let evaluator = Evaluator::new(
context,
HashMap::new(), );
let expr1 = "(a + b) * c";
let res1 = evaluator.evaluate(expr1).unwrap();
assert_eq!(
res1,
serde_json::Value::Number(serde_json::Number::from_f64(9.0).unwrap())
);
let expr2 = "((a + b) * c) / d";
let res2 = evaluator.evaluate(expr2).unwrap();
assert_eq!(
res2,
serde_json::Value::Number(serde_json::Number::from_f64(2.25).unwrap())
);
let expr3 = "((d-b) * ((a+b)*c)) / (10/5)";
let res3 = evaluator.evaluate(expr3).unwrap();
assert_eq!(
res3,
serde_json::Value::Number(serde_json::Number::from_f64(9.0).unwrap())
);
let expr4 = "-(a + b)";
let res4 = evaluator.evaluate(expr4).unwrap();
assert_eq!(
res4,
serde_json::Value::Number(serde_json::Number::from_f64(-3.0).unwrap())
);
let expr5 = "c * (-a - b)"; let res5 = evaluator.evaluate(expr5).unwrap();
assert_eq!(
res5,
serde_json::Value::Number(serde_json::Number::from_f64(-9.0).unwrap())
);
let expr6 = "(a < b) && (c > d)"; let res6 = evaluator.evaluate(expr6).unwrap();
assert_eq!(res6, serde_json::Value::Bool(false));
let expr7 = "a < b && c > d || a == 1"; let res7 = evaluator.evaluate(expr7).unwrap();
assert_eq!(res7, serde_json::Value::Bool(true));
let expr8 = "a < b && (c > d || a == 1)"; let res8 = evaluator.evaluate(expr8).unwrap();
assert_eq!(res8, serde_json::Value::Bool(true));
}
#[test]
fn test_object_has_own_property_success() {
let mut context = HashMap::new();
let mut obj = serde_json::Map::new();
obj.insert("name".to_string(), Value::String("Alice".to_string()));
obj.insert("age".to_string(), Value::Number(30.into()));
obj.insert(
"".to_string(),
Value::String("empty_string_key".to_string()),
); context.insert("myObj".to_string(), Value::Object(obj));
let evaluator = Evaluator::new(context, HashMap::new());
assert_eq!(
evaluator.evaluate("myObj.hasOwnProperty('name')").unwrap(),
Value::Bool(true)
);
assert_eq!(
evaluator.evaluate("myObj.hasOwnProperty(\"age\")").unwrap(),
Value::Bool(true)
);
assert_eq!(
evaluator
.evaluate("myObj.hasOwnProperty('nonExistent')")
.unwrap(),
Value::Bool(false)
);
assert_eq!(
evaluator.evaluate("myObj.hasOwnProperty('')").unwrap(),
Value::Bool(true)
);
assert_eq!(
evaluator.evaluate("myObj.hasOwnProperty(123)").unwrap(),
Value::Bool(false)
); assert_eq!(
evaluator.evaluate("myObj.hasOwnProperty(true)").unwrap(),
Value::Bool(false)
);
let mut context2 = HashMap::new();
let mut obj_with_true_key = serde_json::Map::new();
obj_with_true_key.insert(
"true".to_string(),
Value::String("test value for boolean true key".to_string()),
);
context2.insert("objTrueKey".to_string(), Value::Object(obj_with_true_key));
let evaluator2_true = Evaluator::new(context2.clone(), HashMap::new());
assert_eq!(
evaluator2_true
.evaluate("objTrueKey.hasOwnProperty(true)")
.unwrap(), Value::Bool(true),
"objTrueKey should have property 'true' when called with boolean true"
);
assert_eq!(
evaluator2_true
.evaluate("objTrueKey.hasOwnProperty(false)")
.unwrap(), Value::Bool(false),
"objTrueKey should not have property 'false'"
);
let mut context3 = HashMap::new();
let mut obj_with_num_key_str = serde_json::Map::new();
obj_with_num_key_str.insert("123".to_string(), Value::Bool(true));
context3.insert(
"objNumStrKey".to_string(),
Value::Object(obj_with_num_key_str),
);
let evaluator3_num = Evaluator::new(context3, HashMap::new());
assert_eq!(
evaluator3_num
.evaluate("objNumStrKey.hasOwnProperty(123)")
.unwrap(),
Value::Bool(false)
);
assert_eq!(
evaluator3_num
.evaluate("objNumStrKey.hasOwnProperty('123')")
.unwrap(),
Value::Bool(true)
);
}
#[test]
fn test_object_has_own_property_arity_error() {
let mut context = HashMap::new();
context.insert("myObj".to_string(), Value::Object(serde_json::Map::new()));
let evaluator = Evaluator::new(context, HashMap::new());
let res_no_args = evaluator.evaluate("myObj.hasOwnProperty()");
match res_no_args {
Err(EvaluationError::CustomFunction(CustomFuncError::ArityError { expected, got })) => {
assert_eq!(expected, 1);
assert_eq!(got, 0);
}
_ => panic!(
"Expected ArityError for no arguments, got {:?}",
res_no_args
),
}
let res_many_args = evaluator.evaluate("myObj.hasOwnProperty('prop', 'extra')");
match res_many_args {
Err(EvaluationError::CustomFunction(CustomFuncError::ArityError { expected, got })) => {
assert_eq!(expected, 1);
assert_eq!(got, 2);
}
_ => panic!(
"Expected ArityError for many arguments, got {:?}",
res_many_args
),
}
}
#[test]
fn test_object_has_own_property_on_non_object() {
let mut context = HashMap::new();
context.insert("myArr".to_string(), Value::Array(vec![]));
context.insert("myStr".to_string(), Value::String("text".to_string()));
let evaluator = Evaluator::new(context, HashMap::new());
let expr_arr = "myArr.hasOwnProperty('length')";
let result_arr = evaluator.evaluate(expr_arr);
match result_arr {
Err(EvaluationError::TypeError(msg)) => {
assert_eq!(
msg,
"'null' (resulting from expression 'myArr.hasOwnProperty') is not a function."
);
}
_ => panic!(
"Expected TypeError for myArr.hasOwnProperty, got {:?}",
result_arr
),
}
let expr_str = "myStr.hasOwnProperty('length')";
let result_str = evaluator.evaluate(expr_str);
match result_str {
Err(EvaluationError::TypeError(msg)) => {
assert_eq!(msg, "Cannot read properties of null or primitive value: text (trying to access property: hasOwnProperty)");
}
_ => panic!(
"Expected TypeError for myStr.hasOwnProperty, got {:?}",
result_str
),
}
}
#[test]
fn test_object_has_own_property_nested() {
let mut context = HashMap::new();
let mut inner_obj = serde_json::Map::new();
inner_obj.insert("value".to_string(), Value::Bool(true));
let mut outer_obj = serde_json::Map::new();
outer_obj.insert("nestedObj".to_string(), Value::Object(inner_obj));
context.insert("item".to_string(), Value::Object(outer_obj));
let evaluator = Evaluator::new(context, HashMap::new());
assert_eq!(
evaluator
.evaluate("item.nestedObj.hasOwnProperty('value')")
.unwrap(),
Value::Bool(true)
);
assert_eq!(
evaluator
.evaluate("item.nestedObj.hasOwnProperty('nonExistent')")
.unwrap(),
Value::Bool(false)
);
}
#[test]
fn test_array_includes_success() {
let mut context = HashMap::new();
let arr = Value::Array(vec![
Value::Number(10.into()),
Value::String("hello".to_string()),
Value::Bool(true),
Value::Null,
]);
context.insert("myArr".to_string(), arr);
let evaluator = Evaluator::new(context, HashMap::new());
assert_eq!(
evaluator.evaluate("myArr.includes(10)").unwrap(),
Value::Bool(true)
);
assert_eq!(
evaluator.evaluate("myArr.includes(\"hello\")").unwrap(),
Value::Bool(true)
);
assert_eq!(
evaluator.evaluate("myArr.includes(true)").unwrap(),
Value::Bool(true)
);
assert_eq!(
evaluator.evaluate("myArr.includes(null)").unwrap(),
Value::Bool(true)
);
assert_eq!(
evaluator.evaluate("myArr.includes(20)").unwrap(),
Value::Bool(false)
);
assert_eq!(
evaluator.evaluate("myArr.includes(\"world\")").unwrap(),
Value::Bool(false)
);
assert_eq!(
evaluator.evaluate("myArr.includes(false)").unwrap(),
Value::Bool(false)
);
assert_eq!(
evaluator.evaluate("myArr.includes({})").unwrap(),
Value::Bool(false)
);
}
#[test]
fn test_array_includes_arity_error() {
let mut context = HashMap::new();
context.insert("myArr".to_string(), Value::Array(vec![]));
let evaluator = Evaluator::new(context, HashMap::new());
let res_no_args = evaluator.evaluate("myArr.includes()");
match res_no_args {
Err(EvaluationError::CustomFunction(CustomFuncError::ArityError { expected, got })) => {
assert_eq!(expected, 1);
assert_eq!(got, 0);
}
_ => panic!(
"Expected ArityError for no arguments, got {:?}",
res_no_args
),
}
let res_many_args = evaluator.evaluate("myArr.includes(1, 2)");
match res_many_args {
Err(EvaluationError::CustomFunction(CustomFuncError::ArityError { expected, got })) => {
assert_eq!(expected, 1);
assert_eq!(got, 2);
}
_ => panic!(
"Expected ArityError for many arguments, got {:?}",
res_many_args
),
}
}
#[test]
fn test_array_includes_on_non_array() {
let mut context = HashMap::new();
context.insert("notAnArray".to_string(), Value::String("hello".to_string()));
let evaluator = Evaluator::new(context, HashMap::new());
let result = evaluator.evaluate("notAnArray.includes(1)");
match result {
Err(EvaluationError::TypeError(msg)) => {
assert_eq!(msg, "Cannot read properties of null or primitive value: hello (trying to access property: includes)");
}
_ => panic!(
"Expected TypeError when calling .includes on non-array, got {:?}",
result
),
}
}
#[test]
fn test_array_includes_nested() {
let mut context = HashMap::new();
let mut obj = serde_json::Map::new();
let arr = Value::Array(vec![Value::Number(42.into())]);
obj.insert("nestedArr".to_string(), arr);
context.insert("myObj".to_string(), Value::Object(obj));
let evaluator = Evaluator::new(context, HashMap::new());
assert_eq!(
evaluator.evaluate("myObj.nestedArr.includes(42)").unwrap(),
Value::Bool(true)
);
assert_eq!(
evaluator.evaluate("myObj.nestedArr.includes(100)").unwrap(),
Value::Bool(false)
);
}
#[test]
fn test_custom_adder_success() {
let context = HashMap::new();
let mut custom_funcs: HashMap<String, Arc<dyn CustomFunction>> = HashMap::new();
custom_funcs.insert("custom_add".to_string(), Arc::new(MyTestAdder));
let evaluator = Evaluator::new(
context,
custom_funcs,
);
let result = evaluator.evaluate("custom_add(10, 20.5)").unwrap();
assert_eq!(result.as_f64(), Some(30.5));
let result_neg = evaluator.evaluate("custom_add(-5, -2)").unwrap();
assert_eq!(result_neg.as_f64(), Some(-7.0));
}
#[test]
fn test_custom_adder_arity_error_few_args() {
let context = HashMap::new();
let mut custom_funcs: HashMap<String, Arc<dyn CustomFunction>> = HashMap::new();
custom_funcs.insert("custom_add".to_string(), Arc::new(MyTestAdder));
let evaluator = Evaluator::new(context, custom_funcs);
let result = evaluator.evaluate("custom_add(10)");
match result {
Err(EvaluationError::CustomFunction(CustomFuncError::ArityError { expected, got })) => {
assert_eq!(expected, 2);
assert_eq!(got, 1);
}
_ => panic!("Expected ArityError, got {:?}", result),
}
}
#[test]
fn test_custom_adder_arity_error_many_args() {
let context = HashMap::new();
let mut custom_funcs: HashMap<String, Arc<dyn CustomFunction>> = HashMap::new();
custom_funcs.insert("custom_add".to_string(), Arc::new(MyTestAdder));
let evaluator = Evaluator::new(context, custom_funcs);
let result = evaluator.evaluate("custom_add(10, 20, 30)");
match result {
Err(EvaluationError::CustomFunction(CustomFuncError::ArityError { expected, got })) => {
assert_eq!(expected, 2);
assert_eq!(got, 3);
}
_ => panic!("Expected ArityError, got {:?}", result),
}
}
#[test]
fn test_custom_adder_type_error_arg1() {
let context = HashMap::new();
let mut custom_funcs: HashMap<String, Arc<dyn CustomFunction>> = HashMap::new();
custom_funcs.insert("custom_add".to_string(), Arc::new(MyTestAdder));
let evaluator = Evaluator::new(context, custom_funcs);
let result = evaluator.evaluate("custom_add('not_a_number', 10)");
match result {
Err(EvaluationError::CustomFunction(CustomFuncError::ArgumentError(msg))) => {
assert_eq!(msg, "Arguments must be numbers");
}
_ => panic!("Expected ArgumentError, got {:?}", result),
}
}
#[test]
fn test_custom_adder_type_error_arg2() {
let context = HashMap::new();
let mut custom_funcs: HashMap<String, Arc<dyn CustomFunction>> = HashMap::new();
custom_funcs.insert("custom_add".to_string(), Arc::new(MyTestAdder));
let evaluator = Evaluator::new(context, custom_funcs);
let result = evaluator.evaluate("custom_add(10, 'not_a_number')");
match result {
Err(EvaluationError::CustomFunction(CustomFuncError::ArgumentError(msg))) => {
assert_eq!(msg, "Arguments must be numbers");
}
_ => panic!("Expected ArgumentError, got {:?}", result),
}
}
#[test]
fn test_custom_adder_non_finite_number_error() {
let context = HashMap::new();
let mut custom_funcs: HashMap<String, Arc<dyn CustomFunction>> = HashMap::new();
custom_funcs.insert("custom_add".to_string(), Arc::new(MyTestAdder));
let evaluator = Evaluator::new(context, custom_funcs);
}
#[test]
fn test_array_length_direct() {
let mut context = HashMap::new();
let my_array = Value::Array(vec![Value::from(1), Value::from(2), Value::from(3)]);
context.insert("myArray".to_string(), my_array);
let evaluator = Evaluator::new(context, HashMap::new());
let result = evaluator.evaluate("myArray.length").unwrap();
assert_eq!(
result,
Value::Number(serde_json::Number::from_f64(3.0).unwrap())
);
}
#[test]
fn test_array_length_nested_in_object() {
let mut context = HashMap::new();
let my_array = Value::Array(vec![Value::from("a"), Value::from("b")]);
let mut obj = serde_json::Map::new();
obj.insert("arr".to_string(), my_array);
context.insert("myObj".to_string(), Value::Object(obj));
let evaluator = Evaluator::new(context, HashMap::new());
let result = evaluator.evaluate("myObj.arr.length").unwrap();
assert_eq!(
result,
Value::Number(serde_json::Number::from_f64(2.0).unwrap())
);
}
#[test]
fn test_length_on_non_array() {
let mut context = HashMap::new();
context.insert("myString".to_string(), Value::String("hello".to_string()));
context.insert("myNum".to_string(), Value::Number(123.into()));
let mut obj_without_length = serde_json::Map::new();
obj_without_length.insert("prop".to_string(), Value::from("value"));
context.insert("myObj".to_string(), Value::Object(obj_without_length));
context.insert("nullVar".to_string(), Value::Null);
let evaluator = Evaluator::new(context.clone(), HashMap::new());
let res_str = evaluator.evaluate("myString.length");
match res_str {
Err(EvaluationError::TypeError(msg)) => {
assert_eq!(
msg,
"Cannot read property 'length' of non-array/non-object value: hello"
);
}
_ => panic!("Expected TypeError for string.length, got {:?}", res_str),
}
let res_num = evaluator.evaluate("myNum.length");
match res_num {
Err(EvaluationError::TypeError(msg)) => {
assert_eq!(
msg,
"Cannot read property 'length' of non-array/non-object value: 123"
);
}
_ => panic!("Expected TypeError for number.length, got {:?}", res_num),
}
let res_obj = evaluator.evaluate("myObj.length").unwrap();
assert_eq!(res_obj, Value::Null);
let res_null = evaluator.evaluate("nullVar.length");
match res_null {
Err(EvaluationError::TypeError(msg)) => {
assert_eq!(
msg,
"Cannot read property 'length' of non-array/non-object value: null"
);
}
_ => panic!("Expected TypeError for null.length, got {:?}", res_null),
}
}
#[test]
fn test_other_property_on_array() {
let mut context = HashMap::new();
let my_array = Value::Array(vec![Value::from(1)]);
context.insert("myArray".to_string(), my_array);
let evaluator = Evaluator::new(context, HashMap::new());
let result = evaluator.evaluate("myArray.foo").unwrap();
assert_eq!(result, Value::Null); }
#[test]
fn test_property_access_on_object() {
let mut context = HashMap::new();
let mut obj = serde_json::Map::new();
obj.insert("name".to_string(), Value::String("Tester".to_string()));
obj.insert("age".to_string(), Value::Number(30.into()));
context.insert("user".to_string(), Value::Object(obj));
let evaluator = Evaluator::new(context, HashMap::new());
assert_eq!(
evaluator.evaluate("user.name").unwrap(),
Value::String("Tester".to_string())
);
assert_eq!(
evaluator.evaluate("user.age").unwrap(),
Value::Number(30.into())
);
assert_eq!(evaluator.evaluate("user.nonexistent").unwrap(), Value::Null);
}
#[test]
fn test_property_access_on_nested_object() {
let mut context = HashMap::new();
let mut inner_obj = serde_json::Map::new();
inner_obj.insert("value".to_string(), Value::Bool(true));
let mut outer_obj = serde_json::Map::new();
outer_obj.insert("nested".to_string(), Value::Object(inner_obj));
context.insert("item".to_string(), Value::Object(outer_obj));
let evaluator = Evaluator::new(context, HashMap::new());
assert_eq!(
evaluator.evaluate("item.nested.value").unwrap(),
Value::Bool(true)
);
assert_eq!(evaluator.evaluate("item.nested.foo").unwrap(), Value::Null);
let res_access_on_null = evaluator.evaluate("item.nonexistent.bar"); match res_access_on_null {
Err(EvaluationError::TypeError(msg)) => {
assert!(msg.contains("Cannot read properties of null or primitive value: null (trying to access property: bar)"));
}
_ => panic!(
"Expected TypeError for item.nonexistent.bar, got {:?}",
res_access_on_null
),
}
}
#[test]
fn test_property_access_on_null_or_primitive_object_error() {
let mut context = HashMap::new();
context.insert("s".to_string(), Value::String("text".to_string()));
context.insert("n".to_string(), Value::Number(123.into()));
context.insert("b".to_string(), Value::Bool(true));
context.insert("nl".to_string(), Value::Null);
let evaluator = Evaluator::new(context, HashMap::new());
let cases = vec!["s.foo", "n.bar", "b.baz", "nl.qux"];
for case in cases {
let result = evaluator.evaluate(case);
match result {
Err(EvaluationError::TypeError(msg)) => {
assert!(msg.starts_with("Cannot read properties of null or primitive value:"));
}
_ => panic!(
"Expected TypeError for property access on primitive/null, got {:?}",
result
),
}
}
}