fsm-guards 0.1.1

Fuel-metered JSONLogic guard evaluation with per-call custom operators — forked from jsonlogic-rs
Documentation
  • Coverage
  • 66.67%
    22 out of 33 items documented5 out of 9 items with examples
  • Size
  • Source code size: 94.11 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 764.9 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 7s Average build duration of successful builds.
  • all releases: 6s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • fastyoke/fsm-guards
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • fastyoke-internal

fsm-guards

Fuel-metered JSONLogic guard evaluation for FSM transition engines.

A fork of 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 classificationGuardError with five distinct variants replacing stringly-typed errors.
  • Per-evaluation custom operators — register extension functions on EvalCtx for each call; no global state.

Install

[dependencies]
fsm-guards = "0.1"

Quick start

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

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 dataGuardError::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

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

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

EvalCtx

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. 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 v0.5 by Bestow Inc.