Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Plasmatic Logo

datalogic-rs

A fast, production-ready Rust engine for JSONLogic.

Crates.io Documentation License: Apache 2.0


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 Arc sharing
  • 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:

  1. Compilation: Your JSON logic is parsed and compiled into an optimized CompiledLogic structure. This phase:

    • Assigns OpCodes to built-in operators for fast dispatch
    • Pre-evaluates constant expressions
    • Analyzes structure for templating mode
  2. 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

Playground

Try JSONLogic expressions right in your browser! This playground uses the same WebAssembly-compiled engine that powers the Rust library.

How to Use

  1. Logic: Enter your JSONLogic expression in the Logic pane
  2. Data: Enter the JSON data to evaluate against in the Data pane
  3. Run: Press the Run button or use Ctrl+Enter (Cmd+Enter on Mac)
  4. Examples: Use the dropdown to load pre-built examples

Quick Reference

Basic Operators

OperatorExampleDescription
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

OperatorExampleDescription
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

OperatorExampleDescription
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

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:

  1. Create an engine instance
  2. Compile your rule (once)
  3. 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

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:

  1. Parses the JSON into an internal representation
  2. Assigns OpCodes to operators for fast dispatch
  3. Pre-evaluates constant expressions
  4. Wraps the result in Arc for 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):

  1. Dispatches operations via OpCode (O(1) lookup)
  2. Accesses data through the context stack
  3. 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:

  • "" or var with 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

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

CategoryOperatorsDescription
Variable Accessvar, val, existsAccess and check data
Comparison==, ===, !=, !==, >, >=, <, <=Compare values
Logical!, !!, and, orBoolean logic
Arithmetic+, -, *, /, %, max, min, abs, ceil, floorMath operations
Control Flowif, ?:, ??Conditional branching
Stringcat, substr, in, length, starts_with, ends_with, upper, lower, trim, splitString manipulation
Arraymerge, filter, map, reduce, all, some, none, sort, sliceArray operations
DateTimedatetime, timestamp, parse_date, format_date, date_diff, nowDate and time
Missing Valuesmissing, missing_someCheck for missing data
Error Handlingtry, throwException 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 value
  • or: Stops at first truthy value
  • if: 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 null if 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 syntax
  • default - 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 var but 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 false for 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 value
  • b - 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 value
  • b - 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 value
  • b - 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 value
  • b - 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 compare
  • c - 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 compare
  • c - 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 compare
  • c - 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 compare
  • c - 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 to a < 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 and returns true (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 or returns false

Truthiness Reference

The default JavaScript-style truthiness:

ValueTruthy?
trueYes
falseNo
1, 2, -1, 3.14Yes
0, 0.0No
"hello", "0", "false"Yes
""No
[1, 2], {"a": 1}Yes
[]No
nullNo

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 from
  • b - 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 - Dividend
  • b - 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 Infinity or -Infinity

% (Modulo)

Calculate remainder of division.

Syntax:

{ "%": [a, b] }

Arguments:

  • a - Dividend
  • b - 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, or
  • array - 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, or
  • array - 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 evaluate
  • then_value - Value if condition is truthy
  • else_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 evaluate
  • then_value - Value if condition is truthy
  • else_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 or if you want to skip all falsy values
  • Short-circuits: stops at first non-null value

Comparison: if vs ?: vs ?? vs or

OperatorUse CaseFalsy Handling
ifComplex branching, multiple conditionsEvaluates truthiness
?:Simple if/elseEvaluates truthiness
??Default for null onlyOnly skips null
orDefault for any falsySkips 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 string
  • start - 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 for
  • haystack - 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 check
  • prefix - 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 check
  • suffix - 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 split
  • delimiter - 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 filter
  • condition - 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 transform
  • transformation - 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 reduce
  • reducer - Operation combining accumulator and current element
  • initial - 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 check
  • condition - 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 check
  • condition - 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 check
  • condition - 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 sort
  • comparator - 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 array
  • start - 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 string
  • format - 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 parse
  • format - 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 format
  • format - 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 datetime
  • date2 - Second datetime
  • unit - 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

Scenariomissingmissing_some
All fields required{ "!": { "missing": [...] } }N/A
At least N requiredComplex logic needed{ "!": { "missing_some": [N, [...]] } }
Check which are missingReturns missing listReturns missing list if < N present
No minimumAppropriateUse 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 error
  • fallback - 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

  1. Always evaluate arguments before using them
  2. Validate argument count and types early
  3. Return meaningful error messages
  4. Keep operators focused - one responsibility per operator
  5. Document the expected syntax for each operator
  6. Use Arc for shared configuration to maintain thread safety
  7. 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:

ValueJavaScriptPythonStrictBoolean
truetruthytruthytruthy
falsefalsyfalsyfalsy
1truthytruthyerror/falsy
0falsyfalsyerror/falsy
""falsyfalsyerror/falsy
"0"truthytruthyerror/falsy
[]falsyfalsyerror/falsy
[0]truthytruthyerror/falsy
nullfalsyfalsyerror/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

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 a serde_json::Value

Returns:

  • Ok(Arc<CompiledLogic>) - Compiled rule, thread-safe and shareable
  • Err(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 logic
  • data - Reference to input data

Returns:

  • Ok(Value) - Evaluation result
  • Err(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 logic
  • data - Input data (owned)

Returns:

  • Ok(Value) - Evaluation result
  • Err(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 string
  • data - Input data as a JSON string

Returns:

  • Ok(Value) - Evaluation result
  • Err(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, implements Send + 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 rule
  • context - Current evaluation context
  • evaluator - 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" }] }
        ]}}
    ]
});
}