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}