Basic Concepts
Understanding how datalogic-rs works will help you use it effectively.
JSONLogic Format
A JSONLogic rule is a JSON object where:
- The key is the operator name
- The value is an array of arguments (or a single argument)
{ "operator": [arg1, arg2, ...] }
Arguments can be:
- Literal values:
1,"hello",true,null - Arrays:
[1, 2, 3] - Nested operations:
{ "var": "x" }
Examples
// Simple comparison
{ ">": [5, 3] } // true
// Variable access
{ "var": "user.name" } // Access user.name from data
// Nested operations
{ "+": [{ "var": "a" }, { "var": "b" }] } // Add two variables
// Multiple arguments
{ "and": [true, true, false] } // false
Compilation vs Evaluation
datalogic-rs separates rule processing into two phases.
Compilation Phase
When you call engine.compile(rule_str), the library:
- Parses the JSON rule into an internal representation
- Assigns OpCodes to operators for fast dispatch
- Pre-evaluates constant sub-expressions
- Produces a reusable
Logic(noArcwrap by default — wrap explicitly when sharing across threads)
#![allow(unused)]
fn main() {
let compiled = engine.compile(r#"{">": [{"var": "x"}, 10]}"#).unwrap();
// Wrap when you want to share across threads:
let shared = std::sync::Arc::new(compiled);
}
Evaluation Phase
When you evaluate, the engine:
- Dispatches operations via
OpCode(O(1)) for built-ins - Walks the context stack for variable lookups
- Returns an arena-resident
&DataValue<'a>(or an ownedString/OwnedDataValue/serde_json::Valuedepending on the entry point)
There are four entry points, picked by what the caller has on hand and how much arena lifetime they want to manage:
| Entry point | When to use | Returns |
|---|---|---|
datalogic_rs::eval_str(rule, data) (and eval / eval_into / compile) | One-shot, no engine config needed. Uses a shared default engine internally. | String (or OwnedDataValue / T) |
Engine::eval_str(rule, data) (and eval / eval_into) | One-shot through a configured engine — custom operators, non-default config, templating. | String (or OwnedDataValue / T) |
Engine::session().eval* | Repeated calls — the session owns a reusable arena. Caller calls session.reset() between batches. | String / OwnedDataValue / T / borrowed &DataValue<'a> (eval_borrowed) |
Engine::evaluate(logic, data, &arena) | Hot path. You own the bumpalo::Bump and want zero-copy &DataValue<'a> results. | &DataValue<'a> |
#![allow(unused)]
fn main() {
use bumpalo::Bump;
use datalogic_rs::Engine;
let engine = Engine::new();
let compiled = engine.compile(r#"{">": [{"var": "x"}, 10]}"#).unwrap();
// Reusable session — caller resets between batches.
let mut session = engine.session();
let _ = session.eval_str(&compiled, r#"{"x": 42}"#).unwrap();
session.reset();
// Or manage the arena yourself for zero-copy results.
let arena = Bump::new();
let r = engine.evaluate(&compiled, r#"{"x": 42}"#, &arena).unwrap();
assert_eq!(r.as_bool(), Some(true));
}
The Engine
The Engine struct is your main entry point. It is built via Engine::new
or the EngineBuilder:
#![allow(unused)]
fn main() {
use datalogic_rs::{Engine, EvaluationConfig};
// Default engine
let engine = Engine::new();
// Engine with custom configuration
let engine = Engine::builder()
.with_config(EvaluationConfig::strict())
.build();
// Engine with templating mode — needs feature = ["templating"]
#[cfg(feature = "templating")]
let engine = Engine::builder().with_templating(true).build();
// Engine with custom operators
struct MyOp;
impl datalogic_rs::CustomOperator for MyOp {
fn evaluate<'a>(
&self,
_args: &[&'a datalogic_rs::DataValue<'a>],
_ctx: &mut datalogic_rs::operator::EvalContext<'_, 'a>,
arena: &'a bumpalo::Bump,
) -> datalogic_rs::Result<&'a datalogic_rs::DataValue<'a>> {
Ok(arena.alloc(datalogic_rs::DataValue::Null))
}
}
let engine = Engine::builder()
.add_operator("my_op", MyOp)
.build();
}
The engine:
- Owns the registered custom operators (frozen at
build()) - Holds the evaluation configuration
- Provides compile and evaluate methods
Note: v5 makes operator registration builder-only. You can no longer mutate an
Engineto add operators after construction.
Context Stack
The context stack manages variable scope during evaluation. This is
important for array operations like map, filter, and reduce.
#![allow(unused)]
fn main() {
// In a filter operation, "" refers to the current element
let r = datalogic_rs::eval_str(
r#"{"filter": [[1, 2, 3, 4, 5], {">": [{"var": ""}, 3]}]}"#,
r#"{}"#,
).unwrap();
// Result: "[4,5]"
}
During array operations:
""(orvarwith empty string) refers to the current element- The outer data context is still accessible
- Nested operations push and pop frames automatically
Type Coercion
JSONLogic operators often perform type coercion:
Arithmetic
- Strings are parsed as numbers when possible (
"5" + 3 = 8) - Non-numeric strings raise a
Thrown { type: "NaN" }error by default; configurable viaEvaluationConfig::arithmetic_nan_handling
Comparison
==performs loose equality (with type coercion)===performs strict equality (no coercion)
Truthiness
By default, uses JavaScript-style truthiness:
- Falsy:
false,0,"",null,[] - Truthy: everything else
This is configurable via EvaluationConfig.
Thread Safety
Logic is Send + Sync and can be shared across threads via Arc:
#![allow(unused)]
fn main() {
use datalogic_rs::Engine;
use std::sync::Arc;
use std::thread;
let engine = Arc::new(Engine::new());
let compiled = engine.compile_arc(r#"{"+": [{"var": "x"}, 1]}"#).unwrap();
let handles: Vec<_> = (0..4).map(|i| {
let engine = Arc::clone(&engine);
let compiled = Arc::clone(&compiled);
thread::spawn(move || {
let mut session = engine.session();
session.eval_str(&compiled, &format!(r#"{{"x": {}}}"#, i)).unwrap()
})
}).collect();
for h in handles {
println!("{}", h.join().unwrap());
}
}
Next Steps
- Operators Overview - Learn about all available operators
- Configuration - Customize evaluation behavior
- Custom Operators - Extend with your own logic
- Migration Guide - Move from v4 to v5