use fsm_guards::{evaluate, EvalCtx, GuardError};
use serde_json::json;
use std::sync::Arc;
#[test]
fn ok_true() {
let ctx = EvalCtx::new();
assert_eq!(evaluate(&json!({"==": [1, 1]}), &json!({}), &ctx).unwrap(), true);
}
#[test]
fn ok_false() {
let ctx = EvalCtx::new();
assert_eq!(evaluate(&json!({"==": [1, 2]}), &json!({}), &ctx).unwrap(), false);
}
#[test]
fn missing_var_root() {
let ctx = EvalCtx::new();
let rule = json!({"var": "status"});
let data = json!({"other": 1});
assert!(matches!(
evaluate(&rule, &data, &ctx),
Err(GuardError::MissingVar(s)) if s == "status"
));
}
#[test]
fn missing_var_nested_key_is_ok() {
let ctx = EvalCtx::new();
let rule = json!({"==": [{"var": "obj.nested"}, null]});
let data = json!({"obj": {}});
assert_eq!(evaluate(&rule, &data, &ctx).unwrap(), true);
}
#[test]
fn missing_var_in_compound_rule() {
let ctx = EvalCtx::new();
let rule = json!({"and": [{"==": [1, 1]}, {"var": "ghost"}]});
let data = json!({});
assert!(matches!(
evaluate(&rule, &data, &ctx),
Err(GuardError::MissingVar(s)) if s == "ghost"
));
}
#[test]
fn fuel_exceeded() {
let ctx = EvalCtx::new().with_fuel(3);
let rule = json!({"and": [{"and": [{"and": [{"==": [1, 1]}, true]}, true]}, true]});
let data = json!({});
assert!(matches!(
evaluate(&rule, &data, &ctx),
Err(GuardError::FuelExceeded { limit: 3 })
));
}
#[test]
fn custom_op_ok_composes_in_and() {
let ctx = EvalCtx::new()
.with_op("always_true", |_args| Ok(serde_json::Value::Bool(true)));
let rule = json!({"and": [{"always_true": []}, {"==": [1, 1]}]});
let data = json!({});
assert_eq!(evaluate(&rule, &data, &ctx).unwrap(), true);
}
#[test]
fn custom_op_receives_evaluated_args() {
let ctx = EvalCtx::new().with_op("sum_gt", |args| {
let a = args[0].as_f64().unwrap_or(0.0);
let b = args[1].as_f64().unwrap_or(0.0);
let threshold = args[2].as_f64().unwrap_or(0.0);
Ok(serde_json::Value::Bool(a + b > threshold))
});
let rule = json!({"sum_gt": [{"var": "x"}, {"var": "y"}, 10]});
let data_true = json!({"x": 7, "y": 6});
assert_eq!(evaluate(&rule, &data_true, &ctx).unwrap(), true);
let data_false = json!({"x": 3, "y": 4});
assert_eq!(evaluate(&rule, &data_false, &ctx).unwrap(), false);
}
#[test]
fn custom_op_error_surfaces_as_custom_op_failed() {
let ctx = EvalCtx::new().with_op("explode", |_args| {
Err(Box::from("intentional failure") as Box<dyn std::error::Error + Send + Sync>)
});
let rule = json!({"explode": []});
let data = json!({});
assert!(matches!(
evaluate(&rule, &data, &ctx),
Err(GuardError::CustomOpFailed { op, .. }) if op == "explode"
));
}
#[test]
fn unknown_op_surfaces_as_malformed() {
let ctx = EvalCtx::new(); let rule = json!({"state_in": ["A", "B"]});
let data = json!({});
assert!(matches!(
evaluate(&rule, &data, &ctx),
Err(GuardError::Malformed(_))
));
}
#[test]
fn default_fuel_not_exceeded_on_realistic_guard() {
let ctx = EvalCtx::new(); let rule = json!({
"and": [
{"==": [{"var": "status"}, "active"]},
{">=": [{"var": "score"}, 80]},
{"in": [{"var": "role"}, ["admin", "operator"]]}
]
});
let data = json!({"status": "active", "score": 95, "role": "admin"});
assert_eq!(evaluate(&rule, &data, &ctx).unwrap(), true);
}
#[test]
fn evalctx_shared_via_arc_across_threads() {
let ctx = Arc::new(
EvalCtx::new().with_op("double", |args| {
let n = args[0].as_f64().unwrap_or(0.0);
Ok(serde_json::json!(n * 2.0))
}),
);
let ctx2 = Arc::clone(&ctx);
let rule = json!({"double": [5]});
let data = json!({});
let handle = std::thread::spawn(move || evaluate(&rule, &data, &ctx2).unwrap());
assert!(handle.join().unwrap());
}
#[test]
fn non_object_data_skips_prewalk_returns_false() {
let ctx = EvalCtx::new();
let rule = json!({"var": "x"});
assert_eq!(evaluate(&rule, &serde_json::Value::Null, &ctx).unwrap(), false);
}
#[test]
fn var_array_form_default_not_treated_as_missing() {
let ctx = EvalCtx::new();
let rule = json!({"var": ["ghost", "fallback"]});
let data = json!({});
assert_eq!(evaluate(&rule, &data, &ctx).unwrap(), true);
}