pub struct EvalCtx {
pub fuel: usize,
pub ops: HashMap<String, Arc<CustomOp>>,
}Expand description
Per-evaluation context: fuel budget + custom operator map.
EvalCtx is Send + Sync. Build once, share via Arc<EvalCtx> across
threads. Each evaluate() call receives &EvalCtx and maintains its own
stack-local fuel counter — no shared mutable state.
Fields§
§fuel: usizeMaximum AST nodes the evaluator may visit. Default: 200.
ops: HashMap<String, Arc<CustomOp>>Custom operator map: operator name → handler.
Implementations§
Source§impl EvalCtx
impl EvalCtx
Sourcepub fn new() -> Self
pub fn new() -> Self
Create a context with 200 fuel and no custom operators.
§Example
use fsm_guards::EvalCtx;
let ctx = EvalCtx::new();
assert_eq!(ctx.fuel, 200);Examples found in repository?
examples/basic_guard.rs (line 9)
8fn main() {
9 let ctx = EvalCtx::new();
10
11 // Simple equality check
12 let approved = evaluate(
13 &json!({"==": [{"var": "status"}, "approved"]}),
14 &json!({"status": "approved"}),
15 &ctx,
16 )
17 .unwrap();
18 println!("approved: {approved}"); // true
19
20 // Compound rule
21 let ready = evaluate(
22 &json!({"and": [
23 {"==": [{"var": "status"}, "active"]},
24 {">=": [{"var": "score"}, 80]},
25 {"in": [{"var": "role"}, ["admin", "operator"]]}
26 ]}),
27 &json!({"status": "active", "score": 95, "role": "admin"}),
28 &ctx,
29 )
30 .unwrap();
31 println!("ready: {ready}"); // true
32
33 // Missing variable — detected before evaluation
34 match evaluate(
35 &json!({"var": "nonexistent"}),
36 &json!({"status": "active"}),
37 &ctx,
38 ) {
39 Err(GuardError::MissingVar(path)) => println!("missing var: {path}"),
40 other => println!("unexpected: {other:?}"),
41 }
42}More examples
examples/fuel_limits.rs (line 17)
8fn main() {
9 // ── short-circuit: or stops on first true ──────────────────────────────
10 // Build a 200-node branch that would blow a pre-walk budget.
11 let mut deep = json!({"==": [1, 2]});
12 for _ in 0..50 {
13 deep = json!({"and": [deep, {"==": [1, 2]}]});
14 }
15 let rule = json!({"or": [true, deep]});
16
17 let ctx = EvalCtx::new().with_fuel(5);
18 let result = evaluate(&rule, &json!({}), &ctx).unwrap();
19 println!("short-circuit or: {result}"); // true — only ~2 nodes visited
20
21 // ── budget exhausted ───────────────────────────────────────────────────
22 let rule = json!({"and": [
23 {"==": [1, 1]},
24 {"==": [2, 2]},
25 {"==": [3, 3]}
26 ]});
27
28 match evaluate(&rule, &json!({}), &EvalCtx::new().with_fuel(5)) {
29 Ok(v) => println!("passed with fuel:5 → {v}"),
30 Err(GuardError::FuelExceeded { limit }) => println!("exceeded limit:{limit}"),
31 Err(e) => println!("error: {e}"),
32 }
33
34 // ── default fuel (200) handles realistic guards comfortably ───────────
35 let ctx = EvalCtx::new(); // fuel: 200
36 let rule = json!({"and": [
37 {"==": [{"var": "status"}, "active"]},
38 {">=": [{"var": "score"}, 80]},
39 {"in": [{"var": "role"}, ["admin", "operator"]]}
40 ]});
41 let data = json!({"status": "active", "score": 95, "role": "admin"});
42 println!("realistic guard: {}", evaluate(&rule, &data, &ctx).unwrap()); // true
43}examples/custom_operators.rs (line 11)
9fn main() {
10 // ── state_in: membership check ─────────────────────────────────────────
11 let ctx = EvalCtx::new().with_op("state_in", |args| {
12 Ok(Value::Bool(args[1..].contains(&args[0])))
13 });
14
15 let rule = json!({"state_in": [{"var": "status"}, "active", "pending"]});
16 println!("active → {}", evaluate(&rule, &json!({"status": "active"}), &ctx).unwrap()); // true
17 println!("blocked → {}", evaluate(&rule, &json!({"status": "blocked"}), &ctx).unwrap()); // false
18
19 // ── multiple operators ─────────────────────────────────────────────────
20 let ctx = EvalCtx::new()
21 .with_op("state_in", |args| Ok(Value::Bool(args[1..].contains(&args[0]))))
22 .with_op("days_since", |args| {
23 // Stub: in production this would compute real elapsed days
24 let recorded = args[0].as_f64().unwrap_or(0.0);
25 let now = args[1].as_f64().unwrap_or(0.0);
26 Ok(json!(now - recorded))
27 });
28
29 let rule = json!({"and": [
30 {"state_in": [{"var": "status"}, "active", "pending"]},
31 {"<": [{"days_since": [{"var": "created_at"}, 1722470400.0]}, 30]}
32 ]});
33 let data = json!({"status": "active", "created_at": 1720000000.0});
34 println!("compound: {}", evaluate(&rule, &data, &ctx).unwrap());
35
36 // ── Arc sharing across threads ─────────────────────────────────────────
37 let shared_ctx = Arc::new(
38 EvalCtx::new().with_op("always_true", |_| Ok(Value::Bool(true))),
39 );
40
41 let handles: Vec<_> = (0..4)
42 .map(|i| {
43 let ctx = Arc::clone(&shared_ctx);
44 let rule = json!({"always_true": []});
45 std::thread::spawn(move || {
46 let result = evaluate(&rule, &json!({}), &ctx).unwrap();
47 println!("thread {i}: {result}");
48 })
49 })
50 .collect();
51
52 for h in handles {
53 h.join().unwrap();
54 }
55}Sourcepub fn with_fuel(self, fuel: usize) -> Self
pub fn with_fuel(self, fuel: usize) -> Self
Override the fuel budget.
§Example
use fsm_guards::EvalCtx;
let ctx = EvalCtx::new().with_fuel(50);
assert_eq!(ctx.fuel, 50);Examples found in repository?
examples/fuel_limits.rs (line 17)
8fn main() {
9 // ── short-circuit: or stops on first true ──────────────────────────────
10 // Build a 200-node branch that would blow a pre-walk budget.
11 let mut deep = json!({"==": [1, 2]});
12 for _ in 0..50 {
13 deep = json!({"and": [deep, {"==": [1, 2]}]});
14 }
15 let rule = json!({"or": [true, deep]});
16
17 let ctx = EvalCtx::new().with_fuel(5);
18 let result = evaluate(&rule, &json!({}), &ctx).unwrap();
19 println!("short-circuit or: {result}"); // true — only ~2 nodes visited
20
21 // ── budget exhausted ───────────────────────────────────────────────────
22 let rule = json!({"and": [
23 {"==": [1, 1]},
24 {"==": [2, 2]},
25 {"==": [3, 3]}
26 ]});
27
28 match evaluate(&rule, &json!({}), &EvalCtx::new().with_fuel(5)) {
29 Ok(v) => println!("passed with fuel:5 → {v}"),
30 Err(GuardError::FuelExceeded { limit }) => println!("exceeded limit:{limit}"),
31 Err(e) => println!("error: {e}"),
32 }
33
34 // ── default fuel (200) handles realistic guards comfortably ───────────
35 let ctx = EvalCtx::new(); // fuel: 200
36 let rule = json!({"and": [
37 {"==": [{"var": "status"}, "active"]},
38 {">=": [{"var": "score"}, 80]},
39 {"in": [{"var": "role"}, ["admin", "operator"]]}
40 ]});
41 let data = json!({"status": "active", "score": 95, "role": "admin"});
42 println!("realistic guard: {}", evaluate(&rule, &data, &ctx).unwrap()); // true
43}Sourcepub fn with_op<F>(self, name: &str, f: F) -> Self
pub fn with_op<F>(self, name: &str, f: F) -> Self
Register a custom operator. Operators are matched by exact name string.
The handler receives already-evaluated argument values. It may be called
from multiple threads if the EvalCtx is shared via Arc.
§Example
use fsm_guards::{evaluate, EvalCtx};
use serde_json::json;
let ctx = EvalCtx::new().with_op("double", |args| {
let n = args[0].as_f64().unwrap_or(0.0);
Ok(serde_json::json!(n * 2.0))
});
// {"double": [5]} returns 10.0, which is truthy.
assert!(evaluate(&json!({"double": [5]}), &json!({}), &ctx).unwrap());Examples found in repository?
examples/custom_operators.rs (lines 11-13)
9fn main() {
10 // ── state_in: membership check ─────────────────────────────────────────
11 let ctx = EvalCtx::new().with_op("state_in", |args| {
12 Ok(Value::Bool(args[1..].contains(&args[0])))
13 });
14
15 let rule = json!({"state_in": [{"var": "status"}, "active", "pending"]});
16 println!("active → {}", evaluate(&rule, &json!({"status": "active"}), &ctx).unwrap()); // true
17 println!("blocked → {}", evaluate(&rule, &json!({"status": "blocked"}), &ctx).unwrap()); // false
18
19 // ── multiple operators ─────────────────────────────────────────────────
20 let ctx = EvalCtx::new()
21 .with_op("state_in", |args| Ok(Value::Bool(args[1..].contains(&args[0]))))
22 .with_op("days_since", |args| {
23 // Stub: in production this would compute real elapsed days
24 let recorded = args[0].as_f64().unwrap_or(0.0);
25 let now = args[1].as_f64().unwrap_or(0.0);
26 Ok(json!(now - recorded))
27 });
28
29 let rule = json!({"and": [
30 {"state_in": [{"var": "status"}, "active", "pending"]},
31 {"<": [{"days_since": [{"var": "created_at"}, 1722470400.0]}, 30]}
32 ]});
33 let data = json!({"status": "active", "created_at": 1720000000.0});
34 println!("compound: {}", evaluate(&rule, &data, &ctx).unwrap());
35
36 // ── Arc sharing across threads ─────────────────────────────────────────
37 let shared_ctx = Arc::new(
38 EvalCtx::new().with_op("always_true", |_| Ok(Value::Bool(true))),
39 );
40
41 let handles: Vec<_> = (0..4)
42 .map(|i| {
43 let ctx = Arc::clone(&shared_ctx);
44 let rule = json!({"always_true": []});
45 std::thread::spawn(move || {
46 let result = evaluate(&rule, &json!({}), &ctx).unwrap();
47 println!("thread {i}: {result}");
48 })
49 })
50 .collect();
51
52 for h in handles {
53 h.join().unwrap();
54 }
55}Trait Implementations§
Auto Trait Implementations§
impl !RefUnwindSafe for EvalCtx
impl !UnwindSafe for EvalCtx
impl Freeze for EvalCtx
impl Send for EvalCtx
impl Sync for EvalCtx
impl Unpin for EvalCtx
impl UnsafeUnpin for EvalCtx
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more