Getting Started with DataLogic-rs

Introduction

DataLogic-rs is a powerful expression evaluation engine for Rust that implements the JSONLogic specification and will support additional expression formats in the future. This tutorial will guide you through setting up and using DataLogic-rs in your Rust projects.

Installation

Add DataLogic-rs to your Cargo.toml file:

[dependencies]
datalogic-rs = "3.0.7"

Basic Usage

Here's a simple example of how to use DataLogic-rs:

use datalogic_rs::DataLogic;

fn main() {
    // Create a new DataLogic instance
    let dl = DataLogic::new();
    
    // Define a rule and data
    let rule_str = r#"{"==":[{"val":"temperature"},0]}"#;
    let data_str = r#"{"temperature":0}"#;
    
    // Evaluate the rule against the data
    let result = dl.evaluate_str(rule_str, data_str, None).unwrap();
    
    println!("Result: {}", result); // Prints: true
}

This example checks if the temperature in the data is equal to 0.

Working with Complex Rules

DataLogic-rs can handle complex rules and data structures:

use datalogic_rs::DataLogic;

fn main() {
    let dl = DataLogic::new();
    
    // A complex rule that filters items based on their quantity
    let rule_str = r#"{
        "filter": [
            {"val": "items"}, 
            {">=": [{"val": "qty"}, 2]}
        ]
    }"#;
    
    let data_str = r#"{
        "items": [
            {"id": "apple", "qty": 1},
            {"id": "banana", "qty": 3},
            {"id": "orange", "qty": 2}
        ]
    }"#;
    
    let result = dl.evaluate_str(rule_str, data_str, None).unwrap();
    
    println!("Items with qty >= 2: {}", result);
    // Prints items that have qty >= 2 (banana and orange)
}

Core API Methods

DataLogic-rs provides three core methods for evaluating rules against data:

1. evaluate

The evaluate method evaluates a compiled rule against a parsed data context. This is useful when you need to reuse the same rule or data multiple times.

use datalogic_rs::DataLogic;

fn main() {
    let dl = DataLogic::new();
    
    // Parse the rule and data separately
    let rule = dl.parse_logic(r#"{ ">": [{"var": "temp"}, 100] }"#, None).unwrap();
    let data = dl.parse_data(r#"{"temp": 110}"#).unwrap();
    
    // Evaluate the rule against the data
    let result = dl.evaluate(&rule, &data).unwrap();
    
    println!("Result: {}", result); // Prints: true
}

2. evaluate_str

The evaluate_str method combines parsing and evaluation in a single step. It's ideal for one-time evaluations or quick scripting.

use datalogic_rs::DataLogic;

fn main() {
    let dl = DataLogic::new();
    
    // Parse and evaluate in one step
    let result = dl.evaluate_str(
        r#"{ "abs": -42 }"#,
        r#"{}"#,
        None
    ).unwrap();
    
    println!("Result: {}", result); // Prints: 42
}

3. evaluate_json

The evaluate_json method works directly with serde_json Values. This is useful when you're already working with JSON data in your application.

use datalogic_rs::DataLogic;
use serde_json::json;

fn main() {
    let dl = DataLogic::new();
    
    // Use serde_json's json! macro to create JSON values
    let logic = json!({"ceil": 3.14});
    let data = json!({});
    
    // Evaluate using the JSON values directly
    let result = dl.evaluate_json(&logic, &data, None).unwrap();
    
    println!("Result: {}", result); // Prints: 4
}

Choose the method that best fits your use case based on whether you need to reuse rules/data and your preferred input format.

Arena-Based Memory Management

DataLogic-rs uses arena-based memory management for high-performance allocation and deallocation, which is particularly important when processing many rules or working with large datasets.

What is Arena Allocation?

Arena allocation (also known as "bump allocation") offers several key advantages:

  • Memory is allocated in large, contiguous chunks
  • Individual allocations are extremely fast (just incrementing a pointer)
  • Memory is freed all at once instead of individually
  • Improved memory locality leads to better cache performance

Resetting the Arena

For long-running applications, it's important to periodically reset the arena to free memory:

use datalogic_rs::DataLogic;

fn main() {
    // Create a DataLogic instance with default settings
    let mut dl = DataLogic::new();
    
    // Evaluate some rules
    let result1 = dl.evaluate_str(r#"{ ">": [{"var": "temp"}, 100] }"#, r#"{"temp": 110}"#, None).unwrap();
    
    // Reset the arena to free memory
    dl.reset_arena();
    
    // Continue with new evaluations
    let result2 = dl.evaluate_str(r#"{ "<": [{"var": "count"}, 10] }"#, r#"{"count": 5}"#, None).unwrap();
    
    println!("Results: {}, {}", result1, result2);
}

Tuning Arena Size

You can customize the chunk size used by the arena for better performance:

use datalogic_rs::DataLogic;

// Create a DataLogic instance with a 1MB chunk size
let mut dl = DataLogic::with_chunk_size(1024 * 1024);

Processing Large Batches

For batch processing, reset the arena after each batch to prevent memory growth:

use datalogic_rs::{DataLogic, Result};

fn process_batches(batches: Vec<(String, String)>) -> Result<()> {
    let mut dl = DataLogic::new();
    
    for (rule_str, data_str) in batches {
        // Process each batch
        let result = dl.evaluate_str(&rule_str, &data_str, None)?;
        println!("Result: {}", result);
        
        // Reset the arena to free memory after processing a batch
        dl.reset_arena();
    }
    
    Ok(())
}

For more detailed information on arena-based memory management, see the ARENA.md document.

Custom Operators

DataLogic-rs provides two approaches for implementing custom operators:

  • CustomSimple - A simplified API that works with owned values, ideal for scalar operations
  • CustomAdvanced - Direct arena access for maximum performance and complex data structures

CustomSimple - Simplified Approach

The CustomSimple approach lets you work with operators as simple functions without worrying about arena allocation:

use datalogic_rs::{DataLogic, DataValue, SimpleOperatorFn};
use datalogic_rs::value::NumberValue;

// Define a simple custom operator that doubles a number
fn double<'r>(args: Vec>, data: DataValue<'r>) -> std::result::Result, String> {
    if args.is_empty() {
        // If no arguments provided, check for a value in the data context
        if let Some(obj) = data.as_object() {
            for (key, val) in obj {
                if *key == "value" && val.is_number() {
                    if let Some(n) = val.as_f64() {
                        return Ok(DataValue::Number(NumberValue::from_f64(n * 2.0)));
                    }
                }
            }
        }
        return Err("double operator requires at least one argument or 'value' in data".to_string());
    }
    
    if let Some(n) = args[0].as_f64() {
        return Ok(DataValue::Number(NumberValue::from_f64(n * 2.0)));
    }
    
    Err("Argument must be a number".to_string())
}

fn main() {
    let mut dl = DataLogic::new();
    
    // Register the simple custom operator
    dl.register_simple_operator("double", double);
    
    // Use the custom operator with an explicit argument
    let rule_str = r#"{"double":5}"#;
    let result = dl.evaluate_str(rule_str, "{}", None).unwrap();
    println!("Result: {}", result); // Prints: 10
    
    // Use the custom operator with data context
    let rule_str = r#"{"double":[]}"#;
    let data_str = r#"{"value":7}"#;
    let result = dl.evaluate_str(rule_str, data_str, None).unwrap();
    println!("Result from context: {}", result); // Prints: 14
}

The CustomSimple approach is ideal for:

  • Working with scalar values (numbers, strings, booleans)
  • Simpler implementation without arena management
  • Operations that don't require returning complex data structures
  • Direct access to the data context for more flexible operations

CustomAdvanced - Arena-Based Approach

For more complex operations or when you need maximum performance, you can implement the CustomOperator trait:

use datalogic_rs::{DataLogic, DataValue, CustomOperator};
use datalogic_rs::arena::DataArena;
use datalogic_rs::value::NumberValue;
use datalogic_rs::logic::Result;

// Define a custom operator that doubles a number
#[derive(Debug)]
struct DoubleOperator;

impl CustomOperator for DoubleOperator {
    fn evaluate<'a>(&self, args: &'a [DataValue<'a>], arena: &'a DataArena) -> Result<&'a DataValue<'a>> {
        if args.is_empty() {
            return Err("double operator requires at least one argument".into());
        }
        
        // Get the first argument value
        if let Some(num) = args[0].as_f64() {
            // Double it and return - allocated in the arena
            return Ok(arena.alloc(DataValue::Number(NumberValue::from_f64(num * 2.0))));
        }
        
        // Return null for non-numeric values
        Ok(arena.null_value())
    }
}

fn main() {
    let mut dl = DataLogic::new();
    
    // Register the custom operator
    dl.register_custom_operator("double", Box::new(DoubleOperator));
    
    // Use the custom operator
    let rule_str = r#"{"double":{"val":"value"}}"#;
    let data_str = r#"{"value":5}"#;
    
    let result = dl.evaluate_str(rule_str, data_str, None).unwrap();
    
    println!("Result: {}", result); // Prints: 10
}

Working with Complex Data in CustomAdvanced

The CustomAdvanced approach lets you work directly with arrays and objects:

#[derive(Debug)]
struct FilterEven;

impl CustomOperator for FilterEven {
    fn evaluate<'a>(&self, args: &'a [DataValue<'a>], arena: &'a DataArena) -> Result<&'a DataValue<'a>> {
        if args.is_empty() {
            return Ok(arena.empty_array_value());
        }

        if let Some(arr) = args[0].as_array() {
            // Filter numbers that are even
            let filtered: Vec<&DataValue> = arr
                .iter()
                .filter(|v| {
                    if let Some(n) = v.as_i64() {
                        n % 2 == 0
                    } else {
                        false
                    }
                })
                .collect();

            // Allocate the filtered array in the arena
            Ok(arena.alloc_array(&filtered))
        } else {
            Ok(arena.empty_array_value())
        }
    }
}

Using Arena Allocation in CustomAdvanced

When implementing CustomAdvanced operators, always allocate results in the arena:

// Basic allocation for simple values
Ok(arena.alloc(DataValue::Number(NumberValue::from_f64(value))))

// Working with strings
Ok(arena.alloc(DataValue::String(arena.alloc_str("my string"))))

// Working with arrays/collections
let mut temp_vec = arena.get_data_value_vec();
// Add elements to the vector
temp_vec.push(DataValue::Number(1.into()));
temp_vec.push(DataValue::Number(2.into()));

// Convert to a permanent slice in the arena
let result_slice = arena.bump_vec_into_slice(temp_vec);

// Create and return a DataValue array
Ok(arena.alloc(DataValue::Array(result_slice)))

Choosing Between CustomSimple and CustomAdvanced

Key factors to consider:

  • CustomSimple: Easier to implement, works well for scalar operations
  • CustomAdvanced: More complex but offers full control, required for arrays and objects
  • Use CustomSimple when possible for simpler code maintenance
  • Use CustomAdvanced when performance or complex data structures are required

For detailed implementation guidance, see the Custom Operators documentation.

WebAssembly Support

DataLogic-rs can be compiled to WebAssembly for use in web applications:

// In your lib.rs
use wasm_bindgen::prelude::*;
use datalogic_rs::DataLogic;

#[wasm_bindgen]
pub struct JsDataLogic {
    engine: DataLogic,
}

#[wasm_bindgen]
impl JsDataLogic {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {
        JsDataLogic {
            engine: DataLogic::new(),
        }
    }
    
    #[wasm_bindgen]
    pub fn evaluate(&self, rule: &str, data: &str) -> Result {
        match self.engine.evaluate_str(rule, data, None) {
            Ok(result) => Ok(result.to_string()),
            Err(e) => Err(JsValue::from_str(&e.to_string())),
        }
    }
}

Then in JavaScript, you can use it like this:

import { JsDataLogic } from 'datalogic-rs';

const logic = new JsDataLogic();
const rule = '{"==":[{"val":"temperature"},0]}';
const data = '{"temperature":0}';

try {
    const result = logic.evaluate(rule, data);
    console.log(`Result: ${result}`); // Outputs: Result: true
} catch (error) {
    console.error(`Error: ${error}`);
}

Next Steps

Now that you understand the basics of DataLogic-rs, you can: