This guide documents the public API for the DataLogic-rs crate, which provides a Rust implementation for evaluating JSON Logic rules.
The library exposes several key types that most users will need:
DataLogic
: The main entry point for parsing and evaluating logic rulesDataValue
: A memory-efficient value type for representing JSON-like dataLogic
: Represents a compiled logic rule ready for evaluationLogicError
: Error type for all operations in the libraryResult<T>
: Alias forstd::result::Result<T, LogicError>
The DataLogic-rs library provides multiple ways to evaluate rules, depending on your specific needs:
Method | Input Types | Output Type | Use Case |
---|---|---|---|
evaluate |
Logic , DataValue |
&DataValue |
Best for reusing parsed rules and data |
evaluate_json |
&JsonValue , &JsonValue |
JsonValue |
Working directly with JSON values |
evaluate_str |
&str , &str |
JsonValue |
One-step parsing and evaluation from strings |
Here's a simple example of using the library with evaluate_str
:
use datalogic_rs::DataLogic;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a new DataLogic instance
let dl = DataLogic::new();
// Parse and evaluate in one step
let result = dl.evaluate_str(
r#"{ ">": [{"var": "temp"}, 100] }"#, // Logic rule
r#"{"temp": 110, "name": "user"}"#, // Data
None // Use default parser
)?;
println!("Result: {}", result); // Output: true
Ok(())
}
When you need to reuse rules or data across multiple evaluations:
use datalogic_rs::DataLogic;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let dl = DataLogic::new();
// Parse the rule and data separately
let rule = dl.parse_logic(r#"{ ">": [{"var": "temp"}, 100] }"#, None)?;
let data = dl.parse_data(r#"{"temp": 110}"#)?;
// Evaluate the rule against the data
let result = dl.evaluate(&rule, &data)?;
println!("Result: {}", result); // Prints: true
Ok(())
}
This approach is most efficient when:
- Evaluating the same rule against different data sets
- Evaluating different rules against the same data
- You need fine-grained control over the parsing and evaluation steps
For quick, one-time evaluations from string inputs:
use datalogic_rs::DataLogic;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let dl = DataLogic::new();
// Parse and evaluate in one step
let result = dl.evaluate_str(
r#"{ "abs": -42 }"#,
r#"{}"#,
None
)?;
println!("Result: {}", result); // Prints: 42
Ok(())
}
When your application already has the rule and data as serde_json::Value
objects:
use datalogic_rs::DataLogic;
use serde_json::json;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let dl = DataLogic::new();
// Use serde_json values directly
let logic = json!({"ceil": 3.14});
let data = json!({});
// Evaluate using the JSON values
let result = dl.evaluate_json(&logic, &data, None)?;
println!("Result: {}", result); // Prints: 4
Ok(())
}
DataLogic-rs provides methods to parse rules and data separately:
parse_logic(&self, source: &str, format: Option<&str>) -> Result<Logic>
: Parse a logic rule from a stringparse_logic_json(&self, source: &JsonValue, format: Option<&str>) -> Result<Logic>
: Parse a logic rule from a JSON value
parse_data(&self, source: &str) -> Result<DataValue>
: Parse data from a stringparse_data_json(&self, source: &JsonValue) -> Result<DataValue>
: Parse data from a JSON value
DataLogic-rs uses an arena-based memory management system for efficient allocation and deallocation of values during rule evaluation. This approach significantly improves performance and reduces memory overhead.
DataLogic::with_chunk_size(size: usize) -> Self
: Create a new instance with a specific arena chunk sizereset_arena(&mut self)
: Reset the arena to free all allocated memory
For long-running applications or when processing many rules, periodically reset the arena to prevent excessive memory usage:
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(())
}
- Reset Periodically: Call
reset_arena()
after processing batches of rules to free memory - Tune Chunk Size: For memory-sensitive applications, customize the arena chunk size
- Reuse Parsed Rules: Parse rules once and reuse them to avoid repeated parsing costs
- Beware of Dangling References: After
reset_arena()
is called, all previously returned values become invalid
For more detailed information on using the arena, see the ARENA.md document.
All operations that can fail return a Result<T, LogicError>
which should be properly handled:
use datalogic_rs::{DataLogic, LogicError, Result};
fn process_input(rule: &str, data: &str) -> Result<()> {
let dl = DataLogic::new();
match dl.evaluate_str(rule, data, None) {
Ok(result) => {
println!("Success: {}", result);
Ok(())
},
Err(LogicError::ParseError { reason }) => {
eprintln!("Parse error: {}", reason);
Err(LogicError::ParseError { reason })
},
Err(err) => {
eprintln!("Other error: {}", err);
Err(err)
}
}
}
- Use
DataLogic::with_chunk_size()
to tune memory allocation for your workload - Parse rules once and reuse them with different data inputs using the
evaluate
method - Use
reset_arena()
periodically for long-running applications - Choose the most appropriate method based on your input format:
- Already have
serde_json::Value
? Useevaluate_json
- Working with strings? Use
evaluate_str
- Need to reuse rules/data? Parse separately and use
evaluate
- Already have
The library supports extending its functionality with custom operators. These operators need to be arena-aware to properly interact with the memory management system.
Custom operators in DataLogic-rs implement the CustomOperator
trait, which requires an evaluate
method that takes arguments and returns a result allocated within the arena:
use datalogic_rs::{CustomOperator, DataLogic, DataValue, Result};
use datalogic_rs::value::NumberValue;
use datalogic_rs::arena::DataArena;
use std::fmt::Debug;
// 1. Define a struct that implements the CustomOperator trait
#[derive(Debug)]
struct PowerOperator;
impl CustomOperator for PowerOperator {
fn evaluate<'a>(&self, args: &'a [DataValue<'a>], arena: &'a DataArena) -> Result<&'a DataValue<'a>> {
if args.len() != 2 {
return Err(LogicError::InvalidArgument {
reason: "Power operator requires exactly 2 arguments".to_string(),
});
}
if let (Some(base), Some(exp)) = (args[0].as_f64(), args[1].as_f64()) {
// Allocate the result in the arena
return Ok(arena.alloc(DataValue::Number(NumberValue::from_f64(base.powf(exp)))));
}
Err(LogicError::InvalidArgument {
reason: "Arguments must be numbers".to_string(),
})
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut dl = DataLogic::new();
// 2. Register the custom operator with DataLogic
dl.register_custom_operator("pow", Box::new(PowerOperator));
// 3. Use the custom operator in your logic expressions
let result = dl.evaluate_str(
r#"{"pow": [2, 3]}"#,
r#"{}"#,
None
)?;
println!("2^3 = {}", result); // Prints: 2^3 = 8
Ok(())
}
For simpler use cases, DataLogic-rs provides a more convenient way to implement custom operators:
use datalogic_rs::{DataLogic, DataValue, SimpleOperatorFn};
use datalogic_rs::value::NumberValue;
// Define a custom operator function with access to data context
fn pow<'r>(args: Vec<DataValue<'r>>, data: DataValue<'r>) -> std::result::Result<DataValue<'r>, String> {
// Check if we have the expected number of arguments
if args.len() != 2 {
// If arguments are missing, try to find them in the data context
if args.len() == 1 && args[0].is_number() {
if let Some(obj) = data.as_object() {
for (key, val) in obj {
if *key == "exponent" && val.is_number() {
if let (Some(base), Some(exp)) = (args[0].as_f64(), val.as_f64()) {
return Ok(DataValue::Number(NumberValue::from_f64(base.powf(exp))));
}
}
}
}
}
return Err("Power operator requires 2 arguments or second arg in data context".to_string());
}
// Process the arguments
if let (Some(base), Some(exp)) = (args[0].as_f64(), args[1].as_f64()) {
return Ok(DataValue::Number(NumberValue::from_f64(base.powf(exp))));
}
Err("Arguments must be numbers".to_string())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut dl = DataLogic::new();
// Register the custom operator
dl.register_simple_operator("pow", pow);
// Use the custom operator with explicit arguments
let result = dl.evaluate_str(
r#"{"pow": [2, 3]}"#,
r#"{}"#,
None
)?;
println!("2^3 = {}", result); // Prints: 2^3 = 8
// Use the custom operator with data context
let result = dl.evaluate_str(
r#"{"pow": [2]}"#,
r#"{"exponent": 4}"#,
None
)?;
println!("2^4 = {}", result); // Prints: 2^4 = 16
Ok(())
}
The SimpleOperatorFn approach:
- Has access to both arguments and the current data context
- Works with owned DataValues
- Can return scalar types (numbers, strings, booleans, null)
- Handles arena allocation automatically
- Is ideal for most use cases
When implementing custom operators, follow these arena allocation best practices:
-
Always allocate results in the arena: Use
arena.alloc()
for any values you return -
Use arena helper methods for collections:
arena.get_data_value_vec()
- Get a temporary vector for building collectionsarena.bump_vec_into_slice()
- Convert a temporary vector to a permanent slicearena.alloc_str()
- Allocate string valuesarena.alloc_slice_copy()
- Allocate arrays of copyable types
-
Return references from the arena: The return type must be a reference to a value in the arena
For operators that need to build collections:
fn evaluate<'a>(&self, args: &'a [DataValue<'a>], arena: &'a DataArena) -> Result<&'a DataValue<'a>> {
// Create a temporary vector backed by the arena
let mut temp_vec = arena.get_data_value_vec();
// Add elements to it
for i in 1..=5 {
temp_vec.push(DataValue::Number(i.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 allocated in the arena
Ok(arena.alloc(DataValue::Array(result_slice)))
}
Both custom operator types provide access to the current data context:
- SimpleOperatorFn: The data context is directly passed as the second parameter
- CustomOperator trait: The data context can be accessed via
arena.current_context(0)
This allows custom operators to access values from the data independent of the arguments provided, enabling more flexible and powerful operations.
To register a custom operator with DataLogic:
// For custom operator trait implementations
dl.register_custom_operator("operator_name", Box::new(OperatorImplementation));
// For simple function-based operators
dl.register_simple_operator("operator_name", function_name);
Custom operators can be combined with built-in operators and data access:
// Calculate 2 * (base^2) * 3 where base comes from input data
let rule = r#"{
"*": [
2,
{"pow": [{"var": "base"}, 2]},
3
]
}"#;
let data = r#"{"base": 4}"#;
// With base = 4, this calculates 2 * 4² * 3 = 2 * 16 * 3 = 96
let result = dl.evaluate_str(rule, data, None)?;
For more examples, see the examples/custom.rs
and examples/custom_simple.rs
files in the repository.
For a full list of available methods and types, refer to the Rust documentation:
cargo doc --open