fsm-guards 0.1.1

Fuel-metered JSONLogic guard evaluation with per-call custom operators — forked from jsonlogic-rs
Documentation
# fsm-guards

Fuel-metered JSONLogic guard evaluation for FSM transition engines.

A fork of [jsonlogic-rs](https://crates.io/crates/jsonlogic-rs) v0.5, extended with:

- **In-loop fuel metering** — node-visit budget that short-circuits correctly: `{"or": [true, huge_branch]}` with `fuel: 10` costs 2 nodes, not 1000.
- **Typed error classification**`GuardError` with five distinct variants replacing stringly-typed errors.
- **Per-evaluation custom operators** — register extension functions on `EvalCtx` for each call; no global state.

## Install

```toml
[dependencies]
fsm-guards = "0.1"
```

## Quick start

```rust
use fsm_guards::{evaluate, EvalCtx};
use serde_json::json;

let ctx = EvalCtx::new()
    .with_fuel(50)
    .with_op("state_in", |args| {
        Ok(serde_json::Value::Bool(args[1..].contains(&args[0])))
    });

let rule = json!({
    "and": [
        {"state_in": [{"var": "status"}, "active", "pending"]},
        {">=": [{"var": "score"}, 80]}
    ]
});

let data = json!({"status": "active", "score": 95});
assert!(evaluate(&rule, &data, &ctx).unwrap());
```

## API

### `evaluate` — guard tier

```rust
pub fn evaluate(rule: &Value, data: &Value, ctx: &EvalCtx) -> Result<bool, GuardError>
```

1. Pre-walks for `{"var": "X"}` references whose root key is absent from `data``GuardError::MissingVar`.
2. Runs the evaluator with `ctx.fuel` as the node budget.
3. Coerces the result to `bool` (JSONLogic truthy: null/false/0/""/[] → false).

### `apply` — raw tier

```rust
pub fn apply(rule: &Value, data: &Value) -> Result<Value, ApplyError>
```

No fuel limit. No custom operators. Returns the raw JSONLogic result value.

### `EvalCtx`

```rust
let ctx = EvalCtx::new()          // fuel: 200, no custom ops
    .with_fuel(50)
    .with_op("my_op", |args| Ok(args[0].clone()));
```

`EvalCtx` is `Send + Sync`. Wrap in `Arc` to share across threads; each `evaluate` call uses its own stack-local fuel counter.

### Error variants

| Variant | Meaning |
|---|---|
| `GuardError::Malformed(msg)` | Structural AST problem or unknown operator |
| `GuardError::MissingVar(path)` | `{"var": "X"}` where root key absent from data |
| `GuardError::TypeError(msg)` | Type coercion or argument-arity error |
| `GuardError::FuelExceeded { limit }` | Node budget exhausted |
| `GuardError::CustomOpFailed { op, source }` | Registered operator returned `Err` |

## Fuel accounting

Fuel is decremented for every node visit — literals, operators, all values. Short-circuit operators charge only for visited branches:

```
{"or": [true, <200-node-branch>]}  with fuel:10 → Ok(true)   // 2 nodes visited
{"or": [false, {"==": [1,1]}]}     with fuel:10 → Ok(true)   // 3 nodes visited
```

Default: 200 nodes. FastYoke's production limit: 50 nodes for trigger predicates, 200 for guard rules.

## JSONLogic compatibility

Passes 273/278 of the [JSONLogic test suite](https://jsonlogic.com/tests.json). The 5 failures are inherited jsonlogic-rs v0.5 limitations:
- Array literals containing `{"var": "..."}` — vars inside array literals are not recursively evaluated
- `?:` ternary operator — not implemented
- `missing` with an expression argument — does not pre-evaluate its argument

## License

MIT. Forked from [jsonlogic-rs](https://crates.io/crates/jsonlogic-rs) v0.5 by Bestow Inc.