datalogic-rs is a high-performance Rust implementation of JSONLogic for evaluating logical rules expressed as JSON. It provides a fast, memory-efficient, and thread-safe way to evaluate complex business rules, feature flags, dynamic pricing logic, and more.
Why datalogic-rs?
- Fast: Uses OpCode-based dispatch with compile-time optimization for maximum performance
- Thread-Safe: Compile once, evaluate anywhere with zero-copy
Arcsharing - Intuitive: Works seamlessly with
serde_json::Value - Extensible: Add custom operators with a simple trait
- Feature-Rich: 59 built-in operators including datetime, regex, and error handling
- Fully Compliant: Passes the official JSONLogic test suite
How It Works
datalogic-rs uses a two-phase approach:
-
Compilation: Your JSON logic is parsed and compiled into an optimized
CompiledLogicstructure. This phase:- Assigns OpCodes to built-in operators for fast dispatch
- Pre-evaluates constant expressions
- Analyzes structure for templating mode
-
Evaluation: The compiled logic is evaluated against your data with:
- Direct OpCode dispatch (no string lookups at runtime)
- Context stack for nested operations (map, filter, reduce)
- Efficient value passing with minimal allocations
Quick Example
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::new();
// Define a rule: is the user's age greater than 18?
let rule = json!({ ">": [{ "var": "age" }, 18] });
// Compile once
let compiled = engine.compile(&rule).unwrap();
// Evaluate against different data
let result = engine.evaluate_owned(&compiled, json!({ "age": 21 })).unwrap();
assert_eq!(result, json!(true));
let result = engine.evaluate_owned(&compiled, json!({ "age": 16 })).unwrap();
assert_eq!(result, json!(false));
}
What is JSONLogic?
JSONLogic is a standard for expressing logic rules as JSON. This makes it:
- Portable: Rules can be stored in databases, sent over APIs, or embedded in configuration
- Language-agnostic: The same rules work across different implementations
- Human-readable: Rules are easier to understand than arbitrary code
- Safe: Rules can be evaluated without arbitrary code execution
A JSONLogic rule is a JSON object where:
- The key is the operator name
- The value is an array of arguments
{"operator": [arg1, arg2, ...]}
For example:
{"and": [
{">": [{"var": "age"}, 18]},
{"==": [{"var": "country"}, "US"]}
]}
This rule checks if age > 18 AND country == "US".
Next Steps
- Installation - Add datalogic-rs to your project
- Quick Start - Get up and running in minutes
- Operators - Explore all 59 built-in operators
Playground
Try JSONLogic expressions right in your browser! This playground uses the same WebAssembly-compiled engine that powers the Rust library.
How to Use
- Logic: Enter your JSONLogic expression in the Logic pane
- Data: Enter the JSON data to evaluate against in the Data pane
- Run: Press the Run button or use Ctrl+Enter (Cmd+Enter on Mac)
- Examples: Use the dropdown to load pre-built examples
Quick Reference
Basic Operators
| Operator | Example | Description |
|---|---|---|
var | {"var": "x"} | Access variable |
== | {"==": [1, 1]} | Equality |
>, <, >=, <= | {">": [5, 3]} | Comparison |
and, or | {"and": [true, true]} | Logical |
if | {"if": [cond, then, else]} | Conditional |
+, -, *, / | {"+": [1, 2]} | Arithmetic |
Array Operations
| Operator | Example | Description |
|---|---|---|
map | {"map": [arr, expr]} | Transform elements |
filter | {"filter": [arr, cond]} | Filter elements |
reduce | {"reduce": [arr, expr, init]} | Reduce to value |
all, some, none | {"all": [arr, cond]} | Check conditions |
String Operations
| Operator | Example | Description |
|---|---|---|
cat | {"cat": ["a", "b"]} | Concatenate |
substr | {"substr": ["hello", 0, 2]} | Substring |
in | {"in": ["@", "a@b.com"]} | Contains |
Example: Feature Flag
Determine if a user has access to a premium feature:
{
"and": [
{"==": [{"var": "user.plan"}, "premium"]},
{">=": [{"var": "user.accountAge"}, 30]}
]
}
Data:
{
"user": {
"plan": "premium",
"accountAge": 45
}
}
Example: Dynamic Pricing
Calculate a discounted price based on quantity:
{
"if": [
{">=": [{"var": "quantity"}, 100]},
{"*": [{"var": "price"}, 0.8]},
{"if": [
{">=": [{"var": "quantity"}, 50]},
{"*": [{"var": "price"}, 0.9]},
{"var": "price"}
]}
]
}
Data:
{
"quantity": 75,
"price": 100
}
Learn More
- Operators Overview - Full operator documentation
- Getting Started - Using the library
- Use Cases - Real-world examples
Installation
Adding to Your Project
Add datalogic-rs to your Cargo.toml:
[dependencies]
datalogic-rs = "4.0"
serde_json = "1.0"
Or use cargo add:
cargo add datalogic-rs serde_json
Version Selection
- v4.x (recommended): Ergonomic API with
serde_json::Value, simpler to use - v3.x: Arena-based allocation for maximum raw performance
Both versions are actively maintained. Choose v4 for ease of use, v3 if you need every bit of performance.
Feature Flags
datalogic-rs has minimal dependencies by default. All features are included in the base crate.
WebAssembly Support
For WebAssembly targets, use the datalogic-wasm crate:
npm install datalogic-wasm
Or build from source:
cd datalogic-wasm
wasm-pack build --target web
Minimum Rust Version
datalogic-rs requires Rust 1.70 or later.
Verifying Installation
Create a simple test:
use datalogic_rs::DataLogic;
use serde_json::json;
fn main() {
let engine = DataLogic::new();
let rule = json!({ "+": [1, 2] });
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({})).unwrap();
println!("1 + 2 = {}", result);
assert_eq!(result, json!(3));
}
Run with:
cargo run
You should see: 1 + 2 = 3
Quick Start
This guide will get you evaluating JSONLogic rules in minutes.
Basic Workflow
The typical workflow with datalogic-rs is:
- Create an engine instance
- Compile your rule (once)
- Evaluate against data (many times)
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
// 1. Create engine
let engine = DataLogic::new();
// 2. Compile rule
let rule = json!({ ">": [{ "var": "score" }, 50] });
let compiled = engine.compile(&rule).unwrap();
// 3. Evaluate against data
let result = engine.evaluate_owned(&compiled, json!({ "score": 75 })).unwrap();
assert_eq!(result, json!(true));
}
Quick JSON Evaluation
For one-off evaluations, use evaluate_json:
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::new();
let result = engine.evaluate_json(
r#"{ "+": [1, 2, 3] }"#,
r#"{}"#
).unwrap();
assert_eq!(result, json!(6));
}
Working with Variables
Access data using the var operator:
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::new();
// Simple variable access
let rule = json!({ "var": "name" });
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({ "name": "Alice" })).unwrap();
assert_eq!(result, json!("Alice"));
// Nested variable access with dot notation
let rule = json!({ "var": "user.address.city" });
let compiled = engine.compile(&rule).unwrap();
let data = json!({
"user": {
"address": {
"city": "New York"
}
}
});
let result = engine.evaluate_owned(&compiled, data).unwrap();
assert_eq!(result, json!("New York"));
// Default values
let rule = json!({ "var": ["missing_key", "default_value"] });
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({})).unwrap();
assert_eq!(result, json!("default_value"));
}
Conditional Logic
Use if for branching:
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::new();
let rule = json!({
"if": [
{ ">=": [{ "var": "age" }, 18] },
"adult",
"minor"
]
});
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({ "age": 25 })).unwrap();
assert_eq!(result, json!("adult"));
let result = engine.evaluate_owned(&compiled, json!({ "age": 15 })).unwrap();
assert_eq!(result, json!("minor"));
}
Combining Conditions
Use and and or to combine conditions:
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::new();
// AND: all conditions must be true
let rule = json!({
"and": [
{ ">=": [{ "var": "age" }, 18] },
{ "==": [{ "var": "verified" }, true] }
]
});
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({
"age": 21,
"verified": true
})).unwrap();
assert_eq!(result, json!(true));
// OR: at least one condition must be true
let rule = json!({
"or": [
{ "==": [{ "var": "role" }, "admin"] },
{ "==": [{ "var": "role" }, "moderator"] }
]
});
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({ "role": "admin" })).unwrap();
assert_eq!(result, json!(true));
}
Array Operations
Filter, map, and reduce arrays:
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::new();
// Filter: keep elements matching a condition
let rule = json!({
"filter": [
{ "var": "numbers" },
{ ">": [{ "var": "" }, 5] }
]
});
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({
"numbers": [1, 3, 5, 7, 9]
})).unwrap();
assert_eq!(result, json!([7, 9]));
// Map: transform each element
let rule = json!({
"map": [
{ "var": "numbers" },
{ "*": [{ "var": "" }, 2] }
]
});
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({
"numbers": [1, 2, 3]
})).unwrap();
assert_eq!(result, json!([2, 4, 6]));
}
Error Handling
Handle errors gracefully:
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::new();
// Compilation errors
let result = engine.compile(&json!({ "unknown_op": [] }));
// Result depends on preserve_structure setting
// Evaluation errors with try/catch
let rule = json!({
"try": [
{ "/": [10, { "var": "divisor" }] },
0 // Fallback value
]
});
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({ "divisor": 0 })).unwrap();
assert_eq!(result, json!(0)); // Division by zero caught
}
Next Steps
- Basic Concepts - Understand the architecture
- Operators - Explore all available operators
- Custom Operators - Extend with your own logic
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), the library:
- Parses the JSON into an internal representation
- Assigns OpCodes to operators for fast dispatch
- Pre-evaluates constant expressions
- Wraps the result in
Arcfor thread-safe sharing
#![allow(unused)]
fn main() {
// Compile once
let compiled = engine.compile(&rule).unwrap();
// The compiled rule can be shared across threads
let compiled_clone = Arc::clone(&compiled);
}
Evaluation Phase
When you call engine.evaluate_owned(&compiled, data):
- Dispatches operations via OpCode (O(1) lookup)
- Accesses data through the context stack
- Returns the result as a
Value
#![allow(unused)]
fn main() {
// Evaluate many times with different data
let result1 = engine.evaluate_owned(&compiled, json!({ "x": 1 })).unwrap();
let result2 = engine.evaluate_owned(&compiled, json!({ "x": 2 })).unwrap();
}
The DataLogic Engine
The DataLogic struct is your main entry point:
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
// Default engine
let engine = DataLogic::new();
// Engine with configuration
let engine = DataLogic::with_config(config);
// Engine with structure preservation (templating mode)
let engine = DataLogic::with_preserve_structure();
// Engine with both
let engine = DataLogic::with_config_and_structure(config, true);
}
The engine:
- Stores custom operators
- Holds evaluation configuration
- Provides compile and evaluate methods
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 rule = json!({
"filter": [
[1, 2, 3, 4, 5],
{ ">": [{ "var": "" }, 3] } // "" = current element
]
});
// Result: [4, 5]
}
During array operations:
""orvarwith empty string refers to the current element- The outer data context is still accessible
- Nested operations create nested contexts
Evaluation Methods
evaluate_owned
Takes ownership of the data, best for most use cases:
#![allow(unused)]
fn main() {
let result = engine.evaluate_owned(&compiled, json!({ "x": 1 })).unwrap();
}
evaluate
Borrows the data, useful when you need to reuse the data:
#![allow(unused)]
fn main() {
let data = json!({ "x": 1 });
let result = engine.evaluate(&compiled, &data).unwrap();
// data is still available here
}
evaluate_json
Convenience method that parses JSON strings:
#![allow(unused)]
fn main() {
let result = engine.evaluate_json(
r#"{ "+": [1, 2] }"#, // Rule as JSON string
r#"{"x": 10}"# // Data as JSON string
).unwrap();
}
Type Coercion
JSONLogic operators often perform type coercion:
Arithmetic
- Strings are parsed as numbers when possible
"5" + 3=8- Non-numeric strings may result in errors or NaN (configurable)
Comparison
==performs loose equality (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
CompiledLogic is wrapped in Arc and is Send + Sync:
#![allow(unused)]
fn main() {
use std::sync::Arc;
use std::thread;
let engine = Arc::new(DataLogic::new());
let compiled = engine.compile(&rule).unwrap();
let handles: Vec<_> = (0..4).map(|i| {
let engine = Arc::clone(&engine);
let compiled = Arc::clone(&compiled);
thread::spawn(move || {
engine.evaluate_owned(&compiled, json!({ "x": i }))
})
}).collect();
for handle in handles {
let result = handle.join().unwrap();
// Each thread gets its result
}
}
Next Steps
- Operators Overview - Learn about all available operators
- Configuration - Customize evaluation behavior
- Custom Operators - Extend with your own logic
Operators Overview
datalogic-rs provides 59 built-in operators organized into logical categories. This section documents each operator with syntax, examples, and notes on behavior.
Operator Categories
| Category | Operators | Description |
|---|---|---|
| Variable Access | var, val, exists | Access and check data |
| Comparison | ==, ===, !=, !==, >, >=, <, <= | Compare values |
| Logical | !, !!, and, or | Boolean logic |
| Arithmetic | +, -, *, /, %, max, min, abs, ceil, floor | Math operations |
| Control Flow | if, ?:, ?? | Conditional branching |
| String | cat, substr, in, length, starts_with, ends_with, upper, lower, trim, split | String manipulation |
| Array | merge, filter, map, reduce, all, some, none, sort, slice | Array operations |
| DateTime | datetime, timestamp, parse_date, format_date, date_diff, now | Date and time |
| Missing Values | missing, missing_some | Check for missing data |
| Error Handling | try, throw | Exception handling |
Operator Syntax
All operators follow the JSONLogic format:
{ "operator": [arg1, arg2, ...] }
Some operators accept a single argument without an array:
{ "var": "name" }
// Equivalent to:
{ "var": ["name"] }
Lazy Evaluation
Several operators use lazy (short-circuit) evaluation:
and: Stops at first falsy valueor: Stops at first truthy valueif: Only evaluates the matching branch?:: Only evaluates the matching branch??: Only evaluates fallback if first value is null
This is important when operations have side effects or when you want to avoid errors:
{
"and": [
{ "var": "user" },
{ "var": "user.profile.name" }
]
}
If user is null, the second condition is never evaluated, avoiding an error.
Type Coercion
Operators handle types differently:
Loose vs Strict
==and!=perform type coercion===and!==require exact type match
{ "==": [1, "1"] } // true (loose)
{ "===": [1, "1"] } // false (strict)
Numeric Coercion
Arithmetic operators attempt to convert values to numbers:
{ "+": ["5", 3] } // 8 (string "5" becomes number 5)
Truthiness
Boolean operators use configurable truthiness rules. By default (JavaScript-style):
- Falsy:
false,0,"",null,[] - Truthy: Everything else
Custom Operators
You can add your own operators. See Custom Operators for details.
#![allow(unused)]
fn main() {
engine.add_operator("myop".to_string(), Box::new(MyOperator));
}
Custom operators follow the same syntax:
{ "myop": [arg1, arg2] }
Variable Access Operators
These operators access data from the evaluation context.
var
Access a value from the data object using dot notation.
Syntax:
{ "var": "path" }
{ "var": ["path", default] }
Arguments:
path- Dot-separated path to the value (string)default- Optional default value if path doesn’t exist
Returns: The value at the path, or the default value, or null.
Examples:
// Simple access
{ "var": "name" }
// Data: { "name": "Alice" }
// Result: "Alice"
// Nested access
{ "var": "user.address.city" }
// Data: { "user": { "address": { "city": "NYC" } } }
// Result: "NYC"
// Array index access
{ "var": "items.0" }
// Data: { "items": ["a", "b", "c"] }
// Result: "a"
// Default value
{ "var": ["missing", "default"] }
// Data: {}
// Result: "default"
// Access entire data object
{ "var": "" }
// Data: { "x": 1, "y": 2 }
// Result: { "x": 1, "y": 2 }
Try it:
Notes:
- Empty string
""returns the entire data context - In array operations (
map,filter,reduce),""refers to the current element - Numeric indices work for both arrays and string characters
- Returns
nullif path doesn’t exist and no default is provided
val
Alternative variable access with additional path navigation capabilities.
Syntax:
{ "val": "path" }
{ "val": ["path", default] }
Arguments:
path- Path to the value, supports additional navigation syntaxdefault- Optional default value
Returns: The value at the path, or the default value, or null.
Examples:
// Simple access (same as var)
{ "val": "name" }
// Data: { "name": "Bob" }
// Result: "Bob"
// Nested access
{ "val": "config.settings.enabled" }
// Data: { "config": { "settings": { "enabled": true } } }
// Result: true
Try it:
Notes:
- Similar to
varbut with extended path syntax support - Useful for complex data navigation scenarios
exists
Check if a variable path exists in the data.
Syntax:
{ "exists": "path" }
{ "exists": { "var": "path" } }
Arguments:
path- Path to check (string or var operation)
Returns: true if the path exists, false otherwise.
Examples:
// Check if key exists
{ "exists": "name" }
// Data: { "name": "Alice" }
// Result: true
// Check missing key
{ "exists": "age" }
// Data: { "name": "Alice" }
// Result: false
// Check nested path
{ "exists": "user.profile" }
// Data: { "user": { "profile": { "name": "Bob" } } }
// Result: true
// Check with var
{ "exists": { "var": "fieldName" } }
// Data: { "fieldName": "name", "name": "Alice" }
// Result: true (checks if "name" exists)
Try it:
Notes:
- Returns
falsefor paths that don’t exist - Does not check if the value is null/empty, only if the path exists
- Useful for conditional logic based on data structure
Comparison Operators
Operators for comparing values. All comparison operators support lazy evaluation.
== (Equals)
Loose equality comparison with type coercion.
Syntax:
{ "==": [a, b] }
Arguments:
a- First valueb- Second value
Returns: true if values are equal (after type coercion), false otherwise.
Examples:
// Same type
{ "==": [1, 1] }
// Result: true
// Type coercion
{ "==": [1, "1"] }
// Result: true
{ "==": [0, false] }
// Result: true
{ "==": ["", false] }
// Result: true
// Null comparison
{ "==": [null, null] }
// Result: true
// Arrays
{ "==": [[1, 2], [1, 2]] }
// Result: true
Try it:
Notes:
- Performs type coercion similar to JavaScript’s
== - For strict comparison without coercion, use
===
=== (Strict Equals)
Strict equality comparison without type coercion.
Syntax:
{ "===": [a, b] }
Arguments:
a- First valueb- Second value
Returns: true if values are equal and same type, false otherwise.
Examples:
// Same type and value
{ "===": [1, 1] }
// Result: true
// Different types
{ "===": [1, "1"] }
// Result: false
{ "===": [0, false] }
// Result: false
// Null
{ "===": [null, null] }
// Result: true
Try it:
!= (Not Equals)
Loose inequality comparison with type coercion.
Syntax:
{ "!=": [a, b] }
Arguments:
a- First valueb- Second value
Returns: true if values are not equal (after type coercion), false otherwise.
Examples:
{ "!=": [1, 2] }
// Result: true
{ "!=": [1, "1"] }
// Result: false (type coercion makes them equal)
{ "!=": ["hello", "world"] }
// Result: true
Try it:
!== (Strict Not Equals)
Strict inequality comparison without type coercion.
Syntax:
{ "!==": [a, b] }
Arguments:
a- First valueb- Second value
Returns: true if values are not equal or different types, false otherwise.
Examples:
{ "!==": [1, "1"] }
// Result: true (different types)
{ "!==": [1, 1] }
// Result: false
{ "!==": [1, 2] }
// Result: true
> (Greater Than)
Check if the first value is greater than the second.
Syntax:
{ ">": [a, b] }
{ ">": [a, b, c] }
Arguments:
a,b- Values to comparec- Optional third value for chained comparison
Returns: true if a > b (and b > c if provided), false otherwise.
Examples:
// Simple comparison
{ ">": [5, 3] }
// Result: true
{ ">": [3, 5] }
// Result: false
// Chained comparison (a > b > c)
{ ">": [5, 3, 1] }
// Result: true (5 > 3 AND 3 > 1)
{ ">": [5, 3, 4] }
// Result: false (3 is not > 4)
// String comparison
{ ">": ["b", "a"] }
// Result: true (lexicographic)
// With variables
{ ">": [{ "var": "age" }, 18] }
// Data: { "age": 21 }
// Result: true
Try it:
>= (Greater Than or Equal)
Check if the first value is greater than or equal to the second.
Syntax:
{ ">=": [a, b] }
{ ">=": [a, b, c] }
Arguments:
a,b- Values to comparec- Optional third value for chained comparison
Returns: true if a >= b (and b >= c if provided), false otherwise.
Examples:
{ ">=": [5, 5] }
// Result: true
{ ">=": [5, 3] }
// Result: true
{ ">=": [3, 5] }
// Result: false
// Chained
{ ">=": [5, 3, 3] }
// Result: true (5 >= 3 AND 3 >= 3)
< (Less Than)
Check if the first value is less than the second.
Syntax:
{ "<": [a, b] }
{ "<": [a, b, c] }
Arguments:
a,b- Values to comparec- Optional third value for chained comparison
Returns: true if a < b (and b < c if provided), false otherwise.
Examples:
{ "<": [3, 5] }
// Result: true
{ "<": [5, 3] }
// Result: false
// Chained (useful for range checks)
{ "<": [1, 5, 10] }
// Result: true (1 < 5 AND 5 < 10)
// Range check: is x between 1 and 10?
{ "<": [1, { "var": "x" }, 10] }
// Data: { "x": 5 }
// Result: true
Try it:
<= (Less Than or Equal)
Check if the first value is less than or equal to the second.
Syntax:
{ "<=": [a, b] }
{ "<=": [a, b, c] }
Arguments:
a,b- Values to comparec- Optional third value for chained comparison
Returns: true if a <= b (and b <= c if provided), false otherwise.
Examples:
{ "<=": [3, 5] }
// Result: true
{ "<=": [5, 5] }
// Result: true
{ "<=": [5, 3] }
// Result: false
// Range check (inclusive)
{ "<=": [1, { "var": "x" }, 10] }
// Data: { "x": 10 }
// Result: true (1 <= 10 AND 10 <= 10)
Notes:
- Chained comparisons are useful for range checks
{ "<": [a, x, b] }is equivalent toa < x AND x < b
Logical Operators
Boolean logic operators with short-circuit evaluation.
! (Not)
Logical NOT - negates a boolean value.
Syntax:
{ "!": value }
{ "!": [value] }
Arguments:
value- Value to negate
Returns: true if value is falsy, false if value is truthy.
Examples:
{ "!": true }
// Result: false
{ "!": false }
// Result: true
{ "!": 0 }
// Result: true (0 is falsy)
{ "!": 1 }
// Result: false (1 is truthy)
{ "!": "" }
// Result: true (empty string is falsy)
{ "!": "hello" }
// Result: false (non-empty string is truthy)
{ "!": null }
// Result: true (null is falsy)
{ "!": [] }
// Result: true (empty array is falsy)
{ "!": [1, 2] }
// Note: This negates the array [1, 2], not [value]
// Result: false (non-empty array is truthy)
Try it:
Notes:
- Uses configurable truthiness rules (default: JavaScript-style)
- Falsy values:
false,0,"",null,[] - Truthy values: everything else
!! (Double Not / Boolean Cast)
Convert a value to its boolean equivalent.
Syntax:
{ "!!": value }
{ "!!": [value] }
Arguments:
value- Value to convert to boolean
Returns: true if value is truthy, false if value is falsy.
Examples:
{ "!!": true }
// Result: true
{ "!!": false }
// Result: false
{ "!!": 1 }
// Result: true
{ "!!": 0 }
// Result: false
{ "!!": "hello" }
// Result: true
{ "!!": "" }
// Result: false
{ "!!": [1, 2, 3] }
// Result: true
{ "!!": [] }
// Result: false
{ "!!": null }
// Result: false
Try it:
Notes:
- Equivalent to
{ "!": { "!": value } } - Useful for ensuring a boolean result from any value
and
Logical AND with short-circuit evaluation.
Syntax:
{ "and": [a, b, ...] }
Arguments:
a,b, … - Two or more values to AND together
Returns: The first falsy value encountered, or the last value if all are truthy.
Examples:
// All truthy
{ "and": [true, true] }
// Result: true
// One falsy
{ "and": [true, false] }
// Result: false
// Short-circuit: returns first falsy
{ "and": [true, 0, "never evaluated"] }
// Result: 0
// All truthy returns last value
{ "and": [1, 2, 3] }
// Result: 3
// Multiple conditions
{ "and": [
{ ">": [{ "var": "age" }, 18] },
{ "==": [{ "var": "verified" }, true] },
{ "!=": [{ "var": "banned" }, true] }
]}
// Data: { "age": 21, "verified": true, "banned": false }
// Result: true
Try it:
Notes:
- Short-circuits: stops at first falsy value
- Returns the actual value, not necessarily a boolean
- Empty
andreturnstrue(vacuous truth)
or
Logical OR with short-circuit evaluation.
Syntax:
{ "or": [a, b, ...] }
Arguments:
a,b, … - Two or more values to OR together
Returns: The first truthy value encountered, or the last value if all are falsy.
Examples:
// One truthy
{ "or": [false, true] }
// Result: true
// All falsy
{ "or": [false, false] }
// Result: false
// Short-circuit: returns first truthy
{ "or": [0, "", "found it", "not evaluated"] }
// Result: "found it"
// All falsy returns last value
{ "or": [false, 0, ""] }
// Result: ""
// Default value pattern
{ "or": [{ "var": "nickname" }, { "var": "name" }, "Anonymous"] }
// Data: { "name": "Alice" }
// Result: "Alice" (nickname is null/missing, so returns name)
// Role check
{ "or": [
{ "==": [{ "var": "role" }, "admin"] },
{ "==": [{ "var": "role" }, "moderator"] }
]}
// Data: { "role": "admin" }
// Result: true
Try it:
Notes:
- Short-circuits: stops at first truthy value
- Returns the actual value, not necessarily a boolean
- Useful for default value patterns
- Empty
orreturnsfalse
Truthiness Reference
The default JavaScript-style truthiness:
| Value | Truthy? |
|---|---|
true | Yes |
false | No |
1, 2, -1, 3.14 | Yes |
0, 0.0 | No |
"hello", "0", "false" | Yes |
"" | No |
[1, 2], {"a": 1} | Yes |
[] | No |
null | No |
This can be customized via EvaluationConfig. See Configuration.
Arithmetic Operators
Mathematical operations with type coercion support.
+ (Add)
Add numbers together, or concatenate strings.
Syntax:
{ "+": [a, b, ...] }
{ "+": value }
Arguments:
a,b, … - Values to add (variadic)- Single value is cast to number
Returns: Sum of all arguments, or concatenated string.
Examples:
// Basic addition
{ "+": [1, 2] }
// Result: 3
// Multiple values
{ "+": [1, 2, 3, 4] }
// Result: 10
// Type coercion
{ "+": ["5", 3] }
// Result: 8 (string "5" converted to number)
// Unary plus (convert to number)
{ "+": "42" }
// Result: 42
{ "+": "-3.14" }
// Result: -3.14
// With variables
{ "+": [{ "var": "price" }, { "var": "tax" }] }
// Data: { "price": 100, "tax": 8.5 }
// Result: 108.5
Try it:
Notes:
- Strings are converted to numbers when possible
- Non-numeric strings may result in NaN or error (configurable)
- Single argument converts value to number
- (Subtract)
Subtract numbers.
Syntax:
{ "-": [a, b] }
{ "-": value }
Arguments:
a- Value to subtract fromb- Value to subtract- Single value negates it
Returns: Difference, or negated value.
Examples:
// Subtraction
{ "-": [10, 3] }
// Result: 7
// Unary minus (negate)
{ "-": 5 }
// Result: -5
{ "-": -3 }
// Result: 3
// With coercion
{ "-": ["10", "3"] }
// Result: 7
// Calculate discount
{ "-": [{ "var": "price" }, { "var": "discount" }] }
// Data: { "price": 100, "discount": 15 }
// Result: 85
Try it:
* (Multiply)
Multiply numbers.
Syntax:
{ "*": [a, b, ...] }
Arguments:
a,b, … - Values to multiply (variadic)
Returns: Product of all arguments.
Examples:
// Basic multiplication
{ "*": [3, 4] }
// Result: 12
// Multiple values
{ "*": [2, 3, 4] }
// Result: 24
// With coercion
{ "*": ["5", 2] }
// Result: 10
// Calculate total
{ "*": [{ "var": "quantity" }, { "var": "price" }] }
// Data: { "quantity": 3, "price": 25 }
// Result: 75
// Apply percentage
{ "*": [{ "var": "amount" }, 0.1] }
// Data: { "amount": 200 }
// Result: 20
Try it:
/ (Divide)
Divide numbers.
Syntax:
{ "/": [a, b] }
Arguments:
a- Dividendb- Divisor
Returns: Quotient.
Examples:
// Basic division
{ "/": [10, 2] }
// Result: 5
// Decimal result
{ "/": [7, 2] }
// Result: 3.5
// Division by zero (configurable behavior)
{ "/": [10, 0] }
// Result: Infinity (default) or error
// With coercion
{ "/": ["100", "4"] }
// Result: 25
// Calculate average
{ "/": [{ "+": [10, 20, 30] }, 3] }
// Result: 20
Try it:
Notes:
- Division by zero behavior is configurable via
EvaluationConfig - Default returns
Infinityor-Infinity
% (Modulo)
Calculate remainder of division.
Syntax:
{ "%": [a, b] }
Arguments:
a- Dividendb- Divisor
Returns: Remainder after division.
Examples:
// Basic modulo
{ "%": [10, 3] }
// Result: 1
{ "%": [10, 5] }
// Result: 0
// Negative numbers
{ "%": [-10, 3] }
// Result: -1
// Check if even
{ "==": [{ "%": [{ "var": "n" }, 2] }, 0] }
// Data: { "n": 4 }
// Result: true
Try it:
max
Find the maximum value.
Syntax:
{ "max": [a, b, ...] }
{ "max": array }
Arguments:
a,b, … - Values to compare, orarray- Single array of values
Returns: The largest value.
Examples:
// Multiple arguments
{ "max": [1, 5, 3] }
// Result: 5
// Single array
{ "max": [[1, 5, 3]] }
// Result: 5
// With variables
{ "max": [{ "var": "scores" }] }
// Data: { "scores": [85, 92, 78] }
// Result: 92
// Empty array
{ "max": [[]] }
// Result: null
Try it:
min
Find the minimum value.
Syntax:
{ "min": [a, b, ...] }
{ "min": array }
Arguments:
a,b, … - Values to compare, orarray- Single array of values
Returns: The smallest value.
Examples:
// Multiple arguments
{ "min": [5, 1, 3] }
// Result: 1
// Single array
{ "min": [[5, 1, 3]] }
// Result: 1
// With variables
{ "min": [{ "var": "prices" }] }
// Data: { "prices": [29.99, 19.99, 39.99] }
// Result: 19.99
// Empty array
{ "min": [[]] }
// Result: null
Try it:
abs
Get the absolute value.
Syntax:
{ "abs": value }
Arguments:
value- Number to get absolute value of
Returns: Absolute (positive) value.
Examples:
{ "abs": -5 }
// Result: 5
{ "abs": 5 }
// Result: 5
{ "abs": -3.14 }
// Result: 3.14
{ "abs": 0 }
// Result: 0
// Distance between two points
{ "abs": { "-": [{ "var": "a" }, { "var": "b" }] } }
// Data: { "a": 3, "b": 10 }
// Result: 7
Try it:
ceil
Round up to the nearest integer.
Syntax:
{ "ceil": value }
Arguments:
value- Number to round up
Returns: Smallest integer greater than or equal to value.
Examples:
{ "ceil": 4.1 }
// Result: 5
{ "ceil": 4.9 }
// Result: 5
{ "ceil": 4.0 }
// Result: 4
{ "ceil": -4.1 }
// Result: -4
// Round up to whole units
{ "ceil": { "/": [{ "var": "items" }, 10] } }
// Data: { "items": 25 }
// Result: 3 (need 3 boxes of 10)
Try it:
floor
Round down to the nearest integer.
Syntax:
{ "floor": value }
Arguments:
value- Number to round down
Returns: Largest integer less than or equal to value.
Examples:
{ "floor": 4.9 }
// Result: 4
{ "floor": 4.1 }
// Result: 4
{ "floor": 4.0 }
// Result: 4
{ "floor": -4.1 }
// Result: -5
// Truncate decimal
{ "floor": { "var": "amount" } }
// Data: { "amount": 99.99 }
// Result: 99
Try it:
Control Flow Operators
Conditional branching and value selection operators.
if
Conditional branching with if/then/else chains.
Syntax:
{ "if": [condition, then_value] }
{ "if": [condition, then_value, else_value] }
{ "if": [cond1, value1, cond2, value2, ..., else_value] }
Arguments:
condition- Condition to evaluatethen_value- Value if condition is truthyelse_value- Value if condition is falsy (optional)- Additional condition/value pairs for else-if chains
Returns: The value corresponding to the first truthy condition, or the else value.
Examples:
// Simple if/then
{ "if": [true, "yes"] }
// Result: "yes"
{ "if": [false, "yes"] }
// Result: null
// If/then/else
{ "if": [true, "yes", "no"] }
// Result: "yes"
{ "if": [false, "yes", "no"] }
// Result: "no"
// If/else-if/else chain
{ "if": [
{ ">=": [{ "var": "score" }, 90] }, "A",
{ ">=": [{ "var": "score" }, 80] }, "B",
{ ">=": [{ "var": "score" }, 70] }, "C",
{ ">=": [{ "var": "score" }, 60] }, "D",
"F"
]}
// Data: { "score": 85 }
// Result: "B"
// Nested if
{ "if": [
{ "var": "premium" },
{ "if": [
{ ">": [{ "var": "amount" }, 100] },
"free_shipping",
"standard_shipping"
]},
"no_shipping"
]}
// Data: { "premium": true, "amount": 150 }
// Result: "free_shipping"
Try it:
Notes:
- Only evaluates the matching branch (lazy evaluation)
- Empty condition list returns
null - Odd number of arguments uses last as else value
?: (Ternary)
Ternary conditional operator (shorthand if/then/else).
Syntax:
{ "?:": [condition, then_value, else_value] }
Arguments:
condition- Condition to evaluatethen_value- Value if condition is truthyelse_value- Value if condition is falsy
Returns: then_value if condition is truthy, else_value otherwise.
Examples:
// Basic ternary
{ "?:": [true, "yes", "no"] }
// Result: "yes"
{ "?:": [false, "yes", "no"] }
// Result: "no"
// With comparison
{ "?:": [
{ ">": [{ "var": "age" }, 18] },
"adult",
"minor"
]}
// Data: { "age": 21 }
// Result: "adult"
// Nested ternary
{ "?:": [
{ "var": "vip" },
0,
{ "?:": [
{ ">": [{ "var": "total" }, 50] },
5,
10
]}
]}
// Data: { "vip": false, "total": 75 }
// Result: 5 (shipping cost)
Try it:
Notes:
- Equivalent to
{ "if": [condition, then_value, else_value] } - More concise for simple conditions
- Only evaluates the matching branch
?? (Null Coalesce)
Return the first non-null value.
Syntax:
{ "??": [a, b] }
{ "??": [a, b, c, ...] }
Arguments:
a,b, … - Values to check (variadic)
Returns: The first non-null value, or null if all are null.
Examples:
// First is not null
{ "??": ["hello", "default"] }
// Result: "hello"
// First is null
{ "??": [null, "default"] }
// Result: "default"
// Multiple values
{ "??": [null, null, "found"] }
// Result: "found"
// All null
{ "??": [null, null] }
// Result: null
// With variables (default value pattern)
{ "??": [{ "var": "nickname" }, { "var": "name" }, "Anonymous"] }
// Data: { "name": "Alice" }
// Result: "Alice"
// Note: 0, "", and false are NOT null
{ "??": [0, "default"] }
// Result: 0
{ "??": ["", "default"] }
// Result: ""
{ "??": [false, "default"] }
// Result: false
Try it:
Notes:
- Only checks for
null, not other falsy values - Use
orif you want to skip all falsy values - Short-circuits: stops at first non-null value
Comparison: if vs ?: vs ?? vs or
| Operator | Use Case | Falsy Handling |
|---|---|---|
if | Complex branching, multiple conditions | Evaluates truthiness |
?: | Simple if/else | Evaluates truthiness |
?? | Default for null only | Only skips null |
or | Default for any falsy | Skips all falsy values |
Examples:
// Value is 0 (falsy but not null)
// Data: { "count": 0 }
{ "if": [{ "var": "count" }, { "var": "count" }, 10] }
// Result: 10 (0 is falsy)
{ "?:": [{ "var": "count" }, { "var": "count" }, 10] }
// Result: 10 (0 is falsy)
{ "??": [{ "var": "count" }, 10] }
// Result: 0 (0 is not null)
{ "or": [{ "var": "count" }, 10] }
// Result: 10 (0 is falsy)
Choose the operator based on whether you want to treat 0, "", and false as valid values.
String Operators
String manipulation and searching operations.
cat
Concatenate strings together.
Syntax:
{ "cat": [a, b, ...] }
Arguments:
a,b, … - Values to concatenate (variadic)
Returns: Concatenated string.
Examples:
// Simple concatenation
{ "cat": ["Hello", " ", "World"] }
// Result: "Hello World"
// With variables
{ "cat": ["Hello, ", { "var": "name" }, "!"] }
// Data: { "name": "Alice" }
// Result: "Hello, Alice!"
// Non-strings are converted
{ "cat": ["Value: ", 42] }
// Result: "Value: 42"
{ "cat": ["Is active: ", true] }
// Result: "Is active: true"
// Building paths
{ "cat": ["/users/", { "var": "userId" }, "/profile"] }
// Data: { "userId": 123 }
// Result: "/users/123/profile"
Try it:
substr
Extract a substring.
Syntax:
{ "substr": [string, start] }
{ "substr": [string, start, length] }
Arguments:
string- Source stringstart- Starting index (0-based, negative counts from end)length- Number of characters (optional, negative counts from end)
Returns: Extracted substring.
Examples:
// From start index
{ "substr": ["Hello World", 0, 5] }
// Result: "Hello"
// From middle
{ "substr": ["Hello World", 6] }
// Result: "World"
// Negative start (from end)
{ "substr": ["Hello World", -5] }
// Result: "World"
// Negative length (exclude from end)
{ "substr": ["Hello World", 0, -6] }
// Result: "Hello"
// Get file extension
{ "substr": ["document.pdf", -3] }
// Result: "pdf"
// With variables
{ "substr": [{ "var": "text" }, 0, 10] }
// Data: { "text": "This is a long string" }
// Result: "This is a "
Try it:
in
Check if a value is contained in a string or array.
Syntax:
{ "in": [needle, haystack] }
Arguments:
needle- Value to search forhaystack- String or array to search in
Returns: true if found, false otherwise.
Examples:
// String contains substring
{ "in": ["World", "Hello World"] }
// Result: true
{ "in": ["xyz", "Hello World"] }
// Result: false
// Array contains element
{ "in": [2, [1, 2, 3]] }
// Result: true
{ "in": [5, [1, 2, 3]] }
// Result: false
// Check membership
{ "in": [{ "var": "role" }, ["admin", "moderator"]] }
// Data: { "role": "admin" }
// Result: true
// Check substring
{ "in": ["@", { "var": "email" }] }
// Data: { "email": "user@example.com" }
// Result: true
Try it:
length
Get the length of a string or array.
Syntax:
{ "length": value }
Arguments:
value- String or array
Returns: Length (number of characters or elements).
Examples:
// String length
{ "length": "Hello" }
// Result: 5
// Array length
{ "length": [1, 2, 3, 4, 5] }
// Result: 5
// Empty values
{ "length": "" }
// Result: 0
{ "length": [] }
// Result: 0
// With variables
{ "length": { "var": "items" } }
// Data: { "items": ["a", "b", "c"] }
// Result: 3
// Check minimum length
{ ">=": [{ "length": { "var": "password" } }, 8] }
// Data: { "password": "secret123" }
// Result: true
Try it:
starts_with
Check if a string starts with a prefix.
Syntax:
{ "starts_with": [string, prefix] }
Arguments:
string- String to checkprefix- Prefix to look for
Returns: true if string starts with prefix, false otherwise.
Examples:
{ "starts_with": ["Hello World", "Hello"] }
// Result: true
{ "starts_with": ["Hello World", "World"] }
// Result: false
// Check URL scheme
{ "starts_with": [{ "var": "url" }, "https://"] }
// Data: { "url": "https://example.com" }
// Result: true
// Case sensitive
{ "starts_with": ["Hello", "hello"] }
// Result: false
Try it:
ends_with
Check if a string ends with a suffix.
Syntax:
{ "ends_with": [string, suffix] }
Arguments:
string- String to checksuffix- Suffix to look for
Returns: true if string ends with suffix, false otherwise.
Examples:
{ "ends_with": ["Hello World", "World"] }
// Result: true
{ "ends_with": ["Hello World", "Hello"] }
// Result: false
// Check file extension
{ "ends_with": [{ "var": "filename" }, ".pdf"] }
// Data: { "filename": "report.pdf" }
// Result: true
// Case sensitive
{ "ends_with": ["test.PDF", ".pdf"] }
// Result: false
Try it:
upper
Convert string to uppercase.
Syntax:
{ "upper": string }
Arguments:
string- String to convert
Returns: Uppercase string.
Examples:
{ "upper": "hello" }
// Result: "HELLO"
{ "upper": "Hello World" }
// Result: "HELLO WORLD"
// With variable
{ "upper": { "var": "name" } }
// Data: { "name": "alice" }
// Result: "ALICE"
Try it:
lower
Convert string to lowercase.
Syntax:
{ "lower": string }
Arguments:
string- String to convert
Returns: Lowercase string.
Examples:
{ "lower": "HELLO" }
// Result: "hello"
{ "lower": "Hello World" }
// Result: "hello world"
// Case-insensitive comparison
{ "==": [
{ "lower": { "var": "input" } },
"yes"
]}
// Data: { "input": "YES" }
// Result: true
Try it:
trim
Remove leading and trailing whitespace.
Syntax:
{ "trim": string }
Arguments:
string- String to trim
Returns: String with whitespace removed from both ends.
Examples:
{ "trim": " hello " }
// Result: "hello"
{ "trim": "\n\ttext\n\t" }
// Result: "text"
// Clean user input
{ "trim": { "var": "userInput" } }
// Data: { "userInput": " search query " }
// Result: "search query"
Try it:
split
Split a string into an array.
Syntax:
{ "split": [string, delimiter] }
Arguments:
string- String to splitdelimiter- Delimiter to split on
Returns: Array of substrings.
Examples:
// Split by space
{ "split": ["Hello World", " "] }
// Result: ["Hello", "World"]
// Split by comma
{ "split": ["a,b,c", ","] }
// Result: ["a", "b", "c"]
// Split by empty string (characters)
{ "split": ["abc", ""] }
// Result: ["a", "b", "c"]
// Parse CSV-like data
{ "split": [{ "var": "tags" }, ","] }
// Data: { "tags": "rust,json,logic" }
// Result: ["rust", "json", "logic"]
// Get first part
{ "var": "0" }
// Applied to: { "split": ["user@example.com", "@"] }
// Result: "user"
Try it:
Array Operators
Operations for working with arrays, including iteration and transformation.
merge
Merge multiple arrays into one.
Syntax:
{ "merge": [array1, array2, ...] }
Arguments:
array1,array2, … - Arrays to merge
Returns: Single flattened array.
Examples:
// Merge two arrays
{ "merge": [[1, 2], [3, 4]] }
// Result: [1, 2, 3, 4]
// Merge multiple
{ "merge": [[1], [2], [3]] }
// Result: [1, 2, 3]
// Non-arrays are wrapped
{ "merge": [[1, 2], 3, [4, 5]] }
// Result: [1, 2, 3, 4, 5]
// With variables
{ "merge": [{ "var": "arr1" }, { "var": "arr2" }] }
// Data: { "arr1": [1, 2], "arr2": [3, 4] }
// Result: [1, 2, 3, 4]
Try it:
filter
Filter array elements based on a condition.
Syntax:
{ "filter": [array, condition] }
Arguments:
array- Array to filtercondition- Condition applied to each element (use{"var": ""}for current element)
Returns: Array of elements where condition is truthy.
Examples:
// Filter numbers greater than 2
{ "filter": [
[1, 2, 3, 4, 5],
{ ">": [{ "var": "" }, 2] }
]}
// Result: [3, 4, 5]
// Filter even numbers
{ "filter": [
[1, 2, 3, 4, 5, 6],
{ "==": [{ "%": [{ "var": "" }, 2] }, 0] }
]}
// Result: [2, 4, 6]
// Filter objects by property
{ "filter": [
{ "var": "users" },
{ "==": [{ "var": "active" }, true] }
]}
// Data: {
// "users": [
// { "name": "Alice", "active": true },
// { "name": "Bob", "active": false },
// { "name": "Carol", "active": true }
// ]
// }
// Result: [{ "name": "Alice", "active": true }, { "name": "Carol", "active": true }]
// Filter with multiple conditions
{ "filter": [
{ "var": "products" },
{ "and": [
{ ">": [{ "var": "price" }, 10] },
{ "var": "inStock" }
]}
]}
Try it:
Notes:
- Inside the condition,
{"var": ""}refers to the current element - The original array is not modified
map
Transform each element of an array.
Syntax:
{ "map": [array, transformation] }
Arguments:
array- Array to transformtransformation- Operation applied to each element
Returns: Array of transformed elements.
Examples:
// Double each number
{ "map": [
[1, 2, 3],
{ "*": [{ "var": "" }, 2] }
]}
// Result: [2, 4, 6]
// Extract property from objects
{ "map": [
{ "var": "users" },
{ "var": "name" }
]}
// Data: {
// "users": [
// { "name": "Alice", "age": 30 },
// { "name": "Bob", "age": 25 }
// ]
// }
// Result: ["Alice", "Bob"]
// Create new objects
{ "map": [
{ "var": "items" },
{ "cat": ["Item: ", { "var": "name" }] }
]}
// Data: { "items": [{ "name": "A" }, { "name": "B" }] }
// Result: ["Item: A", "Item: B"]
// Square numbers
{ "map": [
[1, 2, 3, 4],
{ "*": [{ "var": "" }, { "var": "" }] }
]}
// Result: [1, 4, 9, 16]
Try it:
reduce
Reduce an array to a single value.
Syntax:
{ "reduce": [array, reducer, initial] }
Arguments:
array- Array to reducereducer- Operation combining accumulator and current elementinitial- Initial value for accumulator
Returns: Final accumulated value.
Context Variables:
{"var": "current"}- Current element{"var": "accumulator"}- Current accumulated value
Examples:
// Sum all numbers
{ "reduce": [
[1, 2, 3, 4, 5],
{ "+": [{ "var": "accumulator" }, { "var": "current" }] },
0
]}
// Result: 15
// Product of all numbers
{ "reduce": [
[1, 2, 3, 4],
{ "*": [{ "var": "accumulator" }, { "var": "current" }] },
1
]}
// Result: 24
// Concatenate strings
{ "reduce": [
["a", "b", "c"],
{ "cat": [{ "var": "accumulator" }, { "var": "current" }] },
""
]}
// Result: "abc"
// Find maximum
{ "reduce": [
[3, 1, 4, 1, 5, 9],
{ "if": [
{ ">": [{ "var": "current" }, { "var": "accumulator" }] },
{ "var": "current" },
{ "var": "accumulator" }
]},
0
]}
// Result: 9
// Count elements matching condition
{ "reduce": [
[1, 2, 3, 4, 5, 6],
{ "+": [
{ "var": "accumulator" },
{ "if": [{ ">": [{ "var": "current" }, 3] }, 1, 0] }
]},
0
]}
// Result: 3 (count of numbers > 3)
Try it:
all
Check if all elements satisfy a condition.
Syntax:
{ "all": [array, condition] }
Arguments:
array- Array to checkcondition- Condition applied to each element
Returns: true if all elements satisfy condition, false otherwise.
Examples:
// All positive
{ "all": [
[1, 2, 3],
{ ">": [{ "var": "" }, 0] }
]}
// Result: true
// All greater than 5
{ "all": [
[1, 2, 3],
{ ">": [{ "var": "" }, 5] }
]}
// Result: false
// All users active
{ "all": [
{ "var": "users" },
{ "var": "active" }
]}
// Data: { "users": [{ "active": true }, { "active": true }] }
// Result: true
// Empty array returns true (vacuous truth)
{ "all": [[], { ">": [{ "var": "" }, 0] }] }
// Result: true
Try it:
some
Check if any element satisfies a condition.
Syntax:
{ "some": [array, condition] }
Arguments:
array- Array to checkcondition- Condition applied to each element
Returns: true if at least one element satisfies condition, false otherwise.
Examples:
// Any negative
{ "some": [
[1, -2, 3],
{ "<": [{ "var": "" }, 0] }
]}
// Result: true
// Any greater than 10
{ "some": [
[1, 2, 3],
{ ">": [{ "var": "" }, 10] }
]}
// Result: false
// Any admin user
{ "some": [
{ "var": "users" },
{ "==": [{ "var": "role" }, "admin"] }
]}
// Data: {
// "users": [
// { "role": "user" },
// { "role": "admin" }
// ]
// }
// Result: true
// Empty array returns false
{ "some": [[], { ">": [{ "var": "" }, 0] }] }
// Result: false
Try it:
none
Check if no elements satisfy a condition.
Syntax:
{ "none": [array, condition] }
Arguments:
array- Array to checkcondition- Condition applied to each element
Returns: true if no elements satisfy condition, false otherwise.
Examples:
// None negative
{ "none": [
[1, 2, 3],
{ "<": [{ "var": "" }, 0] }
]}
// Result: true
// None greater than 0
{ "none": [
[1, 2, 3],
{ ">": [{ "var": "" }, 0] }
]}
// Result: false
// No banned users
{ "none": [
{ "var": "users" },
{ "var": "banned" }
]}
// Data: { "users": [{ "banned": false }, { "banned": false }] }
// Result: true
// Empty array returns true
{ "none": [[], { ">": [{ "var": "" }, 0] }] }
// Result: true
Try it:
sort
Sort an array.
Syntax:
{ "sort": array }
{ "sort": [array] }
{ "sort": [array, comparator] }
Arguments:
array- Array to sortcomparator- Optional comparison logic
Returns: Sorted array.
Examples:
// Sort numbers
{ "sort": [[3, 1, 4, 1, 5, 9]] }
// Result: [1, 1, 3, 4, 5, 9]
// Sort strings
{ "sort": [["banana", "apple", "cherry"]] }
// Result: ["apple", "banana", "cherry"]
// Sort with custom comparator
{ "sort": [
{ "var": "items" },
{ "-": [{ "var": "a.price" }, { "var": "b.price" }] }
]}
// Data: {
// "items": [
// { "name": "B", "price": 20 },
// { "name": "A", "price": 10 }
// ]
// }
// Result: [{ "name": "A", "price": 10 }, { "name": "B", "price": 20 }]
Try it:
slice
Extract a portion of an array.
Syntax:
{ "slice": [array, start] }
{ "slice": [array, start, end] }
Arguments:
array- Source arraystart- Starting index (negative counts from end)end- Ending index, exclusive (optional, negative counts from end)
Returns: Array slice.
Examples:
// From index 2 to end
{ "slice": [[1, 2, 3, 4, 5], 2] }
// Result: [3, 4, 5]
// From index 1 to 3
{ "slice": [[1, 2, 3, 4, 5], 1, 3] }
// Result: [2, 3]
// Last 2 elements
{ "slice": [[1, 2, 3, 4, 5], -2] }
// Result: [4, 5]
// First 3 elements
{ "slice": [[1, 2, 3, 4, 5], 0, 3] }
// Result: [1, 2, 3]
// Pagination
{ "slice": [
{ "var": "items" },
{ "*": [{ "var": "page" }, 10] },
{ "+": [{ "*": [{ "var": "page" }, 10] }, 10] }
]}
// Data: { "items": [...], "page": 0 }
// Result: first 10 items
Try it:
DateTime Operators
Operations for working with dates and times.
now
Get the current timestamp.
Syntax:
{ "now": [] }
Arguments: None
Returns: Current Unix timestamp in milliseconds.
Examples:
{ "now": [] }
// Result: 1704067200000 (example timestamp)
// Check if date is in the future
{ ">": [{ "var": "expiresAt" }, { "now": [] }] }
// Data: { "expiresAt": 1735689600000 }
// Result: true or false depending on current time
// Calculate age in days
{ "/": [
{ "-": [{ "now": [] }, { "var": "createdAt" }] },
86400000
]}
// 86400000 = milliseconds in a day
Try it:
Notes:
- Returns milliseconds since Unix epoch (January 1, 1970)
- Useful for time-based conditions and calculations
datetime
Parse or create a datetime value.
Syntax:
{ "datetime": value }
{ "datetime": [value, format] }
Arguments:
value- Timestamp (number) or date stringformat- Optional format string for parsing
Returns: Datetime value (as timestamp or formatted string depending on usage).
Examples:
// From timestamp
{ "datetime": 1704067200000 }
// Result: datetime object
// From ISO string
{ "datetime": "2024-01-01T00:00:00Z" }
// Result: datetime object
// Parse with format
{ "datetime": ["01/15/2024", "%m/%d/%Y"] }
// Result: datetime object for January 15, 2024
timestamp
Convert a datetime to Unix timestamp.
Syntax:
{ "timestamp": datetime }
Arguments:
datetime- Datetime value or ISO string
Returns: Unix timestamp in milliseconds.
Examples:
// From ISO string
{ "timestamp": "2024-01-01T00:00:00Z" }
// Result: 1704067200000
// With variable
{ "timestamp": { "var": "date" } }
// Data: { "date": "2024-06-15T12:00:00Z" }
// Result: 1718452800000
Try it:
parse_date
Parse a date string into a datetime.
Syntax:
{ "parse_date": [string, format] }
Arguments:
string- Date string to parseformat- Format string (strftime-style)
Returns: Parsed datetime.
Format Specifiers:
%Y- 4-digit year (2024)%m- Month (01-12)%d- Day (01-31)%H- Hour 24h (00-23)%M- Minute (00-59)%S- Second (00-59)%y- 2-digit year (24)%b- Abbreviated month (Jan)%B- Full month (January)
Examples:
// Parse US date format
{ "parse_date": ["12/25/2024", "%m/%d/%Y"] }
// Result: datetime for December 25, 2024
// Parse European format
{ "parse_date": ["25-12-2024", "%d-%m-%Y"] }
// Result: datetime for December 25, 2024
// Parse with time
{ "parse_date": ["2024-01-15 14:30:00", "%Y-%m-%d %H:%M:%S"] }
// Result: datetime for January 15, 2024 at 2:30 PM
// With variable
{ "parse_date": [{ "var": "dateStr" }, "%Y-%m-%d"] }
// Data: { "dateStr": "2024-06-15" }
// Result: datetime for June 15, 2024
format_date
Format a datetime as a string.
Syntax:
{ "format_date": [datetime, format] }
Arguments:
datetime- Datetime value to formatformat- Format string (strftime-style)
Returns: Formatted date string.
Examples:
// Format as ISO date
{ "format_date": [{ "now": [] }, "%Y-%m-%d"] }
// Result: "2024-01-15"
// Format as US date
{ "format_date": [{ "var": "date" }, "%m/%d/%Y"] }
// Data: { "date": "2024-12-25T00:00:00Z" }
// Result: "12/25/2024"
// Format with time
{ "format_date": [{ "now": [] }, "%Y-%m-%d %H:%M:%S"] }
// Result: "2024-01-15 14:30:00"
// Human-readable format
{ "format_date": [{ "var": "date" }, "%B %d, %Y"] }
// Data: { "date": "2024-01-15T00:00:00Z" }
// Result: "January 15, 2024"
// Just time
{ "format_date": [{ "now": [] }, "%H:%M"] }
// Result: "14:30"
date_diff
Calculate the difference between two dates.
Syntax:
{ "date_diff": [date1, date2, unit] }
Arguments:
date1- First datetimedate2- Second datetimeunit- Unit of measurement: “days”, “hours”, “minutes”, “seconds”, “milliseconds”
Returns: Difference as a number in the specified unit.
Examples:
// Days between dates
{ "date_diff": [
"2024-12-31T00:00:00Z",
"2024-01-01T00:00:00Z",
"days"
]}
// Result: 365
// Hours difference
{ "date_diff": [
{ "var": "end" },
{ "var": "start" },
"hours"
]}
// Data: {
// "start": "2024-01-01T00:00:00Z",
// "end": "2024-01-01T12:00:00Z"
// }
// Result: 12
// Check if within 24 hours
{ "<": [
{ "date_diff": [{ "now": [] }, { "var": "timestamp" }, "hours"] },
24
]}
// Data: { "timestamp": "2024-01-15T10:00:00Z" }
// Result: true or false
// Calculate age in years (approximate)
{ "floor": {
"/": [
{ "date_diff": [{ "now": [] }, { "var": "birthdate" }, "days"] },
365.25
]
}}
// Data: { "birthdate": "1990-01-15T00:00:00Z" }
// Result: age in years
DateTime Patterns
Check if date is in the past
{ "<": [{ "var": "date" }, { "now": [] }] }
Check if date is in the future
{ ">": [{ "var": "date" }, { "now": [] }] }
Check if within time window
{ "and": [
{ ">=": [{ "now": [] }, { "var": "startTime" }] },
{ "<=": [{ "now": [] }, { "var": "endTime" }] }
]}
Calculate expiration
{ "+": [
{ "var": "createdAt" },
{ "*": [{ "var": "ttlDays" }, 86400000] }
]}
// Returns expiration timestamp
Missing Value Operators
Operators for checking if data fields are missing or undefined.
missing
Check for missing fields in the data.
Syntax:
{ "missing": [key1, key2, ...] }
{ "missing": key }
Arguments:
key1,key2, … - Field names to check
Returns: Array of missing field names.
Examples:
// Check single field
{ "missing": "name" }
// Data: { "age": 25 }
// Result: ["name"]
// Check multiple fields
{ "missing": ["name", "email", "phone"] }
// Data: { "name": "Alice", "phone": "555-1234" }
// Result: ["email"]
// All fields present
{ "missing": ["name", "age"] }
// Data: { "name": "Alice", "age": 25 }
// Result: []
// All fields missing
{ "missing": ["name", "age"] }
// Data: {}
// Result: ["name", "age"]
// Nested fields
{ "missing": ["user.name", "user.email"] }
// Data: { "user": { "name": "Alice" } }
// Result: ["user.email"]
Common Patterns
Require all fields:
{ "!": { "missing": ["name", "email", "password"] } }
// Returns true only if all fields are present
Check if any field is missing:
{ "!!": { "missing": ["name", "email"] } }
// Returns true if ANY field is missing
Conditional validation:
{ "if": [
{ "missing": ["required_field"] },
{ "throw": "Missing required field" },
"ok"
]}
Try it:
missing_some
Check if at least N fields are missing from a set.
Syntax:
{ "missing_some": [minimum, [key1, key2, ...]] }
Arguments:
minimum- Minimum number of fields that should be present[key1, key2, ...]- Array of field names to check
Returns: Array of missing field names if fewer than minimum are present, empty array otherwise.
Examples:
// Need at least 1 of these contact methods
{ "missing_some": [1, ["email", "phone", "address"]] }
// Data: { "email": "a@b.com" }
// Result: [] (1 present, requirement met)
// Data: {}
// Result: ["email", "phone", "address"] (0 present, need at least 1)
// Need at least 2 of these
{ "missing_some": [2, ["name", "email", "phone"]] }
// Data: { "name": "Alice" }
// Result: ["email", "phone"] (only 1 present, need 2)
// Data: { "name": "Alice", "email": "a@b.com" }
// Result: [] (2 present, requirement met)
// Data: { "name": "Alice", "email": "a@b.com", "phone": "555" }
// Result: [] (3 present, exceeds requirement)
Common Patterns
Require at least one contact method:
{ "!": { "missing_some": [1, ["email", "phone", "fax"]] } }
// Returns true if at least one contact method is provided
Flexible field requirements:
{ "if": [
{ "missing_some": [2, ["street", "city", "zip", "country"]] },
"Please provide at least 2 address fields",
"Address accepted"
]}
Require majority of fields:
{ "!": { "missing_some": [3, ["field1", "field2", "field3", "field4", "field5"]] } }
// Returns true if at least 3 of 5 fields are present
Try it:
Comparison: missing vs missing_some
| Scenario | missing | missing_some |
|---|---|---|
| All fields required | { "!": { "missing": [...] } } | N/A |
| At least N required | Complex logic needed | { "!": { "missing_some": [N, [...]] } } |
| Check which are missing | Returns missing list | Returns missing list if < N present |
| No minimum | Appropriate | Use with minimum=1 |
Integration with Validation
Form validation example:
{ "if": [
{ "missing": ["username", "password"] },
{ "throw": { "code": "VALIDATION_ERROR", "missing": { "missing": ["username", "password"] } } },
{ "if": [
{ "missing_some": [1, ["email", "phone"]] },
{ "throw": { "code": "CONTACT_REQUIRED", "message": "Provide email or phone" } },
"valid"
]}
]}
Conditional field requirements:
// If business account, require company name
{ "if": [
{ "==": [{ "var": "accountType" }, "business"] },
{ "!": { "missing": ["companyName", "taxId"] } },
true
]}
Error Handling Operators
Operators for throwing and catching errors, providing exception-like error handling in JSONLogic.
try
Catch errors and provide fallback values.
Syntax:
{ "try": [expression, fallback] }
{ "try": [expression, catch_expression] }
Arguments:
expression- Expression that might throw an errorfallback- Value or expression to use if an error occurs
Returns: Result of expression if successful, or fallback value/expression result if an error occurs.
Context in Catch:
When an error is caught, the catch expression can access error details via var:
{ "var": "message" }- Error message{ "var": "code" }- Error code (if thrown with one){ "var": "" }- Entire error object
Examples:
// Simple fallback value
{ "try": [
{ "/": [10, 0] },
0
]}
// Result: 0 (division by zero caught)
// Expression that succeeds
{ "try": [
{ "+": [1, 2] },
0
]}
// Result: 3 (no error, normal result)
// Catch with error access
{ "try": [
{ "throw": { "code": "NOT_FOUND", "message": "User not found" } },
{ "cat": ["Error: ", { "var": "message" }] }
]}
// Result: "Error: User not found"
// Access error code
{ "try": [
{ "throw": { "code": 404 } },
{ "var": "code" }
]}
// Result: 404
// Nested try for multiple error sources
{ "try": [
{ "try": [
{ "var": "data.nested.value" },
{ "throw": "nested access failed" }
]},
"default"
]}
Common Patterns
Safe division:
{ "try": [
{ "/": [{ "var": "numerator" }, { "var": "denominator" }] },
0
]}
Safe property access:
{ "try": [
{ "var": "user.profile.settings.theme" },
"default-theme"
]}
Error logging pattern:
{ "try": [
{ "risky_operation": [] },
{ "cat": ["Operation failed: ", { "var": "message" }] }
]}
Try it:
throw
Throw an error with optional details.
Syntax:
{ "throw": message }
{ "throw": { "code": code, "message": message, ...} }
Arguments:
message- Error message string, or- Error object with
code,message, and additional properties
Returns: Never returns normally; throws an error that must be caught by try.
Examples:
// Simple string error
{ "throw": "Something went wrong" }
// Throws error with message "Something went wrong"
// Error with code
{ "throw": { "code": "INVALID_INPUT", "message": "Age must be positive" } }
// Throws error with code and message
// Error with additional data
{ "throw": {
"code": "VALIDATION_ERROR",
"message": "Invalid email format",
"field": "email",
"value": { "var": "email" }
}}
// Throws detailed error with context
// Conditional throw
{ "if": [
{ "<": [{ "var": "age" }, 0] },
{ "throw": { "code": "INVALID_AGE", "message": "Age cannot be negative" } },
{ "var": "age" }
]}
// Data: { "age": -5 }
// Throws error
// Data: { "age": 25 }
// Result: 25
Common Patterns
Validation with throw:
{ "if": [
{ "missing": ["name", "email"] },
{ "throw": {
"code": "MISSING_FIELDS",
"message": "Required fields missing",
"fields": { "missing": ["name", "email"] }
}},
"valid"
]}
Business rule enforcement:
{ "if": [
{ ">": [{ "var": "amount" }, { "var": "balance" }] },
{ "throw": {
"code": "INSUFFICIENT_FUNDS",
"message": "Amount exceeds balance",
"requested": { "var": "amount" },
"available": { "var": "balance" }
}},
{ "-": [{ "var": "balance" }, { "var": "amount" }] }
]}
Type validation:
{ "if": [
{ "!==": [{ "type": { "var": "value" } }, "number"] },
{ "throw": { "code": "TYPE_ERROR", "message": "Expected number" } },
{ "*": [{ "var": "value" }, 2] }
]}
Try it:
Error Handling Patterns
Graceful Degradation
{ "try": [
{ "var": "user.preferences.language" },
{ "try": [
{ "var": "defaults.language" },
"en"
]}
]}
// Try user preference, then defaults, then hardcoded "en"
Validation Pipeline
{ "try": [
{ "if": [
{ "!": { "var": "input" } },
{ "throw": { "code": "EMPTY", "message": "Input required" } },
{ "if": [
{ "<": [{ "length": { "var": "input" } }, 3] },
{ "throw": { "code": "TOO_SHORT", "message": "Minimum 3 characters" } },
{ "var": "input" }
]}
]},
{ "cat": ["Validation error: ", { "var": "message" }] }
]}
Error Recovery with Retry Logic
{ "try": [
{ "primary_operation": [] },
{ "try": [
{ "fallback_operation": [] },
"all operations failed"
]}
]}
Collecting All Errors
While JSONLogic doesn’t natively support collecting multiple errors, you can structure validations to report all issues:
{
"errors": { "filter": [
[
{ "if": [{ "missing": ["name"] }, "name is required", null] },
{ "if": [{ "missing": ["email"] }, "email is required", null] },
{ "if": [
{ "and": [
{ "!": { "missing": ["email"] } },
{ "!": { "in": ["@", { "var": "email" }] } }
]},
"invalid email format",
null
]}
],
{ "!==": [{ "var": "" }, null] }
]}
}
This returns an array of error messages for all validation failures.
Custom Operators
Extend datalogic-rs with your own operators to implement domain-specific logic.
Basic Custom Operator
Custom operators implement the Operator trait:
#![allow(unused)]
fn main() {
use datalogic_rs::{DataLogic, Operator, ContextStack, Evaluator, Result, Error};
use serde_json::{json, Value};
struct DoubleOperator;
impl Operator for DoubleOperator {
fn evaluate(
&self,
args: &[Value],
context: &mut ContextStack,
evaluator: &dyn Evaluator,
) -> Result<Value> {
// Arguments are unevaluated - must call evaluate() first!
let value = evaluator.evaluate(
args.first().unwrap_or(&Value::Null),
context
)?;
match value.as_f64() {
Some(n) => Ok(json!(n * 2.0)),
None => Err(Error::InvalidArguments("Expected number".to_string()))
}
}
}
}
Registering Custom Operators
Add custom operators to the engine before compiling rules:
#![allow(unused)]
fn main() {
let mut engine = DataLogic::new();
engine.add_operator("double".to_string(), Box::new(DoubleOperator));
// Now use it in rules
let rule = json!({ "double": 21 });
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({})).unwrap();
assert_eq!(result, json!(42.0));
}
Important: Evaluating Arguments
Arguments passed to custom operators are unevaluated. You must call evaluator.evaluate() to resolve them:
#![allow(unused)]
fn main() {
impl Operator for MyOperator {
fn evaluate(
&self,
args: &[Value],
context: &mut ContextStack,
evaluator: &dyn Evaluator,
) -> Result<Value> {
// WRONG: Using args directly
// let value = args[0].as_f64();
// CORRECT: Evaluate first
let value = evaluator.evaluate(&args[0], context)?;
let num = value.as_f64();
// Now work with the evaluated value
// ...
}
}
}
This allows your operator to work with both literals and expressions:
// Works with literals
{ "double": 21 }
// Also works with variables
{ "double": { "var": "x" } }
// And nested expressions
{ "double": { "+": [10, 5] } }
Example: Average Operator
An operator that calculates the average of numbers:
#![allow(unused)]
fn main() {
struct AverageOperator;
impl Operator for AverageOperator {
fn evaluate(
&self,
args: &[Value],
context: &mut ContextStack,
evaluator: &dyn Evaluator,
) -> Result<Value> {
// Evaluate the argument (should be an array)
let value = evaluator.evaluate(
args.first().unwrap_or(&Value::Null),
context
)?;
let arr = value.as_array()
.ok_or_else(|| Error::InvalidArguments("Expected array".to_string()))?;
if arr.is_empty() {
return Ok(Value::Null);
}
let sum: f64 = arr.iter()
.filter_map(|v| v.as_f64())
.sum();
let count = arr.len() as f64;
Ok(json!(sum / count))
}
}
// Usage
engine.add_operator("avg".to_string(), Box::new(AverageOperator));
let rule = json!({ "avg": { "var": "scores" } });
let compiled = engine.compile(&rule).unwrap();
let result = engine.evaluate_owned(&compiled, json!({
"scores": [80, 90, 85, 95]
})).unwrap();
assert_eq!(result, json!(87.5));
}
Example: Range Check Operator
An operator that checks if a value is within a range:
#![allow(unused)]
fn main() {
struct InRangeOperator;
impl Operator for InRangeOperator {
fn evaluate(
&self,
args: &[Value],
context: &mut ContextStack,
evaluator: &dyn Evaluator,
) -> Result<Value> {
if args.len() != 3 {
return Err(Error::InvalidArguments(
"inRange requires 3 arguments: value, min, max".to_string()
));
}
let value = evaluator.evaluate(&args[0], context)?
.as_f64()
.ok_or_else(|| Error::InvalidArguments("Expected number".to_string()))?;
let min = evaluator.evaluate(&args[1], context)?
.as_f64()
.ok_or_else(|| Error::InvalidArguments("Expected number".to_string()))?;
let max = evaluator.evaluate(&args[2], context)?
.as_f64()
.ok_or_else(|| Error::InvalidArguments("Expected number".to_string()))?;
Ok(json!(value >= min && value <= max))
}
}
// Usage
engine.add_operator("inRange".to_string(), Box::new(InRangeOperator));
let rule = json!({ "inRange": [{ "var": "age" }, 18, 65] });
}
Example: String Formatting Operator
#![allow(unused)]
fn main() {
struct FormatOperator;
impl Operator for FormatOperator {
fn evaluate(
&self,
args: &[Value],
context: &mut ContextStack,
evaluator: &dyn Evaluator,
) -> Result<Value> {
let template = evaluator.evaluate(
args.first().unwrap_or(&Value::Null),
context
)?;
let template_str = template.as_str()
.ok_or_else(|| Error::InvalidArguments("Expected string template".to_string()))?;
// Replace {0}, {1}, etc. with arguments
let mut result = template_str.to_string();
for (i, arg) in args.iter().skip(1).enumerate() {
let value = evaluator.evaluate(arg, context)?;
let value_str = match &value {
Value::String(s) => s.clone(),
v => v.to_string(),
};
result = result.replace(&format!("{{{}}}", i), &value_str);
}
Ok(json!(result))
}
}
// Usage
engine.add_operator("format".to_string(), Box::new(FormatOperator));
let rule = json!({
"format": ["Hello, {0}! You have {1} messages.", { "var": "name" }, { "var": "count" }]
});
// Data: { "name": "Alice", "count": 5 }
// Result: "Hello, Alice! You have 5 messages."
}
Thread Safety Requirements
Custom operators must be Send + Sync for thread-safe usage:
#![allow(unused)]
fn main() {
// This is automatically satisfied for most operators
struct MyOperator {
// Use Arc for shared state
config: Arc<Config>,
}
// For mutable state, use synchronization primitives
struct StatefulOperator {
counter: Arc<AtomicUsize>,
}
impl Operator for StatefulOperator {
fn evaluate(
&self,
args: &[Value],
context: &mut ContextStack,
evaluator: &dyn Evaluator,
) -> Result<Value> {
let count = self.counter.fetch_add(1, Ordering::SeqCst);
Ok(json!(count))
}
}
}
Error Handling
Return appropriate errors for invalid inputs:
#![allow(unused)]
fn main() {
impl Operator for MyOperator {
fn evaluate(
&self,
args: &[Value],
context: &mut ContextStack,
evaluator: &dyn Evaluator,
) -> Result<Value> {
// Check argument count
if args.is_empty() {
return Err(Error::InvalidArguments(
"myop requires at least one argument".to_string()
));
}
// Check argument types
let value = evaluator.evaluate(&args[0], context)?;
let num = value.as_f64().ok_or_else(|| {
Error::InvalidArguments(format!(
"Expected number, got {}",
value_type_name(&value)
))
})?;
// Business logic errors
if num < 0.0 {
return Err(Error::Custom(
"Value must be non-negative".to_string()
));
}
Ok(json!(num.sqrt()))
}
}
fn value_type_name(v: &Value) -> &'static str {
match v {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
}
Best Practices
- Always evaluate arguments before using them
- Validate argument count and types early
- Return meaningful error messages
- Keep operators focused - one responsibility per operator
- Document the expected syntax for each operator
- Use
Arcfor shared configuration to maintain thread safety - Test with both literals and expressions as arguments
Configuration
Customize evaluation behavior with EvaluationConfig.
Creating a Configured Engine
#![allow(unused)]
fn main() {
use datalogic_rs::{DataLogic, EvaluationConfig, NanHandling};
// Default configuration
let engine = DataLogic::new();
// Custom configuration
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::IgnoreValue);
let engine = DataLogic::with_config(config);
}
Configuration Options
NaN Handling
Control how non-numeric values are handled in arithmetic operations.
#![allow(unused)]
fn main() {
use datalogic_rs::{EvaluationConfig, NanHandling};
// Option 1: Throw an error (default)
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::ThrowError);
// Option 2: Ignore non-numeric values
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::IgnoreValue);
// Option 3: Coerce to zero
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::CoerceToZero);
}
Behavior comparison:
#![allow(unused)]
fn main() {
let rule = json!({ "+": [1, "text", 2] });
// NanHandling::ThrowError
// Result: Error
// NanHandling::IgnoreValue
// Result: 3 (ignores "text")
// NanHandling::CoerceToZero
// Result: 3 ("text" becomes 0)
}
Division by Zero
Control how division by zero is handled.
#![allow(unused)]
fn main() {
use datalogic_rs::{EvaluationConfig, DivisionByZero};
// Option 1: Return Infinity/-Infinity (default)
let config = EvaluationConfig::default()
.with_division_by_zero(DivisionByZero::ReturnBounds);
// Option 2: Throw an error
let config = EvaluationConfig::default()
.with_division_by_zero(DivisionByZero::ThrowError);
// Option 3: Return null
let config = EvaluationConfig::default()
.with_division_by_zero(DivisionByZero::ReturnNull);
}
Behavior comparison:
#![allow(unused)]
fn main() {
let rule = json!({ "/": [10, 0] });
// DivisionByZero::ReturnBounds
// Result: Infinity
// DivisionByZero::ThrowError
// Result: Error
// DivisionByZero::ReturnNull
// Result: null
}
Truthiness Evaluation
Control how values are evaluated for truthiness in boolean contexts.
#![allow(unused)]
fn main() {
use datalogic_rs::{EvaluationConfig, TruthyEvaluator};
use std::sync::Arc;
// Option 1: JavaScript-style (default)
let config = EvaluationConfig::default()
.with_truthy_evaluator(TruthyEvaluator::JavaScript);
// Option 2: Python-style
let config = EvaluationConfig::default()
.with_truthy_evaluator(TruthyEvaluator::Python);
// Option 3: Strict boolean (only true/false)
let config = EvaluationConfig::default()
.with_truthy_evaluator(TruthyEvaluator::StrictBoolean);
// Option 4: Custom evaluator
let custom = Arc::new(|value: &serde_json::Value| -> bool {
// Custom logic: only positive numbers are truthy
value.as_f64().map_or(false, |n| n > 0.0)
});
let config = EvaluationConfig::default()
.with_truthy_evaluator(TruthyEvaluator::Custom(custom));
}
Truthiness comparison:
| Value | JavaScript | Python | StrictBoolean |
|---|---|---|---|
true | truthy | truthy | truthy |
false | falsy | falsy | falsy |
1 | truthy | truthy | error/falsy |
0 | falsy | falsy | error/falsy |
"" | falsy | falsy | error/falsy |
"0" | truthy | truthy | error/falsy |
[] | falsy | falsy | error/falsy |
[0] | truthy | truthy | error/falsy |
null | falsy | falsy | error/falsy |
Loose Equality Errors
Control whether loose equality (==) throws errors for incompatible types.
#![allow(unused)]
fn main() {
let config = EvaluationConfig::default()
.with_loose_equality_throws_errors(true); // default
// or
let config = EvaluationConfig::default()
.with_loose_equality_throws_errors(false);
}
Configuration Presets
Safe Arithmetic
Ignores invalid values in arithmetic operations:
#![allow(unused)]
fn main() {
let engine = DataLogic::with_config(EvaluationConfig::safe_arithmetic());
// Equivalent to:
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::IgnoreValue)
.with_division_by_zero(DivisionByZero::ReturnNull);
}
Strict Mode
Throws errors for any type mismatches:
#![allow(unused)]
fn main() {
let engine = DataLogic::with_config(EvaluationConfig::strict());
// Equivalent to:
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::ThrowError)
.with_division_by_zero(DivisionByZero::ThrowError)
.with_loose_equality_throws_errors(true);
}
Combining with Structure Preservation
Use both configuration and structure preservation:
#![allow(unused)]
fn main() {
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::CoerceToZero);
let engine = DataLogic::with_config_and_structure(config, true);
}
Configuration Examples
Lenient Data Processing
For processing potentially messy data:
#![allow(unused)]
fn main() {
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::IgnoreValue)
.with_division_by_zero(DivisionByZero::ReturnNull);
let engine = DataLogic::with_config(config);
// This won't error even with bad data
let rule = json!({ "+": [1, "not a number", null, 2] });
let result = engine.evaluate_json(&rule.to_string(), "{}").unwrap();
// Result: 3 (ignores non-numeric values)
}
Strict Validation
For scenarios requiring precise type handling:
#![allow(unused)]
fn main() {
let config = EvaluationConfig::strict();
let engine = DataLogic::with_config(config);
// This will error on type mismatches
let rule = json!({ "+": [1, "2"] });
let result = engine.evaluate_json(&rule.to_string(), "{}");
// Result: Error (strict mode doesn't coerce "2" to number)
}
Custom Business Logic Truthiness
For domain-specific truth evaluation:
#![allow(unused)]
fn main() {
use std::sync::Arc;
// Only non-empty strings and positive numbers are truthy
let custom_truthy = Arc::new(|value: &serde_json::Value| -> bool {
match value {
serde_json::Value::Bool(b) => *b,
serde_json::Value::Number(n) => n.as_f64().map_or(false, |n| n > 0.0),
serde_json::Value::String(s) => !s.is_empty(),
_ => false,
}
});
let config = EvaluationConfig::default()
.with_truthy_evaluator(TruthyEvaluator::Custom(custom_truthy));
let engine = DataLogic::with_config(config);
// With this config:
// { "if": [0, "yes", "no"] } => "no" (0 is not positive)
// { "if": [-5, "yes", "no"] } => "no" (-5 is not positive)
// { "if": [1, "yes", "no"] } => "yes" (1 is positive)
}
Structured Objects (Templating)
Use JSONLogic as a templating engine with structure preservation mode.
Enabling Structure Preservation
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
// Enable structure preservation
let engine = DataLogic::with_preserve_structure();
// Or combine with configuration
let engine = DataLogic::with_config_and_structure(config, true);
}
How It Works
In normal mode, unknown keys in a JSON object are treated as errors (or custom operators). With structure preservation enabled, unknown keys become literal output fields.
Normal mode:
{ "user": { "var": "name" } }
// Error: "user" is not a known operator
Structure preservation mode:
{ "user": { "var": "name" } }
// Result: { "user": "Alice" }
// "user" is preserved as an output key
Basic Templating
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::with_preserve_structure();
let template = json!({
"greeting": { "cat": ["Hello, ", { "var": "name" }, "!"] },
"timestamp": { "now": [] },
"isAdmin": { "==": [{ "var": "role" }, "admin"] }
});
let compiled = engine.compile(&template).unwrap();
let result = engine.evaluate_owned(&compiled, json!({
"name": "Alice",
"role": "admin"
})).unwrap();
// Result:
// {
// "greeting": "Hello, Alice!",
// "timestamp": 1704067200000,
// "isAdmin": true
// }
}
Nested Structures
Structure preservation works at any depth:
#![allow(unused)]
fn main() {
let template = json!({
"user": {
"profile": {
"displayName": { "var": "firstName" },
"email": { "var": "userEmail" },
"verified": true
},
"settings": {
"theme": { "??": [{ "var": "preferredTheme" }, "light"] },
"notifications": { "var": "notificationsEnabled" }
}
},
"metadata": {
"generatedAt": { "now": [] },
"version": "1.0"
}
});
let result = engine.evaluate_owned(&compiled, json!({
"firstName": "Bob",
"userEmail": "bob@example.com",
"notificationsEnabled": true
})).unwrap();
// Result:
// {
// "user": {
// "profile": {
// "displayName": "Bob",
// "email": "bob@example.com",
// "verified": true
// },
// "settings": {
// "theme": "light",
// "notifications": true
// }
// },
// "metadata": {
// "generatedAt": 1704067200000,
// "version": "1.0"
// }
// }
}
Arrays in Templates
Arrays are processed element by element:
#![allow(unused)]
fn main() {
let template = json!({
"items": [
{ "name": "Item 1", "price": { "var": "price1" } },
{ "name": "Item 2", "price": { "var": "price2" } }
],
"total": { "+": [{ "var": "price1" }, { "var": "price2" }] }
});
let result = engine.evaluate_owned(&compiled, json!({
"price1": 10,
"price2": 20
})).unwrap();
// Result:
// {
// "items": [
// { "name": "Item 1", "price": 10 },
// { "name": "Item 2", "price": 20 }
// ],
// "total": 30
// }
}
Dynamic Arrays with Map
Generate arrays dynamically using map:
#![allow(unused)]
fn main() {
let template = json!({
"users": {
"map": [
{ "var": "userList" },
{
"id": { "var": "id" },
"name": { "var": "name" },
"isActive": { "var": "active" }
}
]
}
});
let result = engine.evaluate_owned(&compiled, json!({
"userList": [
{ "id": 1, "name": "Alice", "active": true },
{ "id": 2, "name": "Bob", "active": false }
]
})).unwrap();
// Result:
// {
// "users": [
// { "id": 1, "name": "Alice", "isActive": true },
// { "id": 2, "name": "Bob", "isActive": false }
// ]
// }
}
The preserve Operator
Use the preserve operator to explicitly preserve a value without evaluation:
#![allow(unused)]
fn main() {
let template = json!({
"data": { "var": "input" },
"literal": { "preserve": { "var": "this is literal" } }
});
// Result:
// {
// "data": <value of input>,
// "literal": { "var": "this is literal" }
// }
}
Use Cases
API Response Transformation
#![allow(unused)]
fn main() {
let template = json!({
"success": true,
"data": {
"user": {
"id": { "var": "userId" },
"profile": {
"name": { "cat": [{ "var": "firstName" }, " ", { "var": "lastName" }] },
"avatar": { "cat": ["https://cdn.example.com/", { "var": "avatarId" }, ".jpg"] }
}
}
},
"timestamp": { "now": [] }
});
}
Document Generation
#![allow(unused)]
fn main() {
let template = json!({
"invoice": {
"number": { "cat": ["INV-", { "var": "invoiceId" }] },
"date": { "format_date": [{ "now": [] }, "%Y-%m-%d"] },
"customer": {
"name": { "var": "customerName" },
"address": { "var": "customerAddress" }
},
"items": { "var": "lineItems" },
"total": {
"reduce": [
{ "var": "lineItems" },
{ "+": [{ "var": "accumulator" }, { "var": "current.amount" }] },
0
]
}
}
});
}
Configuration Templating
#![allow(unused)]
fn main() {
let template = json!({
"database": {
"host": { "??": [{ "var": "DB_HOST" }, "localhost"] },
"port": { "??": [{ "var": "DB_PORT" }, 5432] },
"name": { "var": "DB_NAME" },
"ssl": { "==": [{ "var": "ENV" }, "production"] }
},
"cache": {
"enabled": { "var": "CACHE_ENABLED" },
"ttl": { "if": [
{ "==": [{ "var": "ENV" }, "development"] },
60,
3600
]}
}
});
}
Dynamic Forms
#![allow(unused)]
fn main() {
let template = json!({
"form": {
"title": { "var": "formTitle" },
"fields": {
"map": [
{ "var": "fieldDefinitions" },
{
"name": { "var": "name" },
"type": { "var": "type" },
"required": { "var": "required" },
"label": { "cat": [{ "var": "name" }, { "if": [{ "var": "required" }, " *", ""] }] }
}
]
}
}
});
}
Mixing Operators and Structure
You can mix operators and structure freely:
#![allow(unused)]
fn main() {
let template = json!({
// Static structure
"type": "response",
"version": "2.0",
// Conditional structure
"status": { "if": [
{ "var": "success" },
"ok",
"error"
]},
// Dynamic content
"data": { "if": [
{ "var": "success" },
{
"result": { "var": "data" },
"count": { "length": { "var": "data" } }
},
{
"error": { "var": "errorMessage" },
"code": { "var": "errorCode" }
}
]}
});
}
Thread Safety
datalogic-rs is designed for thread-safe, concurrent evaluation.
Thread-Safe Design
CompiledLogic is Arc-wrapped
When you compile a rule, it’s automatically wrapped in Arc:
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::new();
let rule = json!({ ">": [{ "var": "x" }, 10] });
// compiled is Arc<CompiledLogic>
let compiled = engine.compile(&rule).unwrap();
// Clone is cheap (just increments reference count)
let compiled_clone = compiled.clone(); // or Arc::clone(&compiled)
}
Sharing Across Threads
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
use std::sync::Arc;
use std::thread;
let engine = Arc::new(DataLogic::new());
let rule = json!({ "*": [{ "var": "x" }, 2] });
let compiled = engine.compile(&rule).unwrap();
let handles: Vec<_> = (0..4).map(|i| {
let engine = Arc::clone(&engine);
let compiled = Arc::clone(&compiled);
thread::spawn(move || {
let result = engine.evaluate_owned(
&compiled,
json!({ "x": i })
).unwrap();
println!("Thread {}: {}", i, result);
})
}).collect();
for handle in handles {
handle.join().unwrap();
}
}
Async Runtime Integration
With Tokio
use datalogic_rs::DataLogic;
use serde_json::json;
use std::sync::Arc;
#[tokio::main]
async fn main() {
let engine = Arc::new(DataLogic::new());
let rule = json!({ "+": [{ "var": "a" }, { "var": "b" }] });
let compiled = engine.compile(&rule).unwrap();
// Spawn multiple async tasks
let tasks: Vec<_> = (0..10).map(|i| {
let engine = Arc::clone(&engine);
let compiled = Arc::clone(&compiled);
tokio::spawn(async move {
// Use spawn_blocking for CPU-bound evaluation
tokio::task::spawn_blocking(move || {
engine.evaluate_owned(&compiled, json!({ "a": i, "b": i * 2 }))
}).await.unwrap()
})
}).collect();
for task in tasks {
let result = task.await.unwrap().unwrap();
println!("Result: {}", result);
}
}
Evaluation is CPU-bound
Since evaluation is CPU-bound (not I/O), use spawn_blocking in async contexts:
#![allow(unused)]
fn main() {
async fn evaluate_rule(
engine: Arc<DataLogic>,
compiled: Arc<CompiledLogic>,
data: Value,
) -> Result<Value, Error> {
tokio::task::spawn_blocking(move || {
engine.evaluate_owned(&compiled, data)
}).await.unwrap()
}
}
Thread Pool Pattern
For high-throughput scenarios, use a thread pool:
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
use std::sync::Arc;
use rayon::prelude::*;
let engine = Arc::new(DataLogic::new());
let rule = json!({ "filter": [
{ "var": "items" },
{ ">": [{ "var": "value" }, 50] }
]});
let compiled = engine.compile(&rule).unwrap();
// Process many data sets in parallel
let datasets: Vec<Value> = (0..1000)
.map(|i| json!({
"items": (0..100).map(|j| json!({ "value": (i + j) % 100 })).collect::<Vec<_>>()
}))
.collect();
let results: Vec<_> = datasets
.par_iter() // Rayon parallel iterator
.map(|data| {
engine.evaluate(&compiled, data).unwrap()
})
.collect();
}
Shared Engine vs Per-Thread Engine
Shared Engine (Recommended)
Share one engine across threads when using the same custom operators:
#![allow(unused)]
fn main() {
use std::sync::Arc;
let mut engine = DataLogic::new();
engine.add_operator("custom".to_string(), Box::new(MyOperator));
let engine = Arc::new(engine);
// Share across threads
for _ in 0..4 {
let engine = Arc::clone(&engine);
thread::spawn(move || {
// Use shared engine
});
}
}
Per-Thread Engine
Create separate engines when you need thread-local state:
#![allow(unused)]
fn main() {
thread_local! {
static ENGINE: DataLogic = {
let mut engine = DataLogic::new();
// Thread-local configuration
engine
};
}
// Use in each thread
ENGINE.with(|engine| {
let compiled = engine.compile(&rule).unwrap();
engine.evaluate_owned(&compiled, data)
});
}
Custom Operator Thread Safety
Custom operators must implement Send + Sync:
#![allow(unused)]
fn main() {
use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};
// Thread-safe custom operator
struct CounterOperator {
counter: Arc<AtomicUsize>,
}
impl Operator for CounterOperator {
fn evaluate(
&self,
args: &[Value],
context: &mut ContextStack,
evaluator: &dyn Evaluator,
) -> Result<Value> {
let count = self.counter.fetch_add(1, Ordering::SeqCst);
Ok(json!(count))
}
}
// Create shared counter
let counter = Arc::new(AtomicUsize::new(0));
let mut engine = DataLogic::new();
engine.add_operator("count".to_string(), Box::new(CounterOperator {
counter: Arc::clone(&counter),
}));
}
Performance Considerations
Compile Once, Evaluate Many
#![allow(unused)]
fn main() {
// GOOD: Compile once
let compiled = engine.compile(&rule).unwrap();
for data in datasets {
engine.evaluate(&compiled, &data);
}
// BAD: Compiling in a loop
for data in datasets {
let compiled = engine.compile(&rule).unwrap(); // Unnecessary!
engine.evaluate(&compiled, &data);
}
}
Minimize Cloning
#![allow(unused)]
fn main() {
// GOOD: Use references where possible
let result = engine.evaluate(&compiled, &data)?;
// Use owned version only when you don't need the data afterwards
let result = engine.evaluate_owned(&compiled, data)?;
}
Batch Processing
#![allow(unused)]
fn main() {
// Process in batches to balance parallelism overhead
let batch_size = 100;
for chunk in datasets.chunks(batch_size) {
let results: Vec<_> = chunk.par_iter()
.map(|data| engine.evaluate(&compiled, data))
.collect();
// Process results
}
}
Error Handling in Threads
#![allow(unused)]
fn main() {
use std::thread;
let handles: Vec<_> = datasets.into_iter().map(|data| {
let engine = Arc::clone(&engine);
let compiled = Arc::clone(&compiled);
thread::spawn(move || -> Result<Value, Error> {
engine.evaluate_owned(&compiled, data)
})
}).collect();
// Collect results, handling errors
let results: Vec<Result<Value, Error>> = handles
.into_iter()
.map(|h| h.join().expect("Thread panicked"))
.collect();
// Process results
for (i, result) in results.into_iter().enumerate() {
match result {
Ok(value) => println!("Result {}: {}", i, value),
Err(e) => eprintln!("Error {}: {}", i, e),
}
}
}
API Reference
Core types and methods in datalogic-rs.
DataLogic
The main engine for compiling and evaluating JSONLogic rules.
Creating an Engine
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
// Default engine
let engine = DataLogic::new();
// With configuration
let engine = DataLogic::with_config(config);
// With structure preservation (templating mode)
let engine = DataLogic::with_preserve_structure();
// With both
let engine = DataLogic::with_config_and_structure(config, true);
}
Methods
compile
Compile a JSONLogic rule into an optimized representation.
#![allow(unused)]
fn main() {
pub fn compile(&self, logic: &Value) -> Result<Arc<CompiledLogic>>
}
Parameters:
logic- The JSONLogic rule as aserde_json::Value
Returns:
Ok(Arc<CompiledLogic>)- Compiled rule, thread-safe and shareableErr(Error)- Compilation error
Example:
#![allow(unused)]
fn main() {
let rule = json!({ ">": [{ "var": "x" }, 10] });
let compiled = engine.compile(&rule)?;
}
evaluate
Evaluate compiled logic against borrowed data.
#![allow(unused)]
fn main() {
pub fn evaluate(&self, compiled: &CompiledLogic, data: &Value) -> Result<Value>
}
Parameters:
compiled- Reference to compiled logicdata- Reference to input data
Returns:
Ok(Value)- Evaluation resultErr(Error)- Evaluation error
Example:
#![allow(unused)]
fn main() {
let data = json!({ "x": 15 });
let result = engine.evaluate(&compiled, &data)?;
}
evaluate_owned
Evaluate compiled logic, taking ownership of data.
#![allow(unused)]
fn main() {
pub fn evaluate_owned(&self, compiled: &CompiledLogic, data: Value) -> Result<Value>
}
Parameters:
compiled- Reference to compiled logicdata- Input data (owned)
Returns:
Ok(Value)- Evaluation resultErr(Error)- Evaluation error
Example:
#![allow(unused)]
fn main() {
let result = engine.evaluate_owned(&compiled, json!({ "x": 15 }))?;
}
evaluate_json
Convenience method to evaluate JSON strings directly.
#![allow(unused)]
fn main() {
pub fn evaluate_json(&self, logic: &str, data: &str) -> Result<Value>
}
Parameters:
logic- JSONLogic rule as a JSON stringdata- Input data as a JSON string
Returns:
Ok(Value)- Evaluation resultErr(Error)- Parse or evaluation error
Example:
#![allow(unused)]
fn main() {
let result = engine.evaluate_json(
r#"{ "+": [1, 2] }"#,
r#"{}"#
)?;
}
add_operator
Register a custom operator.
#![allow(unused)]
fn main() {
pub fn add_operator(&mut self, name: String, operator: Box<dyn Operator>)
}
Parameters:
name- Operator name (used in rules)operator- Boxed operator implementation
Example:
#![allow(unused)]
fn main() {
engine.add_operator("double".to_string(), Box::new(DoubleOperator));
}
CompiledLogic
Pre-compiled rule representation. Created by DataLogic::compile().
Characteristics
- Thread-safe: Wrapped in
Arc, implementsSend + Sync - Immutable: Cannot be modified after compilation
- Shareable: Cheap to clone (reference counting)
Usage
#![allow(unused)]
fn main() {
// Compile once
let compiled = engine.compile(&rule)?;
// Share across threads
let compiled_clone = Arc::clone(&compiled);
std::thread::spawn(move || {
engine.evaluate(&compiled_clone, &data);
});
// Evaluate multiple times
for data in datasets {
engine.evaluate(&compiled, &data)?;
}
}
EvaluationConfig
Configuration for evaluation behavior.
Creating Configuration
#![allow(unused)]
fn main() {
use datalogic_rs::EvaluationConfig;
let config = EvaluationConfig::default();
}
Builder Methods
#![allow(unused)]
fn main() {
// NaN handling
config.with_nan_handling(NanHandling::IgnoreValue)
// Division by zero
config.with_division_by_zero(DivisionByZero::ReturnNull)
// Truthiness evaluation
config.with_truthy_evaluator(TruthyEvaluator::Python)
// Loose equality errors
config.with_loose_equality_throws_errors(false)
}
Presets
#![allow(unused)]
fn main() {
// Lenient arithmetic
let config = EvaluationConfig::safe_arithmetic();
// Strict type checking
let config = EvaluationConfig::strict();
}
NanHandling
How to handle non-numeric values in arithmetic.
#![allow(unused)]
fn main() {
pub enum NanHandling {
ThrowError, // Default: throw an error
IgnoreValue, // Skip non-numeric values
CoerceToZero, // Treat as 0
}
}
DivisionByZero
How to handle division by zero.
#![allow(unused)]
fn main() {
pub enum DivisionByZero {
ReturnBounds, // Default: return Infinity/-Infinity
ThrowError, // Throw an error
ReturnNull, // Return null
}
}
TruthyEvaluator
How to evaluate truthiness.
#![allow(unused)]
fn main() {
pub enum TruthyEvaluator {
JavaScript, // Default: JS-style truthiness
Python, // Python-style truthiness
StrictBoolean, // Only true/false are valid
Custom(Arc<dyn Fn(&Value) -> bool + Send + Sync>),
}
}
Operator Trait
Interface for custom operators.
#![allow(unused)]
fn main() {
pub trait Operator: Send + Sync {
fn evaluate(
&self,
args: &[Value],
context: &mut ContextStack,
evaluator: &dyn Evaluator,
) -> Result<Value>;
}
}
Parameters:
args- Unevaluated arguments from the rulecontext- Current evaluation contextevaluator- Interface to evaluate nested expressions
Important: Arguments are unevaluated. Call evaluator.evaluate() to resolve them.
Evaluator Trait
Interface for evaluating expressions (used in custom operators).
#![allow(unused)]
fn main() {
pub trait Evaluator {
fn evaluate(&self, value: &Value, context: &mut ContextStack) -> Result<Value>;
}
}
ContextStack
Manages variable scope during evaluation.
Accessing Current Element
In array operations (map, filter, reduce):
#![allow(unused)]
fn main() {
// Access current element
let current = context.current();
// Access current index
let index = context.index();
// Access accumulator (in reduce)
let acc = context.accumulator();
}
Error
Error types returned by datalogic-rs.
#![allow(unused)]
fn main() {
pub enum Error {
InvalidArguments(String),
UnknownOperator(String),
TypeError(String),
DivisionByZero,
Custom(String),
// ... other variants
}
}
Common Error Handling
#![allow(unused)]
fn main() {
use datalogic_rs::Error;
match engine.evaluate(&compiled, &data) {
Ok(result) => println!("Result: {}", result),
Err(Error::InvalidArguments(msg)) => eprintln!("Bad arguments: {}", msg),
Err(Error::UnknownOperator(op)) => eprintln!("Unknown operator: {}", op),
Err(e) => eprintln!("Error: {}", e),
}
}
Result Type
#![allow(unused)]
fn main() {
pub type Result<T> = std::result::Result<T, Error>;
}
Full Example
use datalogic_rs::{DataLogic, EvaluationConfig, NanHandling, Error};
use serde_json::json;
use std::sync::Arc;
fn main() -> Result<(), Error> {
// Create configured engine
let config = EvaluationConfig::default()
.with_nan_handling(NanHandling::IgnoreValue);
let engine = Arc::new(DataLogic::with_config(config));
// Compile rule
let rule = json!({
"if": [
{ ">=": [{ "var": "score" }, 60] },
"pass",
"fail"
]
});
let compiled = engine.compile(&rule)?;
// Evaluate with different data
let results: Vec<_> = vec![
json!({ "score": 85 }),
json!({ "score": 45 }),
json!({ "score": 60 }),
].into_iter()
.map(|data| engine.evaluate_owned(&compiled, data))
.collect();
for result in results {
println!("{}", result?);
}
Ok(())
}
Use Cases & Examples
Real-world examples of using datalogic-rs for common scenarios.
Feature Flags
Control feature availability based on user attributes.
Basic Feature Flag
#![allow(unused)]
fn main() {
use datalogic_rs::DataLogic;
use serde_json::json;
let engine = DataLogic::new();
// Feature available to premium users in US
let rule = json!({
"and": [
{ "==": [{ "var": "user.plan" }, "premium"] },
{ "==": [{ "var": "user.country" }, "US"] }
]
});
let compiled = engine.compile(&rule).unwrap();
let user_data = json!({
"user": {
"plan": "premium",
"country": "US"
}
});
let enabled = engine.evaluate_owned(&compiled, user_data).unwrap();
assert_eq!(enabled, json!(true));
}
Percentage Rollout
#![allow(unused)]
fn main() {
// Enable for 20% of users (based on user ID hash)
let rule = json!({
"<": [
{ "%": [{ "var": "user.id" }, 100] },
20
]
});
}
Beta Access
#![allow(unused)]
fn main() {
// Enable for beta testers OR employees OR users who signed up before a date
let rule = json!({
"or": [
{ "==": [{ "var": "user.role" }, "beta_tester"] },
{ "ends_with": [{ "var": "user.email" }, "@company.com"] },
{ "<": [{ "var": "user.signup_date" }, "2024-01-01"] }
]
});
}
Dynamic Pricing
Calculate prices based on rules.
Discount by Quantity
#![allow(unused)]
fn main() {
let rule = json!({
"if": [
{ ">=": [{ "var": "quantity" }, 100] },
{ "*": [{ "var": "base_price" }, 0.8] }, // 20% off
{ "if": [
{ ">=": [{ "var": "quantity" }, 50] },
{ "*": [{ "var": "base_price" }, 0.9] }, // 10% off
{ "var": "base_price" }
]}
]
});
let data = json!({
"quantity": 75,
"base_price": 100
});
let price = engine.evaluate_owned(&compiled, data).unwrap();
// Result: 90 (10% discount)
}
Tiered Pricing
#![allow(unused)]
fn main() {
let rule = json!({
"+": [
// First 10 units at $10
{ "*": [{ "min": [{ "var": "quantity" }, 10] }, 10] },
// Next 40 units at $8
{ "*": [
{ "max": [{ "-": [{ "min": [{ "var": "quantity" }, 50] }, 10] }, 0] },
8
]},
// Remaining units at $6
{ "*": [
{ "max": [{ "-": [{ "var": "quantity" }, 50] }, 0] },
6
]}
]
});
}
Member Pricing
#![allow(unused)]
fn main() {
let rule = json!({
"if": [
{ "var": "user.is_member" },
{ "*": [
{ "var": "product.price" },
{ "-": [1, { "/": [{ "var": "user.member_discount" }, 100] }] }
]},
{ "var": "product.price" }
]
});
let data = json!({
"user": { "is_member": true, "member_discount": 15 },
"product": { "price": 200 }
});
// Result: 170 (15% member discount)
}
Form Validation
Validate user input with complex rules.
Required Fields
#![allow(unused)]
fn main() {
let rule = json!({
"if": [
{ "missing": ["name", "email", "password"] },
{
"valid": false,
"errors": { "missing": ["name", "email", "password"] }
},
{ "valid": true }
]
});
}
Field Constraints
#![allow(unused)]
fn main() {
let engine = DataLogic::with_preserve_structure();
let rule = json!({
"valid": { "and": [
// Email format
{ "in": ["@", { "var": "email" }] },
// Password length
{ ">=": [{ "length": { "var": "password" } }, 8] },
// Age range
{ "and": [
{ ">=": [{ "var": "age" }, 18] },
{ "<=": [{ "var": "age" }, 120] }
]}
]},
"errors": { "filter": [
[
{ "if": [
{ "!": { "in": ["@", { "var": "email" }] } },
"Invalid email format",
null
]},
{ "if": [
{ "<": [{ "length": { "var": "password" } }, 8] },
"Password must be at least 8 characters",
null
]},
{ "if": [
{ "or": [
{ "<": [{ "var": "age" }, 18] },
{ ">": [{ "var": "age" }, 120] }
]},
"Age must be between 18 and 120",
null
]}
],
{ "!==": [{ "var": "" }, null] }
]}
});
}
Conditional Validation
#![allow(unused)]
fn main() {
// If business account, require company name
let rule = json!({
"if": [
{ "and": [
{ "==": [{ "var": "account_type" }, "business"] },
{ "missing": ["company_name"] }
]},
{ "error": "Company name required for business accounts" },
{ "valid": true }
]
});
}
Access Control
Determine user permissions.
Role-Based Access
#![allow(unused)]
fn main() {
let rule = json!({
"or": [
{ "==": [{ "var": "user.role" }, "admin"] },
{ "and": [
{ "==": [{ "var": "user.role" }, "editor"] },
{ "==": [{ "var": "resource.owner_id" }, { "var": "user.id" }] }
]}
]
});
}
Permission Checking
#![allow(unused)]
fn main() {
let rule = json!({
"in": [
{ "var": "required_permission" },
{ "var": "user.permissions" }
]
});
let data = json!({
"user": {
"permissions": ["read", "write", "delete"]
},
"required_permission": "write"
});
// Result: true
}
Time-Based Access
#![allow(unused)]
fn main() {
let rule = json!({
"and": [
// Has permission
{ "in": ["access_data", { "var": "user.permissions" }] },
// Within allowed hours (9 AM - 6 PM)
{ "and": [
{ ">=": [{ "var": "current_hour" }, 9] },
{ "<": [{ "var": "current_hour" }, 18] }
]},
// On a weekday
{ "in": [{ "var": "current_day" }, [1, 2, 3, 4, 5]] }
]
});
}
Fraud Detection
Score and flag potentially fraudulent transactions.
Risk Scoring
#![allow(unused)]
fn main() {
let rule = json!({
"+": [
// High amount
{ "if": [{ ">": [{ "var": "amount" }, 1000] }, 30, 0] },
// New account
{ "if": [{ "<": [{ "var": "account_age_days" }, 7] }, 25, 0] },
// Different country
{ "if": [
{ "!=": [{ "var": "billing_country" }, { "var": "shipping_country" }] },
20,
0
]},
// Multiple attempts
{ "if": [{ ">": [{ "var": "attempts_last_hour" }, 3] }, 25, 0] },
// Unusual time
{ "if": [
{ "or": [
{ "<": [{ "var": "hour" }, 6] },
{ ">": [{ "var": "hour" }, 23] }
]},
15,
0
]}
]
});
// Score > 50 = flag for review
let data = json!({
"amount": 1500,
"account_age_days": 3,
"billing_country": "US",
"shipping_country": "CA",
"attempts_last_hour": 1,
"hour": 14
});
// Result: 75 (high amount + new account + different country)
}
Velocity Checks
#![allow(unused)]
fn main() {
let rule = json!({
"or": [
// Too many transactions in short time
{ ">": [{ "var": "transactions_last_hour" }, 10] },
// Too much total amount
{ ">": [{ "var": "total_amount_last_hour" }, 5000] },
// Same card used from multiple IPs
{ ">": [{ "var": "unique_ips_last_day" }, 3] }
]
});
}
Data Transformation
Transform and reshape data.
API Response Mapping
#![allow(unused)]
fn main() {
let engine = DataLogic::with_preserve_structure();
let template = json!({
"users": {
"map": [
{ "var": "raw_users" },
{
"id": { "var": "user_id" },
"fullName": { "cat": [{ "var": "first_name" }, " ", { "var": "last_name" }] },
"email": { "lower": { "var": "email" } },
"isActive": { "==": [{ "var": "status" }, "active"] }
}
]
},
"total": { "length": { "var": "raw_users" } },
"activeCount": { "length": {
"filter": [
{ "var": "raw_users" },
{ "==": [{ "var": "status" }, "active"] }
]
}}
});
}
Report Generation
#![allow(unused)]
fn main() {
let template = json!({
"report": {
"title": { "cat": ["Sales Report - ", { "var": "period" }] },
"generated": { "format_date": [{ "now": [] }, "%Y-%m-%d %H:%M"] },
"summary": {
"totalSales": { "reduce": [
{ "var": "transactions" },
{ "+": [{ "var": "accumulator" }, { "var": "current.amount" }] },
0
]},
"avgTransaction": { "/": [
{ "reduce": [
{ "var": "transactions" },
{ "+": [{ "var": "accumulator" }, { "var": "current.amount" }] },
0
]},
{ "length": { "var": "transactions" } }
]},
"topCategory": { "var": "top_category" }
}
}
});
}
Notification Rules
Determine when and how to send notifications.
Alert Conditions
#![allow(unused)]
fn main() {
let rule = json!({
"if": [
// Critical: immediate
{ ">": [{ "var": "error_rate" }, 10] },
{ "channel": "pager", "priority": "critical" },
// Warning: Slack
{ "if": [
{ ">": [{ "var": "error_rate" }, 5] },
{ "channel": "slack", "priority": "warning" },
// Info: email digest
{ "if": [
{ ">": [{ "var": "error_rate" }, 1] },
{ "channel": "email", "priority": "info" },
null
]}
]}
]
});
}
User Preferences
#![allow(unused)]
fn main() {
let rule = json!({
"and": [
// User has enabled notifications
{ "var": "user.notifications_enabled" },
// Notification type is in user's preferences
{ "in": [
{ "var": "notification.type" },
{ "var": "user.enabled_types" }
]},
// Within user's quiet hours
{ "!": { "and": [
{ ">=": [{ "var": "current_hour" }, { "var": "user.quiet_start" }] },
{ "<": [{ "var": "current_hour" }, { "var": "user.quiet_end" }] }
]}}
]
});
}