diff --git a/Cargo.toml b/Cargo.toml index e467e6a..9e5d9c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,10 @@ name = "jsonlogic" path = "src/bin.rs" required-features = ["cmdline"] +[[example]] +name = "custom_operators" +path = "examples/custom_operators.rs" + [features] cmdline = ["anyhow", "clap"] default = [] @@ -30,6 +34,7 @@ python = ["cpython"] wasm = ["wasm-bindgen"] [dependencies] +once_cell = "1.19" phf = {version = "~0.8.0", features = ["macros"]} serde_json = "~1.0.41" thiserror = "~1.0.11" diff --git a/README.md b/README.md index 74ff115..4e75a3a 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,89 @@ fn main() { } ``` +#### Custom Operations + +You can add your own operations at runtime using the `add_operation`, `add_lazy_operation`, and `add_data_operation` functions: + +```rust +use jsonlogic_rs::{add_operation, apply, NumParams, Error}; +use serde_json::{json, Value}; + +fn main() -> Result<(), Box> { + // Add a custom "double" operation with proper error handling + add_operation("double", |args| { + match args.first() { + Some(Value::Number(n)) => { + if let Some(num) = n.as_f64() { + Ok(json!(num * 2.0)) + } else { + Err(Error::InvalidArgument { + value: (*n).clone().into(), + operation: "double".to_string(), + reason: "Number cannot be converted to f64".to_string(), + }) + } + } + Some(other) => Err(Error::InvalidArgument { + value: other.clone(), + operation: "double".to_string(), + reason: "Expected a number".to_string(), + }), + None => Err(Error::InvalidArgument { + value: Value::Null, + operation: "double".to_string(), + reason: "Missing required argument".to_string(), + }), + } + }, NumParams::Exactly(1))?; + + // Use the custom operation in a rule + let result = apply(&json!({"double": [21]}), &json!({}))?; + assert_eq!(result, json!(42.0)); + + // Custom operations take precedence over built-in ones + add_operation("==", |_args| Ok(json!("custom!")), NumParams::Exactly(2))?; + let result = apply(&json!({"==": [1, 1]}), &json!({}))?; + assert_eq!(result, json!("custom!")); + + Ok(()) +} +``` + +Three types of custom operations are supported: + +- **Regular Operations** (`add_operation`): Receive evaluated arguments, like built-in arithmetic operators +- **Lazy Operations** (`add_lazy_operation`): Receive data context and unevaluated arguments, allowing control over evaluation flow (like `if`, `and`, `or`) +- **Data Operations** (`add_data_operation`): Similar to lazy operations but designed for data access patterns (like `var`, `missing`) + +#### Error Handling + +The `Error` type is exported for proper error handling in custom operations. You can return specific error types from your custom operators: + +```rust +use jsonlogic_rs::Error; + +// Custom operators can return structured errors +Err(Error::InvalidArgument { + value: invalid_value.clone(), + operation: "my_operation".to_string(), + reason: "Expected a positive number".to_string(), +}) +``` + +**Automatic Parameter Validation**: The `NumParams` specification automatically validates argument counts. If users call your custom operation with the wrong number of arguments, they'll receive a `WrongArgumentCount` error before your function is called: + +```rust +match apply(&json!({"my_op": [1]}), &json!({})) { + Err(Error::WrongArgumentCount { expected, actual }) => { + println!("Expected {:?} arguments, got {}", expected, actual); + } + _ => {} +} +``` + +See the [examples/custom_operators.rs](examples/custom_operators.rs) file for more comprehensive examples. + ### Javascript ```js diff --git a/examples/custom_operators.rs b/examples/custom_operators.rs new file mode 100644 index 0000000..574a826 --- /dev/null +++ b/examples/custom_operators.rs @@ -0,0 +1,272 @@ +// Example: Using Dynamic Custom Operators with json-logic-rs +// +// This example demonstrates how to add custom operations at runtime +// that can be used in JsonLogic rules. + +use jsonlogic_rs::{ + add_data_operation, add_lazy_operation, add_operation, apply, clear_operations, + Error, NumParams, +}; +use serde_json::{json, Value}; + +fn main() -> Result<(), Box> { + println!("=== Custom Operations Demo ===\n"); + + // 1. Add a simple custom operation + println!("1. Adding a custom 'double' operation..."); + add_operation( + "double", + |args| { + if let Some(Value::Number(n)) = args.first() { + if let Some(num) = n.as_f64() { + return Ok(json!(num * 2.0)); + } + } + Ok(Value::Null) + }, + NumParams::Exactly(1), + )?; + + let result = apply(&json!({"double": [21]}), &json!({}))?; + println!(" Rule: {{\"double\": [21]}}"); + println!(" Result: {}", result); + println!(" Expected: 42\n"); + + // 2. Add a custom lazy operation (controls evaluation) + println!("2. Adding a custom 'conditional_log' lazy operation..."); + add_lazy_operation( + "conditional_log", + |data, args| { + if args.len() >= 2 { + let condition = &args[0]; + let message = &args[1]; + + // Only evaluate condition if it's true + if let Value::Bool(true) = condition { + if let Value::String(msg) = message { + println!(" LOG: {}", msg); + } + return Ok(json!(true)); + } + } + Ok(json!(false)) + }, + NumParams::Exactly(2), + )?; + + let result = apply( + &json!({"conditional_log": [true, "Hello from JsonLogic!"]}), + &json!({}), + )?; + println!(" Rule: {{\"conditional_log\": [true, \"Hello from JsonLogic!\"]}}"); + println!(" Result: {}\n", result); + + // 3. Add a custom data operation (accesses context data) + println!("3. Adding a custom 'upper' data operation..."); + add_data_operation( + "upper", + |data, args| { + if let Some(Value::String(field_name)) = args.first() { + if let Value::Object(obj) = data { + if let Some(Value::String(value)) = obj.get(field_name) { + return Ok(json!(value.to_uppercase())); + } + } + } + Ok(Value::Null) + }, + NumParams::Exactly(1), + )?; + + let result = apply( + &json!({"upper": ["name"]}), + &json!({"name": "json-logic-rs"}), + )?; + println!(" Rule: {{\"upper\": [\"name\"]}}"); + println!(" Data: {{\"name\": \"json-logic-rs\"}}"); + println!(" Result: {}\n", result); + + // 4. Demonstrate operator precedence (custom overrides built-in) + println!("4. Overriding built-in '==' operator..."); + println!(" Before override:"); + let result = apply(&json!({"==": [1, 1]}), &json!({}))?; + println!(" Rule: {{\"==\": [1, 1]}} -> {}", result); + + add_operation( + "==", + |args| { + // Custom equality that always returns "CUSTOM!" + Ok(json!("CUSTOM!")) + }, + NumParams::Exactly(2), + )?; + + println!(" After adding custom override:"); + let result = apply(&json!({"==": [1, 1]}), &json!({}))?; + println!(" Rule: {{\"==\": [1, 1]}} -> {}\n", result); + + // 5. Complex example: Custom math operations + println!("5. Adding complex math operations..."); + + // Power operation + add_operation( + "pow", + |args| { + if args.len() == 2 { + if let (Some(Value::Number(base)), Some(Value::Number(exp))) = + (args.get(0), args.get(1)) + { + if let (Some(b), Some(e)) = (base.as_f64(), exp.as_f64()) { + return Ok(json!(b.powf(e))); + } + } + } + Ok(Value::Null) + }, + NumParams::Exactly(2), + )?; + + // Square root operation + add_operation( + "sqrt", + |args| { + if let Some(Value::Number(n)) = args.first() { + if let Some(num) = n.as_f64() { + if num >= 0.0 { + return Ok(json!(num.sqrt())); + } + } + } + Ok(Value::Null) + }, + NumParams::Exactly(1), + )?; + + // Test complex expression: sqrt(pow(3, 2) + pow(4, 2)) + let result = apply( + &json!({ + "sqrt": [{ + "+": [ + {"pow": [3, 2]}, + {"pow": [4, 2]} + ] + }] + }), + &json!({}), + )?; + + println!(" Rule: {{\"sqrt\": [{{\"+ \": [{{\"pow\": [3, 2]}}, {{\"pow\": [4, 2]}}]}}]}}"); + println!(" Result: {} (should be 5.0)\n", result); + + // 6. Clean up + println!("6. Cleaning up custom operations..."); + clear_operations()?; + + // Verify built-in operators work again + let result = apply(&json!({"==": [1, 1]}), &json!({}))?; + println!(" After cleanup, built-in '==' works again: {}", result); + + // 7. Demonstrate proper error handling + println!("7. Demonstrating proper error handling..."); + + // Add an operation that validates input and returns meaningful errors + add_operation( + "safe_divide", + |args| match (args.get(0), args.get(1)) { + (Some(Value::Number(a)), Some(Value::Number(b))) => { + let dividend = a.as_f64().ok_or_else(|| Error::InvalidArgument { + value: (*a).clone().into(), + operation: "safe_divide".to_string(), + reason: "First argument must be a valid number".to_string(), + })?; + + let divisor = b.as_f64().ok_or_else(|| Error::InvalidArgument { + value: (*b).clone().into(), + operation: "safe_divide".to_string(), + reason: "Second argument must be a valid number".to_string(), + })?; + + if divisor == 0.0 { + return Err(Error::InvalidArgument { + value: json!(divisor), + operation: "safe_divide".to_string(), + reason: "Division by zero is not allowed".to_string(), + }); + } + + Ok(json!(dividend / divisor)) + } + (Some(non_number), _) => Err(Error::InvalidArgument { + value: (*non_number).clone(), + operation: "safe_divide".to_string(), + reason: "First argument must be a number".to_string(), + }), + (_, Some(non_number)) => Err(Error::InvalidArgument { + value: (*non_number).clone(), + operation: "safe_divide".to_string(), + reason: "Second argument must be a number".to_string(), + }), + _ => Err(Error::InvalidArgument { + value: Value::Null, + operation: "safe_divide".to_string(), + reason: "Missing required arguments".to_string(), + }), + }, + NumParams::Exactly(2), + )?; + + // Test successful division + let result = apply(&json!({"safe_divide": [10, 2]}), &json!({}))?; + println!(" Rule: {{\"safe_divide\": [10, 2]}} -> {}", result); + + // Test error handling - division by zero + match apply(&json!({"safe_divide": [10, 0]}), &json!({})) { + Ok(_) => println!(" ERROR: Division by zero should have failed!"), + Err(e) => println!(" Caught expected error: {}", e), + } + + // Test error handling - invalid argument type + match apply(&json!({"safe_divide": [10, "hello"]}), &json!({})) { + Ok(_) => println!(" ERROR: Invalid argument should have failed!"), + Err(e) => println!(" Caught expected error: {}", e), + } + + // 8. Demonstrate parameter validation + println!("8. Demonstrating parameter validation..."); + + // Add an operation that requires exactly 2 arguments + add_operation( + "multiply", + |args| { + // We can assume args.len() == 2 because NumParams::Exactly(2) validates this + if let (Some(Value::Number(a)), Some(Value::Number(b))) = + (args.get(0), args.get(1)) + { + if let (Some(x), Some(y)) = (a.as_f64(), b.as_f64()) { + return Ok(json!(x * y)); + } + } + Ok(Value::Null) + }, + NumParams::Exactly(2), + )?; + + // Test correct usage + let result = apply(&json!({"multiply": [3, 4]}), &json!({}))?; + println!(" Rule: {{\"multiply\": [3, 4]}} -> {}", result); + + // Test parameter validation - wrong argument count + match apply(&json!({"multiply": [3]}), &json!({})) { + Ok(_) => println!(" ERROR: Should have failed with wrong argument count!"), + Err(Error::WrongArgumentCount { expected, actual }) => { + println!( + " Caught parameter validation error: expected {:?}, got {}", + expected, actual + ); + } + Err(e) => println!(" Caught unexpected error: {}", e), + } + + println!("\n=== Demo Complete ==="); + Ok(()) +} diff --git a/src/error.rs b/src/error.rs index 6b586b7..7640a81 100644 --- a/src/error.rs +++ b/src/error.rs @@ -35,4 +35,7 @@ pub enum Error { #[error("Wrong argument count - expected: {expected:?}, actual: {actual:?}")] WrongArgumentCount { expected: NumParams, actual: usize }, + + #[error("Registry operation failed - {reason}")] + RegistryError { reason: String }, } diff --git a/src/lib.rs b/src/lib.rs index 35103f9..2e3f0d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,14 +7,19 @@ pub mod js_op; mod op; mod value; -use error::Error; +pub use error::Error; +use op::{ + get_custom_operator_registry, DynamicDataOperator, DynamicLazyOperator, + DynamicOperator, +}; +pub use op::{DataOperatorFn, LazyOperatorFn, NumParams, OperatorFn}; use value::{Evaluated, Parsed}; const NULL: Value = Value::Null; trait Parser<'a>: Sized + Into { - fn from_value(value: &'a Value) -> Result, Error>; - fn evaluate(&self, data: &'a Value) -> Result; + fn from_value(value: &'a Value) -> Result, error::Error>; + fn evaluate(&self, data: &'a Value) -> Result; } #[cfg(feature = "wasm")] @@ -84,11 +89,242 @@ pub mod python_iface { /// Run JSONLogic for the given operation and data. /// -pub fn apply(value: &Value, data: &Value) -> Result { +pub fn apply(value: &Value, data: &Value) -> Result { let parsed = Parsed::from_value(&value)?; parsed.evaluate(data).map(Value::from) } +/// Add a custom operation that can be used in JsonLogic rules. +/// +/// This function allows you to register custom operators at runtime that will be +/// available for evaluation. Dynamic operators take precedence over static ones +/// if they share the same name. +/// +/// # Arguments +/// * `name` - The name of the operator (e.g., "my_op") +/// * `operator` - The function that implements the operator logic +/// * `num_params` - Parameter validation rules for the operator +/// +/// # Example +/// ``` +/// use jsonlogic_rs::{add_operation, NumParams, Error}; +/// use serde_json::{json, Value}; +/// +/// // Register a custom "double" operation with proper error handling +/// add_operation("double", |args| { +/// match args.first() { +/// Some(Value::Number(n)) => { +/// if let Some(num) = n.as_f64() { +/// Ok(json!(num * 2.0)) +/// } else { +/// Err(Error::InvalidArgument { +/// value: (*n).clone().into(), +/// operation: "double".to_string(), +/// reason: "Number cannot be converted to f64".to_string(), +/// }) +/// } +/// } +/// Some(other) => Err(Error::InvalidArgument { +/// value: other.clone(), +/// operation: "double".to_string(), +/// reason: "Expected a number".to_string(), +/// }), +/// None => Err(Error::InvalidArgument { +/// value: Value::Null, +/// operation: "double".to_string(), +/// reason: "Missing required argument".to_string(), +/// }), +/// } +/// }, NumParams::Exactly(1)); +/// ``` +pub fn add_operation( + name: &str, + operator: OperatorFn, + num_params: NumParams, +) -> Result<(), Error> { + let registry = get_custom_operator_registry(); + let dynamic_op = DynamicOperator::new(name, operator, num_params); + + registry + .operators + .write() + .map_err(|_| Error::RegistryError { + reason: "Failed to acquire operator registry lock".to_string(), + })? + .insert(name.to_string(), dynamic_op); + + Ok(()) +} + +/// Add a custom lazy operation that can be used in JsonLogic rules. +/// +/// Lazy operators receive the data context and unevaluated arguments, +/// allowing them to control evaluation flow (like `if`, `and`, `or`). +/// +/// # Arguments +/// * `name` - The name of the operator (e.g., "my_lazy_op") +/// * `operator` - The function that implements the operator logic +/// * `num_params` - Parameter validation rules for the operator +/// +/// # Example +/// ``` +/// use jsonlogic_rs::{add_lazy_operation, NumParams}; +/// use serde_json::{json, Value}; +/// +/// // Register a custom "debug" operation that logs and returns the first argument +/// add_lazy_operation("debug", |data, args| { +/// println!("Debug: data={:?}, args={:?}", data, args); +/// Ok(args.first().map(|v| (*v).clone()).unwrap_or(Value::Null)) +/// }, NumParams::AtLeast(1)); +/// ``` +pub fn add_lazy_operation( + name: &str, + operator: LazyOperatorFn, + num_params: NumParams, +) -> Result<(), Error> { + let registry = get_custom_operator_registry(); + let dynamic_op = DynamicLazyOperator::new(name, operator, num_params); + + registry + .lazy_operators + .write() + .map_err(|_| Error::RegistryError { + reason: "Failed to acquire lazy operator registry lock".to_string(), + })? + .insert(name.to_string(), dynamic_op); + + Ok(()) +} + +/// Add a custom data operation that can be used in JsonLogic rules. +/// +/// Data operators receive both the data context and arguments, similar to +/// lazy operators, but are used for data access patterns (like `var`, `missing`). +/// +/// # Arguments +/// * `name` - The name of the operator (e.g., "my_data_op") +/// * `operator` - The function that implements the operator logic +/// * `num_params` - Parameter validation rules for the operator +/// +/// # Example +/// ``` +/// use jsonlogic_rs::{add_data_operation, NumParams}; +/// use serde_json::{json, Value}; +/// +/// // Register a custom "env" operation that reads environment variables +/// add_data_operation("env", |_data, args| { +/// if let Some(Value::String(var_name)) = args.first() { +/// match std::env::var(var_name) { +/// Ok(value) => Ok(json!(value)), +/// Err(_) => Ok(Value::Null), +/// } +/// } else { +/// Ok(Value::Null) +/// } +/// }, NumParams::Exactly(1)); +/// ``` +pub fn add_data_operation( + name: &str, + operator: DataOperatorFn, + num_params: NumParams, +) -> Result<(), Error> { + let registry = get_custom_operator_registry(); + let dynamic_op = DynamicDataOperator::new(name, operator, num_params); + + registry + .data_operators + .write() + .map_err(|_| Error::RegistryError { + reason: "Failed to acquire data operator registry lock".to_string(), + })? + .insert(name.to_string(), dynamic_op); + + Ok(()) +} + +/// Remove a custom operation by name. +/// +/// This function removes a dynamically registered operator from all operator maps. +/// It will not affect built-in static operators. +/// +/// # Arguments +/// * `name` - The name of the operator to remove +/// +/// # Returns +/// * `Ok(true)` if an operator was removed, `Ok(false)` if no operator with that name was found +/// * `Err(Error::RegistryError)` if lock acquisition failed +pub fn remove_operation(name: &str) -> Result { + let registry = get_custom_operator_registry(); + let mut removed = false; + + removed |= registry + .operators + .write() + .map_err(|_| Error::RegistryError { + reason: "Failed to acquire operator registry lock".to_string(), + })? + .remove(name) + .is_some(); + + removed |= registry + .lazy_operators + .write() + .map_err(|_| Error::RegistryError { + reason: "Failed to acquire lazy operator registry lock".to_string(), + })? + .remove(name) + .is_some(); + + removed |= registry + .data_operators + .write() + .map_err(|_| Error::RegistryError { + reason: "Failed to acquire data operator registry lock".to_string(), + })? + .remove(name) + .is_some(); + + Ok(removed) +} + +/// Clear all custom operations. +/// +/// This function removes all dynamically registered operators, but does not +/// affect built-in static operators. +/// +/// # Returns +/// * `Ok(())` if all operations were cleared successfully +/// * `Err(Error::RegistryError)` if lock acquisition failed +pub fn clear_operations() -> Result<(), Error> { + let registry = get_custom_operator_registry(); + + registry + .operators + .write() + .map_err(|_| Error::RegistryError { + reason: "Failed to acquire operator registry lock".to_string(), + })? + .clear(); + + registry + .lazy_operators + .write() + .map_err(|_| Error::RegistryError { + reason: "Failed to acquire lazy operator registry lock".to_string(), + })? + .clear(); + + registry + .data_operators + .write() + .map_err(|_| Error::RegistryError { + reason: "Failed to acquire data operator registry lock".to_string(), + })? + .clear(); + + Ok(()) +} + #[cfg(test)] mod jsonlogic_tests { use super::*; @@ -1400,4 +1636,185 @@ mod jsonlogic_tests { fn test_in_op() { in_cases().into_iter().for_each(assert_jsonlogic) } + + #[test] + fn test_custom_operations() { + // Test custom regular operation + add_operation( + "double", + |args| { + if let Some(Value::Number(n)) = args.first() { + if let Some(num) = n.as_f64() { + return Ok(json!(num * 2.0)); + } + } + Ok(Value::Null) + }, + NumParams::Exactly(1), + ) + .unwrap(); + + let result = apply(&json!({"double": [5]}), &json!({})).unwrap(); + assert_eq!(result, json!(10.0)); + + // Test custom lazy operation + add_lazy_operation( + "first_truthy", + |_data, args| { + for arg in args { + if let Value::Bool(true) = arg { + return Ok(json!(true)); + } + if let Value::Number(n) = arg { + if n.as_f64().unwrap_or(0.0) != 0.0 { + return Ok((*arg).clone()); + } + } + } + Ok(Value::Null) + }, + NumParams::AtLeast(1), + ) + .unwrap(); + + let result = + apply(&json!({"first_truthy": [false, 0, 42, true]}), &json!({})).unwrap(); + assert_eq!(result, json!(42)); + + // Test custom data operation + add_data_operation( + "get_field", + |data, args| { + if let Some(Value::String(field)) = args.first() { + if let Value::Object(obj) = data { + return Ok(obj.get(field).cloned().unwrap_or(Value::Null)); + } + } + Ok(Value::Null) + }, + NumParams::Exactly(1), + ) + .unwrap(); + + let result = + apply(&json!({"get_field": ["name"]}), &json!({"name": "test"})).unwrap(); + assert_eq!(result, json!("test")); + + // Test overriding built-in operators + add_operation( + "==", + |_args| { + // Custom equality that always returns false + Ok(json!(false)) + }, + NumParams::Exactly(2), + ) + .unwrap(); + + let result = apply(&json!({"==": [1, 1]}), &json!({})).unwrap(); + assert_eq!(result, json!(false)); // Our custom operator overrides the built-in + + // Clean up + clear_operations().unwrap(); + + // After clearing, built-in operators should work again + let result = apply(&json!({"==": [1, 1]}), &json!({})).unwrap(); + assert_eq!(result, json!(true)); + } + + #[test] + fn test_error_handling_in_custom_operations() { + // Test that consumers can use the Error type for proper error handling + add_operation( + "test_error_unique", + |args| { + if let Some(Value::String(s)) = args.first() { + if s == "error" { + return Err(Error::InvalidArgument { + value: args[0].clone(), + operation: "test_error_unique".to_string(), + reason: "Custom error message".to_string(), + }); + } + } + Ok(json!("success")) + }, + NumParams::Exactly(1), + ) + .unwrap(); + + // Test successful case + let result = apply(&json!({"test_error_unique": [1]}), &json!({})).unwrap(); + assert_eq!(result, json!("success")); + + // Test custom error case + match apply(&json!({"test_error_unique": ["error"]}), &json!({})) { + Ok(_) => panic!("Should have failed!"), + Err(e) => { + // Verify we can match on the Error type + match e { + Error::InvalidArgument { + operation, reason, .. + } => { + assert_eq!(operation, "test_error_unique"); + assert_eq!(reason, "Custom error message"); + } + _ => panic!("Wrong error type: {:?}", e), + } + } + } + + // Clean up + clear_operations().unwrap(); + } + + #[test] + fn test_wrong_argument_count_error() { + // Register an operation that expects exactly 2 arguments + add_operation( + "test_two_args", + |args| { + if args.len() == 2 { + Ok(json!("success")) + } else { + panic!("Should not reach here - NumParams should validate first") + } + }, + NumParams::Exactly(2), + ) + .unwrap(); + + // Test correct argument count + let result = apply(&json!({"test_two_args": [1, 2]}), &json!({})).unwrap(); + assert_eq!(result, json!("success")); + + // Test wrong argument count - too few + match apply(&json!({"test_two_args": [1]}), &json!({})) { + Ok(_) => panic!("Should have failed with wrong argument count!"), + Err(Error::WrongArgumentCount { expected, actual }) => { + assert_eq!(actual, 1); + match expected { + NumParams::Exactly(n) => assert_eq!(n, 2), + _ => panic!("Expected NumParams::Exactly(2)"), + } + } + Err(e) => panic!("Wrong error type: {:?}", e), + } + + // Test wrong argument count - too many + match apply(&json!({"test_two_args": [1, 2, 3]}), &json!({})) { + Ok(_) => panic!("Should have failed with wrong argument count!"), + Err(Error::WrongArgumentCount { expected, actual }) => { + assert_eq!(actual, 3); + match expected { + NumParams::Exactly(n) => assert_eq!(n, 2), + _ => panic!("Expected NumParams::Exactly(2)"), + } + } + Err(e) => panic!("Wrong error type: {:?}", e), + } + + // Clean up + clear_operations().unwrap(); + } } diff --git a/src/op/mod.rs b/src/op/mod.rs index fa65a31..17c9962 100644 --- a/src/op/mod.rs +++ b/src/op/mod.rs @@ -9,9 +9,12 @@ // as operators. They were originally done differently because there wasn't // yet a LazyOperator concept. +use once_cell::sync::Lazy; use phf::phf_map; use serde_json::{Map, Value}; +use std::collections::HashMap; use std::fmt; +use std::sync::RwLock; use crate::error::Error; use crate::value::to_number_value; @@ -229,6 +232,30 @@ pub const LAZY_OPERATOR_MAP: phf::Map<&'static str, LazyOperator> = phf_map! { }, }; +/// Registry for dynamically registered custom operators +pub struct CustomOperatorRegistry { + pub operators: RwLock>, + pub lazy_operators: RwLock>, + pub data_operators: RwLock>, +} + +impl CustomOperatorRegistry { + fn new() -> Self { + Self { + operators: RwLock::new(HashMap::new()), + lazy_operators: RwLock::new(HashMap::new()), + data_operators: RwLock::new(HashMap::new()), + } + } +} + +static CUSTOM_OPERATOR_REGISTRY: Lazy = + Lazy::new(CustomOperatorRegistry::new); + +pub fn get_custom_operator_registry() -> &'static CustomOperatorRegistry { + &CUSTOM_OPERATOR_REGISTRY +} + #[derive(Debug, Clone)] pub enum NumParams { None, @@ -279,16 +306,44 @@ pub struct Operator { operator: OperatorFn, num_params: NumParams, } + +/// Dynamic version of Operator that can be stored in HashMap +#[derive(Clone, Debug)] +pub struct DynamicOperator { + symbol: String, + operator: OperatorFn, + num_params: NumParams, +} impl Operator { pub fn execute(&self, items: &Vec<&Value>) -> Result { (self.operator)(items) } } + +impl DynamicOperator { + pub fn new(symbol: &str, operator: OperatorFn, num_params: NumParams) -> Self { + Self { + symbol: symbol.to_string(), + operator, + num_params, + } + } + + pub fn execute(&self, items: &Vec<&Value>) -> Result { + (self.operator)(items) + } +} impl CommonOperator for Operator { fn param_info(&self) -> &NumParams { &self.num_params } } + +impl CommonOperator for DynamicOperator { + fn param_info(&self) -> &NumParams { + &self.num_params + } +} impl fmt::Debug for Operator { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Operator") @@ -303,16 +358,44 @@ pub struct LazyOperator { operator: LazyOperatorFn, num_params: NumParams, } + +/// Dynamic version of LazyOperator that can be stored in HashMap +#[derive(Clone, Debug)] +pub struct DynamicLazyOperator { + symbol: String, + operator: LazyOperatorFn, + num_params: NumParams, +} impl LazyOperator { pub fn execute(&self, data: &Value, items: &Vec<&Value>) -> Result { (self.operator)(data, items) } } + +impl DynamicLazyOperator { + pub fn new(symbol: &str, operator: LazyOperatorFn, num_params: NumParams) -> Self { + Self { + symbol: symbol.to_string(), + operator, + num_params, + } + } + + pub fn execute(&self, data: &Value, items: &Vec<&Value>) -> Result { + (self.operator)(data, items) + } +} impl CommonOperator for LazyOperator { fn param_info(&self) -> &NumParams { &self.num_params } } + +impl CommonOperator for DynamicLazyOperator { + fn param_info(&self) -> &NumParams { + &self.num_params + } +} impl fmt::Debug for LazyOperator { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Operator") @@ -332,16 +415,44 @@ pub struct DataOperator { operator: DataOperatorFn, num_params: NumParams, } + +/// Dynamic version of DataOperator that can be stored in HashMap +#[derive(Clone, Debug)] +pub struct DynamicDataOperator { + symbol: String, + operator: DataOperatorFn, + num_params: NumParams, +} impl DataOperator { pub fn execute(&self, data: &Value, items: &Vec<&Value>) -> Result { (self.operator)(data, items) } } + +impl DynamicDataOperator { + pub fn new(symbol: &str, operator: DataOperatorFn, num_params: NumParams) -> Self { + Self { + symbol: symbol.to_string(), + operator, + num_params, + } + } + + pub fn execute(&self, data: &Value, items: &Vec<&Value>) -> Result { + (self.operator)(data, items) + } +} impl CommonOperator for DataOperator { fn param_info(&self) -> &NumParams { &self.num_params } } + +impl CommonOperator for DynamicDataOperator { + fn param_info(&self) -> &NumParams { + &self.num_params + } +} impl fmt::Debug for DataOperator { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Operator") @@ -351,132 +462,238 @@ impl fmt::Debug for DataOperator { } } -type OperatorFn = fn(&Vec<&Value>) -> Result; -type LazyOperatorFn = fn(&Value, &Vec<&Value>) -> Result; -type DataOperatorFn = fn(&Value, &Vec<&Value>) -> Result; +pub type OperatorFn = fn(&Vec<&Value>) -> Result; +pub type LazyOperatorFn = fn(&Value, &Vec<&Value>) -> Result; +pub type DataOperatorFn = fn(&Value, &Vec<&Value>) -> Result; /// An operation that doesn't do any recursive parsing or evaluation. /// /// Any operator functions used must handle parsing of values themselves. #[derive(Debug)] -pub struct LazyOperation<'a> { - operator: &'a LazyOperator, - arguments: Vec, +pub enum LazyOperation<'a> { + Static { + operator: &'a LazyOperator, + arguments: Vec, + }, + Dynamic { + operator: DynamicLazyOperator, + arguments: Vec, + }, } impl<'a> Parser<'a> for LazyOperation<'a> { fn from_value(value: &'a Value) -> Result, Error> { - op_from_map(&LAZY_OPERATOR_MAP, value).and_then(|opt| { - opt.map(|op| { - Ok(LazyOperation { - operator: op.op, - arguments: op.args.into_iter().map(|v| v.clone()).collect(), - }) - }) - .transpose() - }) + let registry = get_custom_operator_registry(); + + match op_from_static_and_dynamic( + &LAZY_OPERATOR_MAP, + ®istry.lazy_operators, + value, + )? { + Some(Either::Left(op)) => Ok(Some(LazyOperation::Static { + operator: op.op, + arguments: op.args.into_iter().map(|v| v.clone()).collect(), + })), + Some(Either::Right(op)) => Ok(Some(LazyOperation::Dynamic { + operator: op.op, + arguments: op.args.into_iter().map(|v| v.clone()).collect(), + })), + None => Ok(None), + } } fn evaluate(&self, data: &'a Value) -> Result { - self.operator - .execute(data, &self.arguments.iter().collect()) - .map(Evaluated::New) + match self { + LazyOperation::Static { + operator, + arguments, + } => operator.execute(data, &arguments.iter().collect()), + LazyOperation::Dynamic { + operator, + arguments, + } => operator.execute(data, &arguments.iter().collect()), + } + .map(Evaluated::New) } } impl From> for Value { fn from(op: LazyOperation) -> Value { let mut rv = Map::with_capacity(1); - rv.insert( - op.operator.symbol.into(), - Value::Array(op.arguments.clone()), - ); + match op { + LazyOperation::Static { + operator, + arguments, + } => { + rv.insert(operator.symbol.into(), Value::Array(arguments)); + } + LazyOperation::Dynamic { + operator, + arguments, + } => { + rv.insert(operator.symbol.clone(), Value::Array(arguments)); + } + } Value::Object(rv) } } #[derive(Debug)] -pub struct Operation<'a> { - operator: &'a Operator, - arguments: Vec>, +pub enum Operation<'a> { + Static { + operator: &'a Operator, + arguments: Vec>, + }, + Dynamic { + operator: DynamicOperator, + arguments: Vec>, + }, } impl<'a> Parser<'a> for Operation<'a> { fn from_value(value: &'a Value) -> Result, Error> { - op_from_map(&OPERATOR_MAP, value).and_then(|opt| { - opt.map(|op| { - Ok(Operation { - operator: op.op, - arguments: Parsed::from_values(op.args)?, - }) - }) - .transpose() - }) + let registry = get_custom_operator_registry(); + + match op_from_static_and_dynamic(&OPERATOR_MAP, ®istry.operators, value)? { + Some(Either::Left(op)) => Ok(Some(Operation::Static { + operator: op.op, + arguments: Parsed::from_values(op.args)?, + })), + Some(Either::Right(op)) => Ok(Some(Operation::Dynamic { + operator: op.op, + arguments: Parsed::from_values(op.args)?, + })), + None => Ok(None), + } } /// Evaluate the operation after recursively evaluating any nested operations fn evaluate(&self, data: &'a Value) -> Result { - let arguments = self - .arguments - .iter() - .map(|value| value.evaluate(data).map(Value::from)) - .collect::, Error>>()?; - self.operator - .execute(&arguments.iter().collect()) - .map(Evaluated::New) + let arguments = match self { + Operation::Static { arguments, .. } + | Operation::Dynamic { arguments, .. } => arguments, + } + .iter() + .map(|value| value.evaluate(data).map(Value::from)) + .collect::, Error>>()?; + + match self { + Operation::Static { operator, .. } => { + operator.execute(&arguments.iter().collect()) + } + Operation::Dynamic { operator, .. } => { + operator.execute(&arguments.iter().collect()) + } + } + .map(Evaluated::New) } } impl From> for Value { fn from(op: Operation) -> Value { let mut rv = Map::with_capacity(1); - let values = op - .arguments - .into_iter() - .map(Value::from) - .collect::>(); - rv.insert(op.operator.symbol.into(), Value::Array(values)); + match op { + Operation::Static { + operator, + arguments, + } => { + let values = arguments + .into_iter() + .map(Value::from) + .collect::>(); + rv.insert(operator.symbol.into(), Value::Array(values)); + } + Operation::Dynamic { + operator, + arguments, + } => { + let values = arguments + .into_iter() + .map(Value::from) + .collect::>(); + rv.insert(operator.symbol.clone(), Value::Array(values)); + } + } Value::Object(rv) } } #[derive(Debug)] -pub struct DataOperation<'a> { - operator: &'a DataOperator, - arguments: Vec>, +pub enum DataOperation<'a> { + Static { + operator: &'a DataOperator, + arguments: Vec>, + }, + Dynamic { + operator: DynamicDataOperator, + arguments: Vec>, + }, } impl<'a> Parser<'a> for DataOperation<'a> { fn from_value(value: &'a Value) -> Result, Error> { - op_from_map(&DATA_OPERATOR_MAP, value).and_then(|opt| { - opt.map(|op| { - Ok(DataOperation { - operator: op.op, - arguments: Parsed::from_values(op.args)?, - }) - }) - .transpose() - }) + let registry = get_custom_operator_registry(); + + match op_from_static_and_dynamic( + &DATA_OPERATOR_MAP, + ®istry.data_operators, + value, + )? { + Some(Either::Left(op)) => Ok(Some(DataOperation::Static { + operator: op.op, + arguments: Parsed::from_values(op.args)?, + })), + Some(Either::Right(op)) => Ok(Some(DataOperation::Dynamic { + operator: op.op, + arguments: Parsed::from_values(op.args)?, + })), + None => Ok(None), + } } /// Evaluate the operation after recursively evaluating any nested operations fn evaluate(&self, data: &'a Value) -> Result { - let arguments = self - .arguments - .iter() - .map(|value| value.evaluate(data).map(Value::from)) - .collect::, Error>>()?; - self.operator - .execute(data, &arguments.iter().collect()) - .map(Evaluated::New) + let arguments = match self { + DataOperation::Static { arguments, .. } + | DataOperation::Dynamic { arguments, .. } => arguments, + } + .iter() + .map(|value| value.evaluate(data).map(Value::from)) + .collect::, Error>>()?; + + match self { + DataOperation::Static { operator, .. } => { + operator.execute(data, &arguments.iter().collect()) + } + DataOperation::Dynamic { operator, .. } => { + operator.execute(data, &arguments.iter().collect()) + } + } + .map(Evaluated::New) } } impl From> for Value { fn from(op: DataOperation) -> Value { let mut rv = Map::with_capacity(1); - let values = op - .arguments - .into_iter() - .map(Value::from) - .collect::>(); - rv.insert(op.operator.symbol.into(), Value::Array(values)); + match op { + DataOperation::Static { + operator, + arguments, + } => { + let values = arguments + .into_iter() + .map(Value::from) + .collect::>(); + rv.insert(operator.symbol.into(), Value::Array(values)); + } + DataOperation::Dynamic { + operator, + arguments, + } => { + let values = arguments + .into_iter() + .map(Value::from) + .collect::>(); + rv.insert(operator.symbol.clone(), Value::Array(values)); + } + } Value::Object(rv) } } @@ -486,6 +703,105 @@ struct OpArgs<'a, 'b, T> { args: Vec<&'b Value>, } +struct DynamicOpArgs<'b, T> { + op: T, + args: Vec<&'b Value>, +} + +/// Enhanced lookup that checks both static PHF maps and dynamic HashMaps +fn op_from_static_and_dynamic<'a, 'b, T, D>( + static_map: &'a phf::Map<&'static str, T>, + dynamic_map: &'a RwLock>, + value: &'b Value, +) -> Result, DynamicOpArgs<'b, D>>>, Error> +where + T: CommonOperator, + D: CommonOperator + Clone, +{ + let obj = match value { + Value::Object(obj) => obj, + _ => return Ok(None), + }; + // With just one key. + if obj.len() != 1 { + return Ok(None); + }; + + // We've already validated the length to be one, so any error + // here is super unexpected. + let key = obj.keys().next().ok_or_else(|| { + Error::UnexpectedError(format!( + "could not get first key from len(1) object: {:?}", + obj + )) + })?; + let val = obj.get(key).ok_or_else(|| { + Error::UnexpectedError(format!( + "could not get value for key '{}' from len(1) object: {:?}", + key, obj + )) + })?; + + // First check dynamic operators (they take precedence) + let dynamic_ops = dynamic_map.read().map_err(|_| { + Error::UnexpectedError( + "Failed to acquire read lock on dynamic operators".to_string(), + ) + })?; + + if let Some(op) = dynamic_ops.get(key) { + let param_info = op.param_info(); + let args = extract_args(val, param_info, key)?; + return Ok(Some(Either::Right(DynamicOpArgs { + op: op.clone(), + args, + }))); + } + + drop(dynamic_ops); // Release the lock early + + // Then check static operators + if let Some(op) = static_map.get(key.as_str()) { + let param_info = op.param_info(); + let args = extract_args(val, param_info, key)?; + return Ok(Some(Either::Left(OpArgs { op, args }))); + } + + Ok(None) +} + +fn extract_args<'a>( + val: &'a Value, + param_info: &NumParams, + key: &str, +) -> Result, Error> { + let err_for_non_unary = || { + Err(Error::InvalidOperation { + key: key.to_string(), + reason: "Arguments to non-unary operations must be arrays".into(), + }) + }; + + // If args value is not an array, and the operator is unary, + // the value is treated as a unary argument array. + let args = match val { + Value::Array(args) => args.iter().collect::>(), + _ => match param_info.can_accept_unary() { + true => vec![val], + false => return err_for_non_unary(), + }, + }; + + param_info.check_len(&args.len())?; + Ok(args) +} + +/// Helper enum to handle either static or dynamic operator results +enum Either { + Left(L), + Right(R), +} + fn op_from_map<'a, 'b, T: CommonOperator>( map: &'a phf::Map<&'static str, T>, value: &'b Value,