datalogic-rs 5.5.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
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
# datalogic-rs

[![Crates.io](https://img.shields.io/crates/v/datalogic-rs.svg)](https://crates.io/crates/datalogic-rs)
[![Documentation](https://docs.rs/datalogic-rs/badge.svg)](https://docs.rs/datalogic-rs)
[![Rust 1.85+](https://img.shields.io/badge/rust-1.85+-orange.svg)](https://www.rust-lang.org)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)

A fast, type-safe Rust implementation of [JSONLogic](http://jsonlogic.com)
for evaluating logical rules as JSON. Compile a rule once, evaluate it
millions of times across threads with zero overhead — same engine powers
a **rule engine**, a **JSON template engine**, or a **safe expression
evaluator**.

This is the **Rust core** of the
[`datalogic-rs` monorepo](https://github.com/GoPlasmatic/datalogic-rs).
The repo also ships official bindings for Node.js, the browser
([WASM](https://www.npmjs.com/package/@goplasmatic/datalogic-wasm)),
[Python](https://pypi.org/project/datalogic-py/), Go, Java, .NET, and PHP,
plus a [React visual debugger](https://www.npmjs.com/package/@goplasmatic/datalogic-ui):
same rules, same semantics. For the cross-runtime overview
and the per-binding READMEs, see the
[repo README](https://github.com/GoPlasmatic/datalogic-rs#readme).

## Install

```bash
cargo add datalogic-rs
```

The default build is `serde_json`-free and ships only the JSONLogic
baseline operators. Opt in to feature flags as needed — see the
[feature flag reference](#feature-flags) below.

## Hello, JSONLogic

```rust
let result = datalogic_rs::eval_str(r#"{"+": [1, 2, 3]}"#, r#"{}"#).unwrap();
assert_eq!(result, "6");
```

That's it. `eval_str` parses the rule, parses the data, evaluates, and
hands you back a JSON string. The free functions on the crate root
wrap a shared default `Engine` — explicit construction lets you add
custom operators, change config, or amortise compilation. The rest of
this README walks through **when to use which API**.

## Choosing your API: five tiers, one engine

The crate exposes five evaluation tiers in increasing order of control.
Pick by use case, not by curiosity — most callers want **Tier 0** for
ad-hoc work or **Tier 2** for repeated evaluation.

| Tier | Entry point                                              | Arena owner             | Returns                              | Use when                                              |
|------|----------------------------------------------------------|-------------------------|--------------------------------------|-------------------------------------------------------|
| **0** | `datalogic_rs::eval_str` / `eval` / `eval_into` / `compile` | lazy static `Engine`   | `String` / `OwnedDataValue` / `T` / `Logic` | One-shot scripts, ad-hoc evaluation, no custom config |
| **1** | `Engine::eval_str` / `eval` / `eval_into`                 | per-call `Bump`         | `String` / `OwnedDataValue` / `T`    | You need custom operators, config, or templating mode |
| **2** | `Engine::session()``Session::eval*`                    | session-owned `Bump`    | owned **or** `&DataValue<'a>` borrow | Hot loops, services, batch jobs                       |
| **3** | `Engine::evaluate(&Logic, data, &Bump)`                   | caller-owned `Bump`     | `&'a DataValue<'a>`                  | Zero-copy result pipelines, custom pool strategies    |
| **4** | `Engine::trace()``TracedSession::*`                    | session-owned + buffer  | `TracedRun<R>` (result + steps)      | Debugging, visualisation, instrumentation             |

### Tier 0 — Module-level one-shot

The free functions wrap a static default `Engine`. No construction,
no configuration. Three result shapes:

```rust
use datalogic_rs::{compile, eval, eval_str};

// JSON string in, JSON string out
let s = eval_str(r#"{">": [{"var": "x"}, 10]}"#, r#"{"x": 42}"#).unwrap();
assert_eq!(s, "true");

// JSON string in, OwnedDataValue out
let v = eval(r#"{"+": [1, 2]}"#, r#"{}"#).unwrap();
assert_eq!(v.as_i64(), Some(3));

// Compile once at the module level when the rule is fixed
let logic = compile(r#"{"==": [{"var": "status"}, "active"]}"#).unwrap();
```

With the `serde_json` feature, `eval_into::<T>` returns any
`T: DeserializeOwned`:

```rust
// Cargo.toml: datalogic-rs = { version = "5", features = ["serde_json"] }
let n: i64 = datalogic_rs::eval_into(r#"{"+": [1, 2, 3]}"#, r#"{}"#).unwrap();
assert_eq!(n, 6);
```

### Tier 1 — Engine one-shot

Construct an `Engine` when you need anything beyond defaults: custom
operators, a non-default `EvaluationConfig`, or templating mode.

```rust
use datalogic_rs::Engine;

let engine = Engine::new();
let result = engine.eval_str(
    r#"{"==": [{"var": "status"}, "active"]}"#,
    r#"{"status": "active"}"#,
).unwrap();
assert_eq!(result, "true");
```

Use `Engine::builder()` to register operators and tweak behaviour:

```rust
use datalogic_rs::{Engine, EvaluationConfig};

let engine = Engine::builder()
    .with_config(EvaluationConfig::safe_arithmetic())
    .with_templating(true)              // requires `templating` feature
    .with_template_key_escape('$')      // optional: emit operator-named keys
    .build();
```

### Tier 2 — Session (the right default for repeated evaluation)

`Session` owns a reusable `bumpalo::Bump`; call `reset()` between
iterations so peak memory tracks the largest single evaluation, not
the sum. The session never resets on its own.

```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..1_000 {
    let payload = format!(r#"{{"x": {x}}}"#);
    let result = session.eval_str(&compiled, &payload).unwrap();
    // ...consume `result`...
    session.reset();          // O(1); keeps chunks for the next iteration
}
```

`Session::eval_borrowed` returns a `&'a DataValue<'a>` borrow into the
session's own arena — skips the owned deep-clone when the result is
consumed before the next session call. For pre-sizing the arena after
a warm-up pass, use `session.allocated_bytes()` +
`session.reset_with_capacity(bytes)`.

**Tokio idiom:** `Arc<Engine>` shared across worker threads (it's
`Send + Sync`), one `Session` per task (it's `Send` but `!Sync`, moves
with the task across `.await` points).

Full pattern: [`examples/compile_once_evaluate_many.rs`](https://github.com/GoPlasmatic/datalogic-rs/blob/main/crates/datalogic-rs/examples/compile_once_evaluate_many.rs).

### Tier 3 — Zero-copy `evaluate(&Bump)`

When the result borrow can stay scoped to a caller-managed arena,
skip the owned deep-clone and use `Engine::evaluate` directly. The
caller owns the `Bump`; the library never resets it.

```rust
use bumpalo::Bump;
use datalogic_rs::Engine;

let engine = Engine::new();
let compiled = engine.compile(r#"{"==": [{"var": "status"}, "active"]}"#).unwrap();

let arena = Bump::new();
let result = engine.evaluate(&compiled, r#"{"status": "active"}"#, &arena).unwrap();
assert_eq!(result.as_bool(), Some(true));
```

Reach for this tier when you have a pool-managed arena, are
pipelining values across stages without crossing the value boundary,
or want maximum control over when memory is reclaimed.

### Tier 4 — Traced evaluation (`trace` feature)

Enable the `trace` feature, then ask the engine for a `TracedSession`.
Each call records the expression tree + per-node execution steps.

```rust
// Cargo.toml: datalogic-rs = { version = "5", features = ["trace"] }
use datalogic_rs::Engine;

let engine = Engine::new();
let traced = engine.trace().eval_str(
    r#"{"and": [true, {"var": "x"}]}"#,
    r#"{"x": true}"#,
);
let result_string = traced.result.unwrap();
// `traced.steps` is the per-node execution trace
// (drop into the React debugger or process programmatically).
```

Full pattern: [`examples/tracing.rs`](https://github.com/GoPlasmatic/datalogic-rs/blob/main/crates/datalogic-rs/examples/tracing.rs).

## Input shapes

`Engine::evaluate` and `Session::eval_borrowed` accept any input the
caller is likely to have on hand, via the sealed [`EvalInput`] trait.
Per-call cost differs:

| Shape                                     | Cost per call                       |
|-------------------------------------------|-------------------------------------|
| `&str` (JSON literal)                     | parse + arena alloc                 |
| `&serde_json::Value` (`serde_json` feature) | deep-convert into the arena       |
| `&OwnedDataValue`                         | deep-borrow into the arena          |
| `DataValue<'a>` (by value)                | one arena alloc for the top node    |
| `&'a DataValue<'a>` (by reference)        | **zero** — pass-through             |
| `&ParsedData` (parse once via `ParsedData::from_json`) | **zero**: same as the pre-parsed row |

For the same-input-many-rules case, or when upstream stages already
produced an arena value, prefer the `&'a DataValue<'a>` path — it's
genuinely allocation-free. `ParsedData` is the owned form of that
pattern: it parses a JSON payload once into its own arena and hands out
`&DataValue` borrows for as long as the handle lives, which is what the
bindings expose as their `DataHandle` tier.

The Tier 0 / Tier 1 one-shot methods (`eval`, `eval_str`,
`eval_into`) accept a similar set via the [`OwnedInput`] trait, which
omits the `DataValue<'a>` shapes (no caller arena to borrow from).

Runnable example: [`examples/zero_copy_input.rs`](https://github.com/GoPlasmatic/datalogic-rs/blob/main/crates/datalogic-rs/examples/zero_copy_input.rs).

[`EvalInput`]: https://docs.rs/datalogic-rs/latest/datalogic_rs/trait.EvalInput.html
[`OwnedInput`]: https://docs.rs/datalogic-rs/latest/datalogic_rs/trait.OwnedInput.html

## Working with `DataValue`

Evaluation returns `&'a DataValue<'a>` — an arena-allocated, borrowed
JSON-shaped value tree. The type lives in the sibling `datavalue`
crate (re-exported at the root and as `datalogic_rs::datavalue`).
Most callers only need a handful of accessors:

```rust
use datalogic_rs::Engine;

let engine = Engine::new();
let compiled = engine.compile(r#"{"var": "user.score"}"#).unwrap();
let mut session = engine.session();
let result = session.eval_borrowed(&compiled, r#"{"user": {"score": 42}}"#).unwrap();

assert_eq!(result.as_i64(), Some(42));
// Other accessors: .as_f64(), .as_str(), .as_bool(), .as_array(), .as_object().
```

Conversion to other shapes:

- **To a JSON string:** `value.to_string()``DataValue` and
  `OwnedDataValue` both implement `Display`.
- **To `serde_json::Value`** (requires `serde_json`): use
  `eval_into::<serde_json::Value>(...)`.
- **To a typed Rust struct** (requires `serde_json`): use
  `eval_into::<T>(...)` where `T: DeserializeOwned`.
- **Owned vs borrowed:** `DataValue<'a>` borrows from a `Bump`;
  `OwnedDataValue` is the heap-owned counterpart for crossing arena
  lifetimes. Convert via `.to_owned()` (borrowed → owned) and
  `.to_arena(&bump)` (owned → borrowed).

## Public types at a glance

| Type                           | Role                                                        |
|--------------------------------|-------------------------------------------------------------|
| [`Engine`]                     | Immutable evaluation engine; entry point for every tier     |
| [`EngineBuilder`]              | Builder for engines with custom config, operators, modes    |
| [`Logic`]                      | Compiled, thread-safe rule snapshot                         |
| [`Session`]                    | Arena-reusing handle for hot loops; caller resets           |
| [`ParsedData`]                 | Self-contained parsed payload; evaluate many rules against one parse |
| [`DataValue`]                  | Arena-borrowed JSON-shaped value (returned from `evaluate`) |
| `OwnedDataValue`               | Heap-owned counterpart of `DataValue` (via `datavalue`)     |
| [`EvaluationConfig`]           | Behaviour knobs: NaN, division by zero, truthiness, coercion |
| [`CustomOperator`]             | Trait you implement to extend the engine                    |
| `operator::EvalContext`        | Opaque engine context passed to `CustomOperator::evaluate`  |
| [`Error`] / [`ErrorKind`]      | Unified error type with operator + node-id breadcrumbs      |
| [`TracedRun`] / [`TracedSession`] | Tracing types (`trace` feature)                          |

[`Engine`]: https://docs.rs/datalogic-rs/latest/datalogic_rs/struct.Engine.html
[`EngineBuilder`]: https://docs.rs/datalogic-rs/latest/datalogic_rs/struct.EngineBuilder.html
[`Logic`]: https://docs.rs/datalogic-rs/latest/datalogic_rs/struct.Logic.html
[`Session`]: https://docs.rs/datalogic-rs/latest/datalogic_rs/struct.Session.html
[`ParsedData`]: https://docs.rs/datalogic-rs/latest/datalogic_rs/struct.ParsedData.html
[`DataValue`]: https://docs.rs/datalogic-rs/latest/datalogic_rs/enum.DataValue.html
[`EvaluationConfig`]: https://docs.rs/datalogic-rs/latest/datalogic_rs/struct.EvaluationConfig.html
[`CustomOperator`]: https://docs.rs/datalogic-rs/latest/datalogic_rs/trait.CustomOperator.html
[`Error`]: https://docs.rs/datalogic-rs/latest/datalogic_rs/struct.Error.html
[`ErrorKind`]: https://docs.rs/datalogic-rs/latest/datalogic_rs/enum.ErrorKind.html
[`TracedRun`]: https://docs.rs/datalogic-rs/latest/datalogic_rs/struct.TracedRun.html
[`TracedSession`]: https://docs.rs/datalogic-rs/latest/datalogic_rs/struct.TracedSession.html

## Custom operators

Register custom operators on `Engine::builder()` and call them from
rules just like the built-ins. Args arrive **pre-evaluated** as
arena-resident `&DataValue<'a>` borrows; you allocate the result back
into the arena.

```rust
use bumpalo::Bump;
use datalogic_rs::{CustomOperator, DataValue, Engine, Result, operator::EvalContext};

struct Double;
impl CustomOperator for Double {
    fn evaluate<'a>(
        &self,
        args: &[&'a DataValue<'a>],
        _ctx: &mut EvalContext<'_, 'a>,
        arena: &'a Bump,
    ) -> Result<&'a DataValue<'a>> {
        let n = args.first().and_then(|v| v.as_f64()).unwrap_or(0.0);
        Ok(arena.alloc(DataValue::from_f64(n * 2.0)))
    }
}

let engine = Engine::builder().add_operator("double", Double).build();
let result = engine.eval_str(r#"{"double": 21}"#, "null").unwrap();
assert_eq!(result, "42");
```

Runnable example: [`examples/custom_operator.rs`](https://github.com/GoPlasmatic/datalogic-rs/blob/main/crates/datalogic-rs/examples/custom_operator.rs).
Full guide: [Custom Operators](https://goplasmatic.github.io/datalogic-rs/advanced/custom-operators.html).

The `CustomOperator` trait is the headline extension point and is
**stable for the 5.x series** — no required-method additions, no
signature changes.

### Introspection

`engine.builtin_operator_names()` iterates every built-in key this
build resolves (canonical name first, then its aliases such as `var`,
`?:`, and `match`), reflecting the compiled feature set;
`engine.custom_operator_names()` lists the registered custom operators
and `engine.has_custom_operator(name)` checks a single name. The union
of the two is the engine's full vocabulary, which authoring tools need
under templating mode, where an unknown key is data rather than an
error. A custom operator registered under a built-in name is never
reached: built-in resolution wins at compile time.

```rust
use datalogic_rs::Engine;

let engine = Engine::new();
let names: Vec<&str> = engine.builtin_operator_names().collect();
assert!(names.contains(&"val"));
assert!(names.contains(&"var")); // alias of `val`
assert_eq!(engine.custom_operator_names().count(), 0);
```

## Configuration

`EvaluationConfig` controls edge-case behaviour:

- **NaN handling** (`NanHandling`) — what happens when arithmetic
  receives a non-number
- **Division by zero** (`DivisionByZeroHandling`)
- **Truthiness** (`TruthyEvaluator`) — JavaScript, Python, strict
  boolean, or a custom closure
- **Numeric coercion** (`NumericCoercionConfig`) — null-to-zero,
  bool-to-number, empty-string-to-zero, etc.
- **Recursion depth** — guards against pathological inputs

Presets like `EvaluationConfig::safe_arithmetic()` and
`EvaluationConfig::strict()` cover common postures. See the
[Configuration guide](https://goplasmatic.github.io/datalogic-rs/advanced/configuration.html)
and the runnable [`examples/configuration.rs`](https://github.com/GoPlasmatic/datalogic-rs/blob/main/crates/datalogic-rs/examples/configuration.rs).

## Templating mode

With `Engine::builder().with_templating(true)` (requires the
`templating` feature), multi-key objects in a compiled rule become
output-shaping templates — keys flow through to the output and
operator values become computed fields.

```rust
// Cargo.toml: datalogic-rs = { version = "5", features = ["templating"] }
use datalogic_rs::Engine;

let engine = Engine::builder().with_templating(true).build();
let result = engine.eval_str(
    r#"{"greeting": {"cat": ["Hello ", {"var": "name"}]},
        "isAdult": {">=": [{"var": "age"}, 18]}}"#,
    r#"{"name": "Jane", "age": 25}"#,
).unwrap();
// {"greeting":"Hello Jane","isAdult":true}
```

A single-key object is an operator invocation, so a key that names a
built-in (`type`, `map`, `if`, `length`, …) or a registered custom
operator is normally unreachable as an output field. Opt into an escape
prefix to recover it:

```rust
let engine = Engine::builder()
    .with_templating(true)
    .with_template_key_escape('$')
    .build();

// {"$type": {"var": "x"}}  ->  {"type": 1}
// {"$$type": 1}            ->  {"$type": 1}   (doubling escapes the sigil)
```

It is unset by default, so `$`-prefixed keys otherwise pass through
verbatim. The prefix is a `char`, so payloads that already use `$` keys
can pick `~` or `#`.

The 4.x JSONLogic `preserve` *operator* was removed in v5: literal
scalars / arrays work inline already; templated objects belong in
templating mode. Runnable example:
[`examples/structured_objects.rs`](https://github.com/GoPlasmatic/datalogic-rs/blob/main/crates/datalogic-rs/examples/structured_objects.rs).

## Error model

Every fallible path returns `Result<T, Error>`. `Error` is
`#[non_exhaustive]`; it exposes one public field and reads the rest
through accessors:

- `kind: ErrorKind`: the discriminant (`ParseError`, `Thrown`,
  `InvalidArguments`, `TypeError`, `ArithmeticError`, `Custom`, …),
  public for pattern matching
- `operator() -> Option<&str>`: the outermost failing operator
- `node_ids() -> &[u32]`: breadcrumbs from the compiled tree
  (leaf to root); resolve to a JSON path via
  `Error::resolve_path(&logic)`, which returns a `Vec<PathStep>` you
  can print or serialise
- `thrown_value() -> Option<&OwnedDataValue>`: the payload of a
  `throw`, or the structured value the engine threw (division by zero
  under the default config throws `{"type": "NaN"}`)

```rust
use datalogic_rs::{Engine, ErrorKind};

let engine = Engine::new();
let err = engine.eval_str(r#"{"/": [1, 0]}"#, r#"{}"#).unwrap_err();
assert!(matches!(err.kind, ErrorKind::Thrown(_)));
assert_eq!(err.operator(), Some("/"));
assert_eq!(err.thrown_value().map(|v| v.to_string()), Some(r#"{"type":"NaN"}"#.to_string()));

// Missing variables are not errors in any config: `{"var": "missing"}`
// evaluates to null. Use `exists`, `missing`, or `??` to detect them.
```

Runnable example: [`examples/error_handling.rs`](https://github.com/GoPlasmatic/datalogic-rs/blob/main/crates/datalogic-rs/examples/error_handling.rs).

## Thread safety

`Engine`, `Logic`, and your `CustomOperator` types are all `Send + Sync`
— construct the `Engine` once, compile the rule once, share both via
`Arc` across as many threads or tokio tasks as you want. `Session` is
the per-task workhorse: each task opens its own. It owns a
`bumpalo::Bump` that can't be shared across threads (the same way a
database connection is per-task in a connection-pool model), and Rust
enforces this at compile time — there's no runtime hazard.

| Type                           | Pattern                                                                        |
|--------------------------------|--------------------------------------------------------------------------------|
| `Engine`                       | One per process; share via `Arc`                                               |
| `Logic`                        | Compile once; share via `Arc` (or use `Engine::compile_arc`)                   |
| `CustomOperator` implementors  | Register on the builder; live inside the shared `Engine` (`Send + Sync` bound) |
| `Session`                      | One per task / per goroutine — the per-task workhorse                          |

Runnable example: [`examples/thread_safety.rs`](https://github.com/GoPlasmatic/datalogic-rs/blob/main/crates/datalogic-rs/examples/thread_safety.rs).

## Feature flags

| Feature           | Effect                                                                    |
|-------------------|---------------------------------------------------------------------------|
| `serde_json`      | `&serde_json::Value` interop and `eval_into::<T>` typed deserialisation   |
| `templating`      | Structure-preservation (templating) mode                                  |
| `datetime`        | Date / time operators (pulls in `chrono` + `chrono-tz` for IANA-zone args) |
| `trace`           | Execution-step recording for the debugger (implies `serde_json`)          |
| `error-handling`  | `try` / `throw` operators                                                 |
| `ext-string`, `ext-array`, `ext-object`, `ext-control`, `ext-math` | Optional operator families |
| `flagd`           | flagd-compat operators (`fractional`, `sem_ver`); pulls in `semver`       |
| `wasm-clock`      | JS-host clock for `now` on `wasm32-unknown-unknown` (`chrono/wasmbind`). Enable only when a JS host runs the module; never for wasmtime / wazero / Chicory (issue #47). Without it `now` traps on that target |
| `tensor`          | The `Tensor` value plus 20 marshalling-only operators over it (JSON to a model's inputs and back). No new dependency |
| `tensor-half`     | Lifts the `f16` / `bf16` restriction on the element-wise tensor operators; pulls in `half` through datavalue |
| `budget`          | Per-evaluation operation counter with a hard abort — `EvaluationConfig::ops_budget`, `Engine::evaluate_metered`, `EvalContext::charge`, `ErrorKind::BudgetExceeded` |

The default build is `serde_json`-free; opt in via
`features = ["serde_json"]` when you need the value boundary.

### `flagd` — OpenFeature flagd-compatible operators

Enables two operators specified by the
[OpenFeature flagd in-process provider](https://flagd.dev/reference/custom-operations/),
implemented to match the canonical
[Go evaluator](https://github.com/open-feature/flagd/tree/main/core/pkg/evaluator)
byte-for-byte:

- **[`fractional`]https://flagd.dev/reference/custom-operations/fractional-operation/**  deterministic percentage bucketing for A/B tests and rollouts. Uses
  MurmurHash3 x86-32 of a bucketing key (explicit string, or implicit
  `flagKey + targetingKey` from the root `$flagd` envelope) plus
  `(hash * total_weight) >> 32` integer distribution, identical to the
  Go evaluator's algorithm. The hash is vendored inline (~30 LOC, no
  external dep) for portability across every target.
- **[`sem_ver`]https://flagd.dev/reference/custom-operations/semver-operation/**  semantic-version comparison with the spec's four input normalizations:
  strip leading `v`/`V`, pad partial versions (`1.0``1.0.0`), coerce
  numeric input to string, and drop SemVer build metadata. Backed by
  the [`semver`]https://docs.rs/semver crate (optional dep). Operators:
  `=`, `!=`, `<`, `<=`, `>`, `>=`, `^` (same major), `~` (same major+minor).

Both operators return `null` on malformed input rather than raising — the
flagd evaluator observes the `null` and falls back to the flag's default
variant; non-flagd callers can compose with `??` or `if` for the same
effect.

```rust,no_run
// Cargo.toml: datalogic-rs = { version = "5", features = ["flagd"] }
use datalogic_rs::Engine;

let engine = Engine::new();

// A typical flagd targeting rule: ship "new-ui" to 50 % of @example.com users.
let result: String = engine.eval_str(
    r#"{
        "fractional": [
            { "cat": [{ "var": "$flagd.flagKey" }, { "var": "email" }] },
            ["new-ui", 50],
            ["old-ui", 50]
        ]
    }"#,
    r#"{"email": "alice@example.com", "$flagd": {"flagKey": "header-color"}}"#,
)?;
// result is one of "\"new-ui\"" or "\"old-ui\"" — sticky per email.
# Ok::<(), datalogic_rs::Error>(())
```

Conformance test suites under
[`tests/suites/flagd/`](https://github.com/GoPlasmatic/datalogic-rs/tree/main/crates/datalogic-rs/tests/suites/flagd/) mirror the canonical Go test
files in `open-feature/flagd` so every release is checked against the
upstream behaviour.

### `budget` — bounding the work a rule does

Untrusted rules need a bound that is not a wall-clock timeout: one that
is the same on every machine, and that refuses the work rather than
reporting it afterwards. The `budget` feature adds a per-evaluation
operation counter with a hard abort.

```rust,ignore
// Cargo.toml: datalogic-rs = { version = "5", features = ["budget"] }
use datalogic_rs::{Engine, EvaluationConfig, Metered};
use bumpalo::Bump;

// Engine-wide: every evaluation is bounded.
let engine = Engine::builder()
    .with_config(EvaluationConfig::default().with_ops_budget(Some(100_000)))
    .build();

// Per call, and reporting what was spent.
let compiled = engine.compile(r#"{"map": [{"var": "xs"}, {"*": [{"var": ""}, 2]}]}"#)?;
let arena = Bump::new();
let Metered { value, ops } =
    engine.evaluate_metered(&compiled, r#"{"xs": [1, 2, 3]}"#, &arena, u64::MAX)?;
# Ok::<(), datalogic_rs::Error>(())
```

One operation is one dispatched node, one item an iterator examines, or
whatever an operator charges for the data it moves — the tensor family
prices itself in elements, and a `CustomOperator` can do the same through
`EvalContext::charge`. Literals and constant-folded subtrees cost nothing.
Crossing the ceiling raises `ErrorKind::BudgetExceeded { budget, spent }`
before the work is done, decorated with the node breadcrumb like every
other error; `try` observes it but cannot recover from it.

Full guide, including how to pick a number and what the count does *not*
promise across versions:
[Operation Budget](https://goplasmatic.github.io/datalogic-rs/advanced/operation-budget.html).

## Performance

Compiled rules dispatch through a single `OpCode` enum (no string
lookups), values live in a `bumpalo::Bump` arena (no per-result heap
allocation), and read-through operators like `var` borrow zero-copy
from the caller's input. Geomean 10.3 ns/op across 51 operator suites
on Apple M2 Pro — see the cross-library comparison in
[`tools/benchmark/BENCHMARK.md`](https://github.com/GoPlasmatic/datalogic-rs/blob/main/tools/benchmark/BENCHMARK.md).

## Migrating from v4

v5 is a breaking release with a hard cliff — no `compat` feature, no
deprecated method shims. Headline renames: `DataLogic` → `Engine`,
`evaluate_json` → `eval_str` / `eval_into::<T>`, `Operator` →
`CustomOperator`, `with_config(...)` →
`Engine::builder().with_config(...).build()`. See
[`MIGRATION.md`](https://github.com/GoPlasmatic/datalogic-rs/blob/main/MIGRATION.md) for the full v4 → v5 cookbook and
[`CHANGELOG.md`](CHANGELOG.md) for the chronological breakage list.

## Learn more

- [Repo README]https://github.com/GoPlasmatic/datalogic-rs#readme — cross-runtime overview, per-binding READMEs
- [Documentation site]https://goplasmatic.github.io/datalogic-rs/ — long-form guide, operator reference, advanced topics
- [Online playground]https://goplasmatic.github.io/datalogic-rs/playground/ — try rules live in the visual debugger
- [`docs.rs/datalogic-rs`]https://docs.rs/datalogic-rs — Rust API reference
- [`examples/README.md`]https://github.com/GoPlasmatic/datalogic-rs/blob/main/crates/datalogic-rs/examples/README.md — index of runnable examples
- [`tests/README.md`]https://github.com/GoPlasmatic/datalogic-rs/blob/main/crates/datalogic-rs/tests/README.md — JSONLogic suite format

## License

Apache 2.0 — see [LICENSE](https://github.com/GoPlasmatic/datalogic-rs/blob/main/LICENSE).