datalogic_rs/lib.rs
1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3#![warn(unreachable_pub)]
4// Enable the `#[doc(cfg(...))]` attribute on docs.rs builds so feature-gated
5// public items render with a "Available on crate feature X only" badge. The
6// matching `--cfg docsrs` is passed by `[package.metadata.docs.rs]` in
7// Cargo.toml; on a regular stable build this attribute is inert.
8#![cfg_attr(docsrs, feature(doc_cfg))]
9
10//! # datalogic-rs
11//!
12//! A high-performance, thread-safe Rust implementation of JSONLogic.
13//!
14//! ## Overview
15//!
16//! `datalogic-rs` provides a powerful rule evaluation engine that compiles JSONLogic
17//! expressions into optimized, reusable structures that can be evaluated across
18//! multiple threads with zero overhead.
19//!
20//! ## Key Features
21//!
22//! - **Compilation-based optimization**: Parse once, evaluate many times
23//! - **Thread-safe by design**: Share compiled logic across threads with `Arc`
24//! - **64 built-in operators**: Complete JSONLogic compatibility plus extensions
25//! - **Arena-allocated evaluation**: Results live in a `bumpalo::Bump` arena and can borrow directly into caller input for zero-copy paths
26//! - **Extensible**: Add custom operators via the [`CustomOperator`] trait
27//! - **Structured templates**: Preserve object structure for dynamic outputs
28//!
29//! ## Quick Start (one-shot)
30//!
31//! ```rust
32//! use datalogic_rs::Engine;
33//!
34//! let engine = Engine::new();
35//! let result = engine.eval_str(
36//! r#"{"==": [{"var": "status"}, "active"]}"#,
37//! r#"{"status": "active"}"#,
38//! ).unwrap();
39//! assert_eq!(result, "true");
40//! ```
41//!
42//! ## Reusing the arena across many evaluations
43//!
44//! For high-throughput callers, open a [`Session`] handle. It owns a
45//! [`bumpalo::Bump`], resets it between calls, and returns owned results so
46//! you don't have to juggle arena lifetimes:
47//!
48//! ```rust
49//! use datalogic_rs::Engine;
50//!
51//! let engine = Engine::new();
52//! let compiled = engine.compile(r#"{"+": [{"var": "x"}, 1]}"#).unwrap();
53//! let mut session = engine.session();
54//!
55//! for x in 0..3 {
56//! let payload = format!(r#"{{"x": {}}}"#, x);
57//! let result = session.eval_str(&compiled, &payload).unwrap();
58//! assert_eq!(result, (x + 1).to_string());
59//! // The session does not auto-reset; bound peak memory by
60//! // resetting between iterations (constant-time, reuses chunks).
61//! session.reset();
62//! }
63//! ```
64//!
65//! ## Power-user (compile once, evaluate many, zero-copy results)
66//!
67//! When the result borrow can stay scoped to a caller-managed
68//! [`bumpalo::Bump`], skip the deep-clone and use [`Engine::evaluate`]
69//! directly. `evaluate` accepts any input shape via [`EvalInput`]:
70//! `&str`, `&OwnedDataValue`, `&serde_json::Value`, an owned `DataValue<'a>`,
71//! or an existing `&'a DataValue<'a>`.
72//!
73//! ```rust
74//! use bumpalo::Bump;
75//! use datalogic_rs::Engine;
76//!
77//! let engine = Engine::new();
78//! let compiled = engine.compile(r#"{"==": [{"var": "status"}, "active"]}"#).unwrap();
79//!
80//! let arena = Bump::new();
81//! let result = engine.evaluate(&compiled, r#"{"status": "active"}"#, &arena).unwrap();
82//! assert_eq!(result.as_bool(), Some(true));
83//! ```
84//!
85//! ## Architecture
86//!
87//! The library uses a two-phase approach:
88//!
89//! 1. **Compilation**: JSON logic is parsed into `Logic` with OpCode dispatch
90//! 2. **Evaluation**: Compiled logic is evaluated through arena dispatch — results
91//! are `&'a DataValue<'a>` allocated in a `bumpalo::Bump` for the duration of
92//! one evaluate call.
93//!
94//! This design enables sharing compiled logic across threads, eliminates
95//! repeated parsing overhead, and lets read-through operations like `var`
96//! return zero-copy borrows into the caller's input data.
97
98mod arena;
99mod arena_ext;
100mod builder;
101mod compile;
102mod config;
103mod engine;
104mod error;
105mod eval_input;
106mod logic_input;
107mod node;
108mod node_serialize;
109mod opcode;
110pub mod operator;
111mod operators;
112mod parsed_data;
113mod path;
114mod result_output;
115#[cfg(feature = "serde_json")]
116mod serde_bridge;
117mod session;
118mod top_level;
119#[cfg(feature = "trace")]
120mod trace;
121
122pub use arena::DataValue;
123pub use arena_ext::ArenaExt;
124pub use builder::EngineBuilder;
125/// The [`bumpalo`] arena allocator, re-exported.
126///
127/// `Engine::evaluate` and the [`CustomOperator`] trait both take a
128/// `&'a bumpalo::Bump` parameter, so callers need a way to construct
129/// arenas. Re-exporting locks the major version of `bumpalo` to whatever
130/// `datalogic-rs` itself depends on — pair with `use datalogic_rs::bumpalo`
131/// instead of an independent `bumpalo` dep to avoid major-version skew.
132pub use bumpalo;
133pub use config::{
134 DivisionByZeroHandling, EvaluationConfig, NanHandling, NumericCoercionConfig, TruthyEvaluator,
135};
136/// The `datavalue` crate, re-exported. `datalogic-rs` builds on `datavalue`'s
137/// owned and borrowed value types — accessing them through this module makes
138/// the dependency explicit at the use site.
139///
140/// # Working with `DataValue`
141///
142/// Evaluation returns [`DataValue`] (re-exported at the crate root and
143/// also reachable as `datalogic_rs::datavalue::DataValue`). It's an
144/// arena-allocated JSON-shaped value tree borrowed from a
145/// [`bumpalo::Bump`]. The accessors most callers reach for live in this
146/// re-exported crate:
147///
148/// - **Type predicates** — `.is_null()`, `.is_bool()`, `.is_number()`,
149/// `.is_string()`, `.is_array()`, `.is_object()`.
150/// - **Owned readers** — `.as_bool()`, `.as_i64()`, `.as_f64()`,
151/// `.as_str()`, `.as_array()`, `.as_object()`. Each returns
152/// `Option<…>`; the `None` case is "wrong variant," not a runtime error.
153/// - **Indexing** — `value["key"]` / `value[idx]` returns `&DataValue`
154/// (or the `Null` singleton on miss, matching `serde_json::Value`).
155///
156/// # Owned vs borrowed
157///
158/// [`DataValue<'a>`](datavalue::DataValue) borrows from a `Bump`;
159/// [`OwnedDataValue`](datavalue::OwnedDataValue) is the heap-owned
160/// counterpart. Use the owned form when you need to outlive the arena —
161/// caching a result, returning across an `await`, sending across a
162/// channel. Convert via `borrowed.to_owned()` and `owned.to_arena(&bump)`.
163///
164/// # Crossing the `serde_json` boundary
165///
166/// Conversions to / from `serde_json::Value` are gated behind the
167/// `serde_json` feature (kept off by default so the crate has zero
168/// external dependencies in the minimal build). With `serde_json`
169/// enabled, pass a `&serde_json::Value` (or any `&T: Serialize`) into
170/// any `eval*` method via [`EvalInput`] / [`IntoLogic`], and ask for a
171/// `serde_json::Value` (or any `T: DeserializeOwned`) back via
172// `Engine::eval_into` / `Session::eval_into` are gated behind
173// `serde_json`; link them when the feature is on, otherwise emit them
174// as code text so default-features `cargo doc` doesn't break.
175#[cfg_attr(
176 feature = "serde_json",
177 doc = "[`Engine::eval_into`] / [`Session::eval_into`]. For the `DataValue"
178)]
179#[cfg_attr(
180 not(feature = "serde_json"),
181 doc = "`Engine::eval_into` / `Session::eval_into`. For the `DataValue"
182)]
183/// → JSON String` path use the standard `value.to_string()`, which is
184/// what [`Engine::eval_str`] uses internally.
185pub use datavalue;
186pub use engine::Engine;
187pub use error::{CustomErrorSource, Error, ErrorKind};
188pub use eval_input::{EvalInput, OwnedInput};
189pub use logic_input::IntoLogic;
190pub use node::Logic;
191pub use parsed_data::ParsedData;
192pub use path::PathStep;
193pub use result_output::FromDataValue;
194pub use session::Session;
195#[cfg(feature = "serde_json")]
196#[cfg_attr(docsrs, doc(cfg(feature = "serde_json")))]
197pub use top_level::eval_into;
198pub use top_level::{compile, eval, eval_str};
199#[cfg(feature = "trace")]
200#[cfg_attr(docsrs, doc(cfg(feature = "trace")))]
201pub use trace::{ExecutionStep, ExpressionNode, TracedRun, TracedSession};
202
203// `CompiledNode`, `OpCode`, `MetadataHint`, `PathSegment`, `ReduceHint` were
204// public in 4.x. They are compile-internal in v5; consumers reach for them
205// via `crate::node::*` / `crate::opcode::*` directly.
206pub(crate) use node::CompiledNode;
207pub(crate) use opcode::OpCode;
208
209/// Result type for Engine operations
210pub type Result<T> = std::result::Result<T, Error>;
211
212/// Custom operator hook for the [`Engine`].
213///
214/// Implementations receive args **already evaluated** as borrowed
215/// [`DataValue`] references and return a `&'a DataValue<'a>` result
216/// allocated in the supplied [`bumpalo::Bump`] arena.
217///
218/// ## Lifetime
219///
220/// `'a` is the arena lifetime, tied to the [`bumpalo::Bump`] allocator
221/// that lives for the duration of one [`Engine::evaluate`] call. Args
222/// borrow from the caller's input and from prior arena allocations; the
223/// returned `&'a DataValue<'a>` must be allocated in the arena (or be a
224/// preallocated singleton) — never a stack reference.
225///
226/// ## Example
227///
228/// ```rust
229/// use datalogic_rs::{CustomOperator, DataValue, Engine, Result, operator::EvalContext};
230/// use bumpalo::Bump;
231///
232/// struct DoubleArena;
233/// impl CustomOperator for DoubleArena {
234/// fn evaluate<'a>(
235/// &self,
236/// args: &[&'a DataValue<'a>],
237/// _ctx: &mut EvalContext<'_, 'a>,
238/// arena: &'a Bump,
239/// ) -> Result<&'a DataValue<'a>> {
240/// let n = args.first().and_then(|v| v.as_f64()).unwrap_or(0.0);
241/// Ok(arena.alloc(DataValue::from_f64(n * 2.0)))
242/// }
243/// }
244///
245/// let engine = Engine::builder().add_operator("double", DoubleArena).build();
246///
247/// let result = engine.eval_str(r#"{"double": 21}"#, "null").unwrap();
248/// assert_eq!(result, "42");
249/// ```
250///
251/// ## Stability
252///
253/// This trait is the headline extension point of the crate and is
254/// intentionally not sealed. Within the **5.x series** the only changes
255/// that will be made to this trait are *default-method additions* — no
256/// new required methods, no signature changes to [`Self::evaluate`], no
257/// lifetime restructuring. Implementations written against 5.0 will
258/// compile against every 5.x release without modification. Any breaking
259/// change here requires a 6.0 bump.
260///
261/// The opaque types in the signature ([`crate::DataValue`],
262/// [`operator::EvalContext`], [`bumpalo::Bump`]) may evolve internally
263/// without breaking this contract, since their public surface is the
264/// stable boundary.
265pub trait CustomOperator: Send + Sync {
266 /// Evaluate this operator with arena-allocated args and result.
267 ///
268 /// # Arguments
269 ///
270 /// * `args` — pre-evaluated args as `&'a DataValue<'a>`. The arena
271 /// dispatcher has already recursed into each arg's expression tree.
272 /// * `ctx` — opaque view into the engine's evaluation context. Most
273 /// operators ignore this; it exposes [`operator::EvalContext::root_input`]
274 /// and [`operator::EvalContext::depth`] for the rare case where an
275 /// operator's behaviour depends on the surrounding context.
276 /// * `arena` — the [`bumpalo::Bump`] allocator. Use `arena.alloc(...)`
277 /// for arena values, `arena.alloc_str(...)` for strings. For the
278 /// common case of returning a typed `DataValue` result, prefer the
279 /// one-call helpers on [`ArenaExt`] (`arena.f64(n)`,
280 /// `arena.string(s)`, `arena.bool(b)`, …) — they are zero-cost
281 /// over the manual form and short-circuit to preallocated
282 /// singletons for `null`, booleans, small ints, and empty
283 /// string/array/object.
284 fn evaluate<'a>(
285 &self,
286 args: &[&'a DataValue<'a>],
287 ctx: &mut operator::EvalContext<'_, 'a>,
288 arena: &'a bumpalo::Bump,
289 ) -> Result<&'a DataValue<'a>>;
290}
291
292// `Box<dyn CustomOperator>` itself implements `CustomOperator` by
293// delegating to the inner trait object. This collapses what used to be
294// two separate registration methods on `EngineBuilder` (`add_operator`
295// for typed operators, `add_operator_box` for pre-boxed trait objects)
296// into a single entry point: `EngineBuilder::add_operator(name, op)`
297// accepts either a typed `T: CustomOperator + 'static` or a
298// `Box<dyn CustomOperator>` produced by a runtime registry.
299impl CustomOperator for Box<dyn CustomOperator> {
300 #[inline]
301 fn evaluate<'a>(
302 &self,
303 args: &[&'a DataValue<'a>],
304 ctx: &mut operator::EvalContext<'_, 'a>,
305 arena: &'a bumpalo::Bump,
306 ) -> Result<&'a DataValue<'a>> {
307 (**self).evaluate(args, ctx, arena)
308 }
309}