Skip to main content

EvalCtx

Struct EvalCtx 

Source
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: usize

Maximum AST nodes the evaluator may visit. Default: 200.

§ops: HashMap<String, Arc<CustomOp>>

Custom operator map: operator name → handler.

Implementations§

Source§

impl EvalCtx

Source

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
Hide additional 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}
Source

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}
Source

pub fn with_op<F>(self, name: &str, f: F) -> Self
where F: Fn(&[Value]) -> Result<Value, Box<dyn Error + Send + Sync>> + Send + Sync + 'static,

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§

Source§

impl Default for EvalCtx

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.