1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//! 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 use ;
pub use apply;
pub use ;
pub use evaluate;