Skip to main content

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//! - **84 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
98// Compile and run the crate README's Rust blocks as doctests, so the
99// README crates.io renders cannot drift from the API. Gated on the
100// features its examples use; `cargo test --all-features` runs them.
101#[cfg(all(
102    doctest,
103    feature = "serde_json",
104    feature = "templating",
105    feature = "trace",
106    feature = "all-operators"
107))]
108#[doc = include_str!("../README.md")]
109struct ReadmeDoctests;
110
111mod arena;
112mod arena_ext;
113mod builder;
114mod check;
115mod compile;
116mod config;
117mod engine;
118mod error;
119mod eval_input;
120mod facts;
121mod logic_input;
122mod node;
123mod node_serialize;
124pub mod operator;
125mod operators;
126mod parsed_data;
127mod path;
128mod projection;
129mod result_output;
130mod roots;
131#[cfg(feature = "serde_json")]
132mod serde_bridge;
133mod session;
134mod top_level;
135#[cfg(feature = "trace")]
136mod trace;
137mod truthy_input;
138
139pub use arena::DataValue;
140pub use arena_ext::ArenaExt;
141pub use builder::EngineBuilder;
142/// The [`bumpalo`] arena allocator, re-exported.
143///
144/// `Engine::evaluate` and the [`CustomOperator`] trait both take a
145/// `&'a bumpalo::Bump` parameter, so callers need a way to construct
146/// arenas. Re-exporting locks the major version of `bumpalo` to whatever
147/// `datalogic-rs` itself depends on — pair with `use datalogic_rs::bumpalo`
148/// instead of an independent `bumpalo` dep to avoid major-version skew.
149pub use bumpalo;
150pub use check::{CheckMode, CompileError, Diagnostic, DiagnosticCode, Severity};
151pub use config::{
152    DivisionByZeroHandling, EvaluationConfig, MissingVar, NanHandling, NumericCoercionConfig,
153    TruthyEvaluator,
154};
155/// The `datavalue` crate, re-exported. `datalogic-rs` builds on `datavalue`'s
156/// owned and borrowed value types — accessing them through this module makes
157/// the dependency explicit at the use site.
158///
159/// # Working with `DataValue`
160///
161/// Evaluation returns [`DataValue`] (re-exported at the crate root and
162/// also reachable as `datalogic_rs::datavalue::DataValue`). It's an
163/// arena-allocated JSON-shaped value tree borrowed from a
164/// [`bumpalo::Bump`]. The accessors most callers reach for live in this
165/// re-exported crate:
166///
167/// - **Type predicates** — `.is_null()`, `.is_bool()`, `.is_number()`,
168///   `.is_string()`, `.is_array()`, `.is_object()`.
169/// - **Owned readers** — `.as_bool()`, `.as_i64()`, `.as_f64()`,
170///   `.as_str()`, `.as_array()`, `.as_object()`. Each returns
171///   `Option<…>`; the `None` case is "wrong variant," not a runtime error.
172/// - **Indexing** — `value["key"]` / `value[idx]` returns `&DataValue`
173///   (or the `Null` singleton on miss, matching `serde_json::Value`).
174///
175/// # Owned vs borrowed
176///
177/// [`DataValue<'a>`](datavalue::DataValue) borrows from a `Bump`;
178/// [`OwnedDataValue`](datavalue::OwnedDataValue) is the heap-owned
179/// counterpart. Use the owned form when you need to outlive the arena —
180/// caching a result, returning across an `await`, sending across a
181/// channel. Convert via `borrowed.to_owned()` and `owned.to_arena(&bump)`.
182///
183/// # Crossing the `serde_json` boundary
184///
185/// Conversions to / from `serde_json::Value` are gated behind the
186/// `serde_json` feature (kept off by default so the crate has zero
187/// external dependencies in the minimal build). With `serde_json`
188/// enabled, pass a `&serde_json::Value` into any `eval*` method via
189/// [`EvalInput`] / [`IntoLogic`] (a `T: Serialize` is converted first with
190/// `serde_json::to_value`; there is no blanket `&T: Serialize` impl), and
191/// ask for a `serde_json::Value` (or any `T: DeserializeOwned`) back via
192// `Engine::eval_into` / `Session::eval_into` are gated behind
193// `serde_json`; link them when the feature is on, otherwise emit them
194// as code text so default-features `cargo doc` doesn't break.
195#[cfg_attr(
196    feature = "serde_json",
197    doc = "[`Engine::eval_into`] / [`Session::eval_into`]. For the `DataValue"
198)]
199#[cfg_attr(
200    not(feature = "serde_json"),
201    doc = "`Engine::eval_into` / `Session::eval_into`. For the `DataValue"
202)]
203/// → JSON String` path use the standard `value.to_string()`, which is
204/// what [`Engine::eval_str`] uses internally.
205pub use datavalue;
206pub use engine::Engine;
207#[cfg(feature = "budget")]
208#[cfg_attr(docsrs, doc(cfg(feature = "budget")))]
209pub use engine::Metered;
210pub use error::{CustomErrorSource, Error, ErrorCode, ErrorKind, UnknownErrorCode};
211pub use eval_input::{EvalInput, OwnedInput};
212pub use facts::{DataPath, Facts};
213pub use logic_input::IntoLogic;
214pub use node::Logic;
215pub use operators::info::{OperatorInfo, ScopedArg};
216pub use operators::table::Family;
217pub use parsed_data::ParsedData;
218pub use path::PathStep;
219pub use result_output::FromDataValue;
220pub use roots::{RootValue, Roots};
221pub use session::{Session, SharedSession};
222#[cfg(feature = "serde_json")]
223#[cfg_attr(docsrs, doc(cfg(feature = "serde_json")))]
224pub use top_level::eval_into;
225pub use top_level::{compile, eval, eval_str};
226#[cfg(feature = "trace")]
227#[cfg_attr(docsrs, doc(cfg(feature = "trace")))]
228pub use trace::{ExecutionStep, ExpressionNode, TracedRun, TracedSession};
229pub use truthy_input::TruthyInput;
230
231// `CompiledNode`, `OpCode`, `MetadataHint`, `PathSegment`, `ReduceHint` were
232// public in 4.x. They are compile-internal in v5; consumers reach for them
233// via `crate::node::*` / `crate::OpCode` directly.
234pub(crate) use node::CompiledNode;
235
236/// Not public API. The operator table's full catalogue, every family
237/// whether compiled in or not, for the conformance runner and repository
238/// tooling to tell a gated-off operator from a misspelled one.
239#[doc(hidden)]
240pub mod __private {
241    pub use crate::operators::table::{CATALOGUE, CatalogueEntry};
242}
243pub(crate) use operators::table::OpCode;
244
245/// Result type for Engine operations
246pub type Result<T> = std::result::Result<T, Error>;
247
248/// Custom operator hook for the [`Engine`].
249///
250/// Implementations receive args **already evaluated** as borrowed
251/// [`DataValue`] references and return a `&'a DataValue<'a>` result
252/// allocated in the supplied [`bumpalo::Bump`] arena.
253///
254/// ## Lifetime
255///
256/// `'a` is the arena lifetime, tied to the [`bumpalo::Bump`] allocator
257/// that lives for the duration of one [`Engine::evaluate`] call. Args
258/// borrow from the caller's input and from prior arena allocations; the
259/// returned `&'a DataValue<'a>` must be allocated in the arena (or be a
260/// preallocated singleton) — never a stack reference.
261///
262/// ## Example
263///
264/// ```rust
265/// use datalogic_rs::{CustomOperator, DataValue, Engine, Result, operator::EvalContext};
266/// use bumpalo::Bump;
267///
268/// struct DoubleArena;
269/// impl CustomOperator for DoubleArena {
270///     fn evaluate<'a>(
271///         &self,
272///         args: &[&'a DataValue<'a>],
273///         _ctx: &mut EvalContext<'_, 'a>,
274///         arena: &'a Bump,
275///     ) -> Result<&'a DataValue<'a>> {
276///         let n = args.first().and_then(|v| v.as_f64()).unwrap_or(0.0);
277///         Ok(arena.alloc(DataValue::from_f64(n * 2.0)))
278///     }
279/// }
280///
281/// let engine = Engine::builder().add_operator("double", DoubleArena).build();
282///
283/// let result = engine.eval_str(r#"{"double": 21}"#, "null").unwrap();
284/// assert_eq!(result, "42");
285/// ```
286///
287/// ## Stability
288///
289/// This trait is the headline extension point of the crate and is
290/// intentionally not sealed. Within the **5.x series** the only changes
291/// that will be made to this trait are *default-method additions* — no
292/// new required methods, no signature changes to [`Self::evaluate`], no
293/// lifetime restructuring. Implementations written against 5.0 will
294/// compile against every 5.x release without modification. Any breaking
295/// change here requires a 6.0 bump.
296///
297/// The opaque types in the signature ([`crate::DataValue`],
298/// [`operator::EvalContext`], [`bumpalo::Bump`]) may evolve internally
299/// without breaking this contract, since their public surface is the
300/// stable boundary.
301pub trait CustomOperator: Send + Sync {
302    /// Evaluate this operator with arena-allocated args and result.
303    ///
304    /// # Arguments
305    ///
306    /// * `args` — pre-evaluated args as `&'a DataValue<'a>`. The arena
307    ///   dispatcher has already recursed into each arg's expression tree.
308    /// * `ctx` — opaque view into the engine's evaluation context. Most
309    ///   operators ignore this; it exposes [`operator::EvalContext::root_input`]
310    ///   and [`operator::EvalContext::depth`] for the rare case where an
311    ///   operator's behaviour depends on the surrounding context.
312    /// * `arena` — the [`bumpalo::Bump`] allocator. Use `arena.alloc(...)`
313    ///   for arena values, `arena.alloc_str(...)` for strings. For the
314    ///   common case of returning a typed `DataValue` result, prefer the
315    ///   one-call helpers on [`ArenaExt`] (`arena.f64(n)`,
316    ///   `arena.string(s)`, `arena.bool(b)`, …) — they are zero-cost
317    ///   over the manual form and short-circuit to preallocated
318    ///   singletons for `null`, booleans, small ints, and empty
319    ///   string/array/object.
320    fn evaluate<'a>(
321        &self,
322        args: &[&'a DataValue<'a>],
323        ctx: &mut operator::EvalContext<'_, 'a>,
324        arena: &'a bumpalo::Bump,
325    ) -> Result<&'a DataValue<'a>>;
326
327    /// What the engine may assume about this operator. The default,
328    /// [`CustomOperatorInfo::opaque`], assumes nothing: the operator may
329    /// return a different result each call and read the whole context.
330    ///
331    /// Declaring more lets the engine do more:
332    ///
333    /// - [`deterministic`](CustomOperatorInfo::deterministic) without
334    ///   [`reads_context`](CustomOperatorInfo::reads_context): a call
335    ///   whose arguments are all constant is evaluated once, when the rule
336    ///   is compiled, and replaced by its result.
337    /// - [`Logic::facts`] trusts the declaration:
338    ///   [`Facts::is_deterministic`] holds for a deterministic operator,
339    ///   and [`Facts::reads_complete`] for one that does not read the
340    ///   context.
341    /// - A declared argument count is checked before any argument is
342    ///   evaluated; a call outside it fails with `InvalidArguments`.
343    ///
344    /// The engine reads this when it compiles a call, so it should not
345    /// change over the operator's lifetime. A wrong declaration gives wrong
346    /// results: an operator that declares itself deterministic but is not
347    /// is folded to whatever it returned at compile time.
348    fn info(&self) -> CustomOperatorInfo {
349        CustomOperatorInfo::opaque()
350    }
351
352    /// Validate a call's arguments as written, before anything runs.
353    /// [`Engine::check`] and [`Engine::compile_checked`] call it for every
354    /// call to this operator whose argument count fits
355    /// [`info`](Self::info); [`Engine::compile`] does not.
356    ///
357    /// `args` is the call's argument list as JSON: `{"op": [a, b]}` gives
358    /// `[a, b]`, and a lone argument that is not an array (`{"op": a}`)
359    /// gives `[a]`. An argument that is an expression arrives as written,
360    /// so an operator call is a one-key object (`{"var": "x"}`) and only a
361    /// literal can be checked as a value.
362    ///
363    /// Return [`Diagnostic::error`] for a call that will fail (it stops
364    /// [`Engine::compile_checked`]) or [`Diagnostic::warning`] for one that
365    /// runs but probably not as meant, narrowed with
366    /// [`Diagnostic::at_argument`]. The checker fills in the location and
367    /// the operator name. The default accepts every call.
368    ///
369    /// ```rust
370    /// use datalogic_rs::datavalue::OwnedDataValue;
371    /// use datalogic_rs::{CheckMode, CustomOperator, DataValue, Diagnostic, Engine, Result};
372    /// use datalogic_rs::operator::EvalContext;
373    ///
374    /// struct Table;
375    /// impl CustomOperator for Table {
376    ///     fn evaluate<'a>(
377    ///         &self,
378    ///         args: &[&'a DataValue<'a>],
379    ///         _ctx: &mut EvalContext<'_, 'a>,
380    ///         _arena: &'a datalogic_rs::bumpalo::Bump,
381    ///     ) -> Result<&'a DataValue<'a>> {
382    ///         Ok(args[0])
383    ///     }
384    ///
385    ///     fn check(&self, args: &[OwnedDataValue]) -> std::result::Result<(), Diagnostic> {
386    ///         match args.first() {
387    ///             Some(OwnedDataValue::String(name)) if name != "users" => {
388    ///                 Err(Diagnostic::error(format!("no table {name:?}")).at_argument(0))
389    ///             }
390    ///             _ => Ok(()),
391    ///         }
392    ///     }
393    /// }
394    ///
395    /// let engine = Engine::builder().add_operator("table", Table).build();
396    /// let diags = engine.check(r#"{"table": ["orders"]}"#, CheckMode::Engine);
397    /// assert_eq!(diags[0].pointer, "/table/0");
398    /// ```
399    fn check(&self, args: &[datavalue::OwnedDataValue]) -> std::result::Result<(), Diagnostic> {
400        let _ = args;
401        Ok(())
402    }
403}
404
405/// What the engine may assume about a [`CustomOperator`], returned by
406/// [`CustomOperator::info`].
407///
408/// Build one from [`Self::opaque`] or [`Self::pure`] and the `with` / `-ing`
409/// methods; the struct is `#[non_exhaustive]` so fields can be added in 5.x.
410///
411/// ```rust
412/// use datalogic_rs::CustomOperatorInfo;
413///
414/// // Depends only on its one or two arguments.
415/// let info = CustomOperatorInfo::pure().with_args(1, Some(2));
416/// assert!(info.deterministic && !info.reads_context);
417/// ```
418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
419#[non_exhaustive]
420pub struct CustomOperatorInfo {
421    /// The same arguments (and, if [`Self::reads_context`], the same data)
422    /// always give the same result, and evaluating it has no effect the
423    /// rule could observe.
424    pub deterministic: bool,
425    /// Reads the data context through
426    /// [`EvalContext`](operator::EvalContext), beyond its arguments.
427    pub reads_context: bool,
428    /// The fewest arguments a call may pass.
429    pub min_args: usize,
430    /// The most arguments a call may pass, or `None` for no limit.
431    pub max_args: Option<usize>,
432}
433
434impl CustomOperatorInfo {
435    /// Whether a call may pass `count` arguments.
436    #[inline]
437    pub(crate) fn accepts(&self, count: usize) -> bool {
438        count >= self.min_args && self.max_args.is_none_or(|max| count <= max)
439    }
440
441    /// Nothing assumed: nondeterministic, may read the context, any
442    /// number of arguments. The default.
443    pub const fn opaque() -> Self {
444        CustomOperatorInfo {
445            deterministic: false,
446            reads_context: true,
447            min_args: 0,
448            max_args: None,
449        }
450    }
451
452    /// A function of its arguments alone: deterministic, reads no
453    /// context, any number of arguments.
454    pub const fn pure() -> Self {
455        CustomOperatorInfo {
456            deterministic: true,
457            reads_context: false,
458            min_args: 0,
459            max_args: None,
460        }
461    }
462
463    /// The same, but also reading the data context.
464    pub const fn reading_context(self) -> Self {
465        CustomOperatorInfo {
466            reads_context: true,
467            ..self
468        }
469    }
470
471    /// The same, accepting between `min` and `max` arguments (`None`: no
472    /// upper limit).
473    pub const fn with_args(self, min: usize, max: Option<usize>) -> Self {
474        CustomOperatorInfo {
475            min_args: min,
476            max_args: max,
477            ..self
478        }
479    }
480}
481
482impl Default for CustomOperatorInfo {
483    fn default() -> Self {
484        Self::opaque()
485    }
486}
487
488// `Box<dyn CustomOperator>` itself implements `CustomOperator` by
489// delegating to the inner trait object. This collapses what used to be
490// two separate registration methods on `EngineBuilder` (`add_operator`
491// for typed operators, `add_operator_box` for pre-boxed trait objects)
492// into a single entry point: `EngineBuilder::add_operator(name, op)`
493// accepts either a typed `T: CustomOperator + 'static` or a
494// `Box<dyn CustomOperator>` produced by a runtime registry.
495impl CustomOperator for Box<dyn CustomOperator> {
496    #[inline]
497    fn evaluate<'a>(
498        &self,
499        args: &[&'a DataValue<'a>],
500        ctx: &mut operator::EvalContext<'_, 'a>,
501        arena: &'a bumpalo::Bump,
502    ) -> Result<&'a DataValue<'a>> {
503        (**self).evaluate(args, ctx, arena)
504    }
505
506    #[inline]
507    fn info(&self) -> CustomOperatorInfo {
508        (**self).info()
509    }
510
511    #[inline]
512    fn check(&self, args: &[datavalue::OwnedDataValue]) -> std::result::Result<(), Diagnostic> {
513        (**self).check(args)
514    }
515}
516
517// `Arc<T>` delegates the same way, so one operator instance (and any state
518// it holds) can be registered on several engines: a host that rebuilds its
519// engine on hot reload keeps its operators in a registry of
520// `Arc<dyn CustomOperator>` and hands each new builder a clone, with no
521// wrapper type of its own.
522impl<T: CustomOperator + ?Sized> CustomOperator for std::sync::Arc<T> {
523    #[inline]
524    fn evaluate<'a>(
525        &self,
526        args: &[&'a DataValue<'a>],
527        ctx: &mut operator::EvalContext<'_, 'a>,
528        arena: &'a bumpalo::Bump,
529    ) -> Result<&'a DataValue<'a>> {
530        (**self).evaluate(args, ctx, arena)
531    }
532
533    #[inline]
534    fn info(&self) -> CustomOperatorInfo {
535        (**self).info()
536    }
537
538    #[inline]
539    fn check(&self, args: &[datavalue::OwnedDataValue]) -> std::result::Result<(), Diagnostic> {
540        (**self).check(args)
541    }
542}