datalogic-rs 5.9.0

High-performance JSONLogic (json-logic) rules engine and sandboxed expression evaluator in Rust — one core, official bindings for Node.js, WASM, Python, Go, Java, .NET, and PHP. Compile once, evaluate in nanoseconds.
Documentation
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
//! Reusable evaluation handle that owns its arena.
//!
//! [`Session`] owns a [`bumpalo::Bump`] and exposes [`Session::reset`] for the
//! caller to bound peak memory between calls. The session itself never resets
//! the arena — every `evaluate*` method appends to the bump and the caller
//! decides when to release that memory back to the start-of-chunk position.
//! Inputs go through [`crate::EvalInput`] so callers pass whatever they have
//! on hand (`&str`, `&OwnedDataValue`, `&serde_json::Value`, …); outputs are
//! either owned ([`OwnedDataValue`] / `String` / `serde_json::Value`) or
//! borrowed from the arena ([`Self::eval_borrowed`]) — borrowed results are
//! invalidated by the next `&mut self` call (Rust's borrow checker enforces).
//!
//! For a one-shot evaluation that owns and drops its arena per call, use
//! [`crate::Engine::eval_str`] (convenience). For full caller control of
//! the arena lifecycle, use [`crate::Engine::evaluate`] directly with a
//! caller-passed `&Bump`.
//!
//! # What each input adds to the arena
//!
//! Every call brings its data into the arena before evaluating, and that
//! stays until [`Session::reset`]. What it costs depends on the input:
//!
//! | Input | Arena cost per call |
//! |-------|---------------------|
//! | `&ParsedData` | nothing: the tree already lives in the handle |
//! | `&DataValue` | nothing |
//! | `&OwnedDataValue` | the array and object spines; strings, keys and tensor bytes are borrowed |
//! | `&serde_json::Value` | the array and object spines; strings and keys are borrowed |
//! | `&str` | a parse tree; unescaped strings are borrowed from the text |
//!
//! For a large context evaluated many times, build a
//! [`crate::ParsedData`] once (`from_json`, `from_value` or `from_owned`)
//! and pass the handle: each call then costs only what the rule does.

use std::marker::PhantomData;
use std::ops::Deref;
use std::sync::Arc;

use bumpalo::Bump;
use datavalue::OwnedDataValue;

use crate::arena::DataValue;
use crate::{Engine, EvalInput, Logic, Result};

/// Reusable evaluation handle. Construct via [`Engine::session`].
///
/// Owns a [`bumpalo::Bump`]; the caller controls reset via [`Self::reset`].
/// Subsequent `evaluate*` calls append to the bump until the caller resets
/// or the session is dropped — letting the caller amortise reset cost across
/// logical batches and avoid resetting between calls that don't need it.
///
/// # Example
///
/// ```rust
/// use datalogic_rs::Engine;
///
/// let engine = Engine::new();
/// let compiled = engine.compile(r#"{"+": [{"var": "x"}, 1]}"#).unwrap();
/// let mut session = engine.session();
///
/// for x in 0..3 {
///     let payload = format!(r#"{{"x": {}}}"#, x);
///     let result = session.eval_str(&compiled, &payload).unwrap();
///     assert_eq!(result, (x + 1).to_string());
///     // Reset between iterations to keep peak memory bounded by the
///     // largest single evaluation rather than the cumulative loop.
///     session.reset();
/// }
/// ```
///
/// # Holding the engine
///
/// `E` is how the session holds its engine. [`Engine::session`] borrows it
/// (`Session<'_>`, the default). A [`SharedSession`] holds an
/// `Arc<Engine>` instead, so it is `'static + Send`: it can be stored in a
/// struct, moved to another thread or held across an `.await` without
/// tying it to the engine's lifetime. Both have the same methods.
pub struct Session<'engine, E = &'engine Engine> {
    engine: E,
    arena: Bump,
    _engine: PhantomData<&'engine Engine>,
}

/// A [`Session`] that holds its engine by `Arc`: `'static + Send`, with no
/// lifetime to thread through the caller's types. Build one with
/// [`SharedSession::new`] or `SharedSession::from(arc)`.
///
/// ```rust
/// use std::sync::Arc;
/// use datalogic_rs::{Engine, SharedSession};
///
/// let engine = Arc::new(Engine::new());
/// let logic = engine.compile(r#"{"var": "x"}"#).unwrap();
/// let mut session = SharedSession::new(engine);
/// let handle = std::thread::spawn(move || session.eval_str(&logic, r#"{"x": 1}"#).unwrap());
/// assert_eq!(handle.join().unwrap(), "1");
/// ```
pub type SharedSession = Session<'static, Arc<Engine>>;

impl SharedSession {
    /// A session over a shared engine. The session keeps the engine alive.
    pub fn new(engine: Arc<Engine>) -> Self {
        Session::with_engine(engine)
    }
}

impl From<Arc<Engine>> for SharedSession {
    fn from(engine: Arc<Engine>) -> Self {
        SharedSession::new(engine)
    }
}

impl<E: Deref<Target = Engine>> std::fmt::Debug for Session<'_, E> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // Print the engine handle plus the arena's currently-allocated byte
        // count — useful for tracking high-water marks across resets without
        // dumping every chunk's raw bytes.
        f.debug_struct("Session")
            .field("engine", &*self.engine)
            .field("arena_allocated_bytes", &self.arena.allocated_bytes())
            .finish_non_exhaustive()
    }
}

impl<'engine> Session<'engine> {
    #[inline]
    pub(crate) fn borrowing(engine: &'engine Engine) -> Self {
        Session::with_engine(engine)
    }
}

impl<'engine, E: Deref<Target = Engine>> Session<'engine, E> {
    #[inline]
    fn with_engine(engine: E) -> Self {
        Self {
            engine,
            arena: Bump::new(),
            _engine: PhantomData,
        }
    }

    /// The engine this session evaluates with.
    #[inline]
    pub fn engine(&self) -> &Engine {
        &self.engine
    }

    /// Reset the session's arena, returning every allocated chunk to the
    /// free list's start-of-chunk position without freeing OS memory.
    ///
    /// Call this between logical batches to bound peak memory. After reset,
    /// any borrowed reference previously returned by [`Self::eval_borrowed`]
    /// is invalidated — the borrow checker enforces this for the common case
    /// (the result borrow ends with the previous `&mut self` borrow).
    ///
    /// `Bump::reset` is constant-time (resets a few pointers); the freed
    /// chunks remain allocated and serve subsequent calls without re-asking
    /// the OS for memory.
    #[inline]
    pub fn reset(&mut self) {
        self.arena.reset();
    }

    /// Drop the session's arena and replace it with a fresh one whose
    /// initial chunk holds at least `capacity` bytes.
    ///
    /// Use this when you know the steady-state high-water mark of your
    /// workload (e.g. captured via [`Self::allocated_bytes`] after a
    /// warm-up pass) and want subsequent calls to run on a single
    /// pre-sized chunk — no chunk-growth events during the timed window.
    ///
    /// Unlike [`Self::reset`], which keeps the existing chunks and only
    /// rewinds the bump pointer, this drops the chunks entirely and
    /// allocates one new chunk of the requested capacity. Any reference
    /// previously returned by [`Self::eval_borrowed`] is invalidated; the
    /// `&mut self` signature lets the borrow checker enforce this.
    pub fn reset_with_capacity(&mut self, capacity: usize) {
        self.arena = Bump::with_capacity(capacity);
    }

    /// Total bytes currently occupied by the session's arena chunks.
    ///
    /// Useful for capturing a workload's steady-state high-water mark
    /// after a warm-up pass — feed the returned value into
    /// [`Self::reset_with_capacity`] to pre-size the arena before a timed
    /// loop. Stable across [`Self::reset`] calls (chunks aren't freed);
    /// drops to the new chunk size after [`Self::reset_with_capacity`].
    ///
    /// Forwards to [`bumpalo::Bump::allocated_bytes`].
    #[inline]
    pub fn allocated_bytes(&self) -> usize {
        self.arena.allocated_bytes()
    }

    /// Evaluate `compiled` against `data` and deep-clone the result into
    /// an [`OwnedDataValue`] that survives subsequent calls and resets.
    ///
    /// The intermediate arena allocations stay in the session's bump
    /// until the caller invokes [`Self::reset`]. For long-running loops,
    /// call `reset` between iterations to keep peak memory bounded.
    ///
    /// # Example
    ///
    /// ```rust
    /// use datalogic_rs::Engine;
    ///
    /// let engine = Engine::new();
    /// let compiled = engine.compile(r#"{"==": [{"var": "x"}, 1]}"#).unwrap();
    /// let mut session = engine.session();
    /// let result = session.eval(&compiled, r#"{"x": 1}"#).unwrap();
    /// assert_eq!(result.as_bool(), Some(true));
    /// ```
    pub fn eval<'a, D>(&'a mut self, compiled: &Logic, data: D) -> Result<OwnedDataValue>
    where
        D: EvalInput<'a>,
    {
        self.eval_as(compiled, data)
    }

    /// Evaluate and convert the result straight into `R`, any
    /// [`crate::FromDataValue`] output: [`OwnedDataValue`], a JSON
    /// [`String`], or a `serde_json::Value` (with `serde_json`). The shared
    /// body of [`Self::eval`] / [`Self::eval_str`] / `eval_into`.
    ///
    /// For a `serde_json::Value` result this is one conversion, where
    /// `eval_into::<serde_json::Value>` builds the value and then
    /// deserializes it into a second one.
    ///
    /// ```rust
    /// # #[cfg(feature = "serde_json")] {
    /// use datalogic_rs::Engine;
    ///
    /// let engine = Engine::new();
    /// let compiled = engine.compile(r#"{"var": "user"}"#).unwrap();
    /// let mut session = engine.session();
    /// let user: serde_json::Value = session
    ///     .eval_as(&compiled, r#"{"user": {"id": 7}}"#)
    ///     .unwrap();
    /// assert_eq!(user["id"], 7);
    /// # }
    /// ```
    pub fn eval_as<'a, R, D>(&'a mut self, compiled: &Logic, data: D) -> Result<R>
    where
        R: crate::FromDataValue,
        D: EvalInput<'a>,
    {
        let arena: &'a Bump = &self.arena;
        // Bring the input in against the rule as this engine runs it, so
        // a projection matches the tree `evaluate` dispatches.
        let compiled = compiled.for_engine(&self.engine);
        let av = data.into_arena_for(compiled, &self.engine, arena)?;
        R::from_arena(self.engine.evaluate(compiled, av, arena)?)
    }

    /// JSON-string convenience: evaluate against `data` and serialise
    /// the result back to a JSON [`String`]. Reuses the arena across
    /// calls; does not reset — see [`Self::reset`].
    pub fn eval_str<'a, D>(&'a mut self, compiled: &Logic, data: D) -> Result<String>
    where
        D: EvalInput<'a>,
    {
        self.eval_as(compiled, data)
    }

    /// Typed convenience: evaluate and deserialise the result into
    /// `T: DeserializeOwned`. Use `T = serde_json::Value` for a JSON
    /// `Value` result; use a typed struct for direct mapping. Internally
    /// routes through `serde_json`.
    #[cfg(feature = "serde_json")]
    #[cfg_attr(docsrs, doc(cfg(feature = "serde_json")))]
    pub fn eval_into<'a, T, D>(&'a mut self, compiled: &Logic, data: D) -> Result<T>
    where
        T: serde::de::DeserializeOwned,
        D: EvalInput<'a>,
    {
        let value: serde_json::Value = self.eval_as(compiled, data)?;
        serde_json::from_value(value).map_err(crate::Error::from)
    }

    /// Evaluate under an explicit operation budget, reporting what the
    /// evaluation spent.
    ///
    /// Session-arena counterpart to [`Engine::evaluate_metered`], which
    /// documents what one operation is. The value is deep-cloned out like
    /// [`Self::eval`]'s, so the count survives the next `reset`.
    ///
    /// # Example
    ///
    /// ```rust
    /// # #[cfg(feature = "budget")] {
    /// use datalogic_rs::{Engine, Metered};
    ///
    /// let engine = Engine::new();
    /// let compiled = engine.compile(r#"{"filter": [{"var": "xs"}, {">": [{"var": ""}, 1]}]}"#).unwrap();
    /// let mut session = engine.session();
    ///
    /// let Metered { value, ops } = session
    ///     .eval_metered(&compiled, r#"{"xs": [1, 2, 3]}"#, u64::MAX)
    ///     .unwrap();
    /// assert_eq!(value.to_json_string(), "[2,3]");
    /// assert!(ops >= 3, "at least one operation per item filtered");
    /// # }
    /// ```
    ///
    /// # Errors
    ///
    /// [`ErrorKind::BudgetExceeded`](crate::ErrorKind::BudgetExceeded)
    /// when the rule charges past `budget`, plus every error
    /// [`Self::eval`] can return. To observe the operations spent even
    /// when evaluation fails, call [`Self::eval_metered_report`].
    #[cfg(feature = "budget")]
    #[cfg_attr(docsrs, doc(cfg(feature = "budget")))]
    pub fn eval_metered<'a, D>(
        &'a mut self,
        compiled: &Logic,
        data: D,
        budget: u64,
    ) -> Result<crate::Metered<OwnedDataValue>>
    where
        D: EvalInput<'a>,
    {
        self.eval_metered_report(compiled, data, budget).transpose()
    }

    /// Failure-inclusive counterpart to [`Self::eval_metered`]: evaluates
    /// under `budget` and returns a [`crate::Metered`] report whose `ops`
    /// is populated whether evaluation succeeds (`value: Ok(...)`) or
    /// fails (`value: Err(...)`).
    ///
    /// Session-arena counterpart to [`Engine::evaluate_metered_report`],
    /// which documents the failure charge semantics (ordinary runtime
    /// errors, `BudgetExceeded`, and pre-dispatch input adaptation errors).
    ///
    /// # Example
    ///
    /// ```rust
    /// # #[cfg(feature = "budget")] {
    /// use datalogic_rs::{Engine, ErrorCode, Metered};
    ///
    /// let engine = Engine::new();
    /// let compiled = engine.compile(r#"{"/": [1, {"var": "zero"}]}"#).unwrap();
    /// let mut session = engine.session();
    ///
    /// let Metered { value, ops } = session
    ///     .eval_metered_report(&compiled, r#"{"zero": 0}"#, 100);
    /// let err = value.unwrap_err();
    /// assert_eq!(err.code(), ErrorCode::Thrown);
    /// assert_eq!(err.operator(), Some("/"));
    /// assert_eq!(ops, 2);
    /// # }
    /// ```
    #[cfg(feature = "budget")]
    #[cfg_attr(docsrs, doc(cfg(feature = "budget")))]
    pub fn eval_metered_report<'a, D>(
        &'a mut self,
        compiled: &Logic,
        data: D,
        budget: u64,
    ) -> crate::Metered<Result<OwnedDataValue>>
    where
        D: EvalInput<'a>,
    {
        let arena: &'a Bump = &self.arena;
        let compiled = compiled.for_engine(&self.engine);
        let av = match data.into_arena_for(compiled, &self.engine, arena) {
            Ok(av) => av,
            Err(e) => {
                return crate::Metered {
                    value: Err(e),
                    ops: 0,
                };
            }
        };
        let metered = self
            .engine
            .evaluate_metered_report(compiled, av, arena, budget);
        crate::Metered {
            value: metered.value.and_then(crate::FromDataValue::from_arena),
            ops: metered.ops,
        }
    }

    /// Evaluate and return a borrowed result tied to this session's
    /// arena. Same semantics as [`Self::eval`] but skips the deep-clone
    /// — the returned reference is invalidated by the next `&mut self`
    /// call (the borrow checker enforces). Use this when the result is
    /// consumed before the next session call; for cross-call retention
    /// use [`Self::eval`].
    ///
    /// Symmetric with [`Engine::evaluate`] (caller-managed bump,
    /// borrowed result) but with the bump owned by the session. Does
    /// not reset the arena — call [`Self::reset`] explicitly.
    ///
    /// # Example
    ///
    /// ```rust
    /// use datalogic_rs::Engine;
    ///
    /// let engine = Engine::new();
    /// let compiled = engine.compile(r#"{"+": [{"var": "x"}, 1]}"#).unwrap();
    /// let mut session = engine.session();
    /// let result = session.eval_borrowed(&compiled, r#"{"x": 5}"#).unwrap();
    /// assert_eq!(result.as_i64(), Some(6));
    /// ```
    pub fn eval_borrowed<'a, D>(
        &'a mut self,
        compiled: &'a Logic,
        data: D,
    ) -> Result<&'a DataValue<'a>>
    where
        D: EvalInput<'a>,
    {
        let arena: &'a Bump = &self.arena;
        self.engine.evaluate(compiled, data, arena)
    }
}