fsm-guards 0.1.1

Fuel-metered JSONLogic guard evaluation with per-call custom operators — forked from jsonlogic-rs
Documentation
//! Fuel-metered JSONLogic guard evaluation for FSM transition engines.
//!
//! `fsm-guards` evaluates [JSONLogic](https://jsonlogic.com) rules as boolean
//! predicates with three production features missing from the upstream
//! `jsonlogic-rs` crate:
//!
//! - **In-loop fuel metering** — a node-visit budget threaded through the
//!   recursive evaluator. Short-circuit operators (`or`, `and`, `if`) charge
//!   only for branches they actually visit. A rule like
//!   `{"or": [true, <200-node-branch>]}` with `fuel: 10` succeeds in 2 node
//!   visits, not 200.
//! - **Typed errors** — [`GuardError`] carries five distinct variants so
//!   callers can distinguish missing variables from type errors from budget
//!   exhaustion.
//! - **Per-evaluation custom operators** — register extension functions on
//!   [`EvalCtx`] for each call. No global registry; no cross-tenant state.
//!
//! # Quick start
//!
//! ```rust
//! use fsm_guards::{evaluate, EvalCtx};
//! use serde_json::json;
//!
//! let ctx = EvalCtx::new();
//! let rule = json!({"and": [
//!     {"==": [{"var": "status"}, "active"]},
//!     {">=": [{"var": "score"}, 80]}
//! ]});
//! let data = json!({"status": "active", "score": 95});
//!
//! assert!(evaluate(&rule, &data, &ctx).unwrap());
//! ```
//!
//! # Two-tier API
//!
//! ## High-level: [`evaluate`]
//!
//! ```rust
//! use fsm_guards::{evaluate, EvalCtx, GuardError};
//! use serde_json::json;
//!
//! let ctx = EvalCtx::new().with_fuel(50);
//!
//! // MissingVar: pre-walk detects the absent key before evaluation starts.
//! let rule = json!({"var": "ghost"});
//! let data = json!({"status": "active"});
//! assert!(matches!(evaluate(&rule, &data, &ctx), Err(GuardError::MissingVar(k)) if k == "ghost"));
//! ```
//!
//! ## Low-level: [`apply`]
//!
//! Returns the raw JSONLogic result value — useful when the result is not a
//! boolean (e.g. extracting a field value or building a derived value).
//!
//! ```rust
//! use fsm_guards::apply;
//! use serde_json::json;
//!
//! let result = apply(&json!({"cat": ["hello", " ", "world"]}), &json!({})).unwrap();
//! assert_eq!(result, json!("hello world"));
//! ```
//!
//! # Custom operators
//!
//! Register domain-specific operators on [`EvalCtx`]. They receive
//! already-evaluated argument values (not raw AST nodes) and compose naturally
//! inside compound expressions.
//!
//! ```rust
//! use fsm_guards::{evaluate, EvalCtx};
//! use serde_json::{json, Value};
//!
//! let ctx = EvalCtx::new()
//!     .with_op("state_in", |args| {
//!         // args[0] = current state value, args[1..] = allowed states
//!         Ok(Value::Bool(args[1..].contains(&args[0])))
//!     });
//!
//! let rule = json!({"state_in": [{"var": "status"}, "active", "pending"]});
//! let data = json!({"status": "active"});
//! assert!(evaluate(&rule, &data, &ctx).unwrap());
//!
//! let data_blocked = json!({"status": "blocked"});
//! assert!(!evaluate(&rule, &data_blocked, &ctx).unwrap());
//! ```
//!
//! # Fuel accounting
//!
//! Fuel is decremented for every node visit — operators *and* literal values.
//! Short-circuit operators charge only for visited branches.
//!
//! ```rust
//! use fsm_guards::{evaluate, EvalCtx, GuardError};
//! use serde_json::json;
//!
//! // {"or": [true, <deep-branch>]} with fuel:5 — short-circuits on `true`,
//! // deep branch never visited, only ~2 nodes charged.
//! let mut deep = json!({"==": [1, 2]});
//! for _ in 0..50 {
//!     deep = json!({"and": [deep, {"==": [1, 2]}]});
//! }
//! let rule = json!({"or": [true, deep]});
//! let ctx = EvalCtx::new().with_fuel(5);
//! assert_eq!(evaluate(&rule, &json!({}), &ctx).unwrap(), true);
//!
//! // A rule that exceeds the budget:
//! let deep_rule = json!({"and": [{"==": [1,1]}, {"and": [{"==": [1,1]}, {"==": [1,1]}]}]});
//! let tight_ctx = EvalCtx::new().with_fuel(4);
//! assert!(matches!(
//!     evaluate(&deep_rule, &json!({}), &tight_ctx),
//!     Err(GuardError::FuelExceeded { limit: 4 })
//! ));
//! ```
//!
//! # Thread safety
//!
//! [`EvalCtx`] is `Send + Sync`. Wrap in `Arc` to share across threads; each
//! `evaluate()` call uses its own stack-local fuel counter.
//!
//! ```rust
//! use fsm_guards::{evaluate, EvalCtx};
//! use serde_json::json;
//! use std::sync::Arc;
//!
//! let ctx = Arc::new(EvalCtx::new());
//! let ctx2 = Arc::clone(&ctx);
//! let rule = json!({"==": [1, 1]});
//! let data = json!({});
//!
//! let handle = std::thread::spawn(move || evaluate(&rule, &data, &ctx2).unwrap());
//! assert!(handle.join().unwrap());
//! ```

pub mod ctx;
pub mod engine;
pub mod error;
pub mod guard;

pub use ctx::{CustomOp, EvalCtx};
pub use engine::apply;
pub use error::{ApplyError, GuardError};
pub use guard::evaluate;