Getting Started with DataLogic-rs
Jump to Section
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:
- Explore the Operators Documentation to learn about all available operators
- Try out examples in the Interactive Playground
- Check out the GitHub repository for more examples and advanced usage
- Read ARENA.md for detailed information on memory management