Skip to main content

datalogic_rs/
engine.rs

1use serde_json::Value;
2use std::borrow::Cow;
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use crate::config::EvaluationConfig;
7use crate::operators::variable;
8use crate::trace::{ExpressionNode, TraceCollector, TracedResult};
9use crate::{CompiledLogic, CompiledNode, ContextStack, Error, Evaluator, Operator, Result};
10
11/// The main DataLogic engine for compiling and evaluating JSONLogic expressions.
12///
13/// The engine provides a two-phase approach to logic evaluation:
14/// 1. **Compilation**: Parse JSON logic into optimized `CompiledLogic`
15/// 2. **Evaluation**: Execute compiled logic against data
16///
17/// # Features
18///
19/// - **Thread-safe**: Compiled logic can be shared across threads with `Arc`
20/// - **Extensible**: Add custom operators via `add_operator`
21/// - **Structure preservation**: Optionally preserve object structure for templating
22/// - **OpCode dispatch**: Built-in operators use fast enum-based dispatch
23///
24/// # Example
25///
26/// ```rust
27/// use datalogic_rs::DataLogic;
28/// use serde_json::json;
29///
30/// let engine = DataLogic::new();
31/// let logic = json!({">": [{"var": "age"}, 18]});
32/// let compiled = engine.compile(&logic).unwrap();
33///
34/// let data = json!({"age": 21});
35/// let result = engine.evaluate_owned(&compiled, data).unwrap();
36/// assert_eq!(result, json!(true));
37/// ```
38pub struct DataLogic {
39    // No more builtin_operators array - OpCode handles dispatch directly!
40    /// HashMap for custom operators only
41    custom_operators: HashMap<String, Box<dyn Operator>>,
42    /// Flag to preserve structure of objects with unknown operators
43    preserve_structure: bool,
44    /// Configuration for evaluation behavior
45    config: EvaluationConfig,
46}
47
48impl Default for DataLogic {
49    fn default() -> Self {
50        Self::new()
51    }
52}
53
54impl DataLogic {
55    /// Creates a new DataLogic engine with all built-in operators.
56    ///
57    /// The engine includes 50+ built-in operators optimized with OpCode dispatch.
58    /// Structure preservation is disabled by default.
59    ///
60    /// # Example
61    ///
62    /// ```rust
63    /// use datalogic_rs::DataLogic;
64    ///
65    /// let engine = DataLogic::new();
66    /// ```
67    pub fn new() -> Self {
68        Self {
69            custom_operators: HashMap::new(),
70            preserve_structure: false,
71            config: EvaluationConfig::default(),
72        }
73    }
74
75    /// Creates a new DataLogic engine with structure preservation enabled.
76    ///
77    /// When enabled, objects with unknown operators are preserved as structured
78    /// templates, allowing for dynamic object generation. Custom operators
79    /// registered via `add_operator` are recognized and evaluated properly,
80    /// even within structured objects.
81    ///
82    /// # Example
83    ///
84    /// ```rust
85    /// use datalogic_rs::DataLogic;
86    /// use serde_json::json;
87    ///
88    /// let engine = DataLogic::with_preserve_structure();
89    /// let logic = json!({
90    ///     "name": {"var": "user.name"},
91    ///     "score": {"+": [{"var": "base"}, {"var": "bonus"}]}
92    /// });
93    /// // Returns: {"name": "Alice", "score": 95}
94    /// ```
95    ///
96    /// # Custom Operators with Preserve Structure
97    ///
98    /// Custom operators work seamlessly in preserve_structure mode:
99    ///
100    /// ```rust
101    /// use datalogic_rs::{DataLogic, Operator, ContextStack, Evaluator, Result, Error};
102    /// use serde_json::{json, Value};
103    /// use std::sync::Arc;
104    ///
105    /// struct UpperOperator;
106    /// impl Operator for UpperOperator {
107    ///     fn evaluate(&self, args: &[Value], context: &mut ContextStack,
108    ///                 evaluator: &dyn Evaluator) -> Result<Value> {
109    ///         let val = evaluator.evaluate(&args[0], context)?;
110    ///         Ok(json!(val.as_str().unwrap_or("").to_uppercase()))
111    ///     }
112    /// }
113    ///
114    /// let mut engine = DataLogic::with_preserve_structure();
115    /// engine.add_operator("upper".to_string(), Box::new(UpperOperator));
116    ///
117    /// let logic = json!({
118    ///     "message": {"upper": {"var": "text"}},
119    ///     "count": {"var": "num"}
120    /// });
121    /// let compiled = engine.compile(&logic).unwrap();
122    /// let result = engine.evaluate(&compiled, Arc::new(json!({"text": "hello", "num": 5}))).unwrap();
123    /// // Returns: {"message": "HELLO", "count": 5}
124    /// ```
125    pub fn with_preserve_structure() -> Self {
126        Self {
127            custom_operators: HashMap::new(),
128            preserve_structure: true,
129            config: EvaluationConfig::default(),
130        }
131    }
132
133    /// Creates a new DataLogic engine with a custom configuration.
134    ///
135    /// # Arguments
136    ///
137    /// * `config` - The evaluation configuration
138    ///
139    /// # Example
140    ///
141    /// ```rust
142    /// use datalogic_rs::{DataLogic, EvaluationConfig, NanHandling};
143    ///
144    /// let config = EvaluationConfig::default()
145    ///     .with_nan_handling(NanHandling::IgnoreValue);
146    /// let engine = DataLogic::with_config(config);
147    /// ```
148    pub fn with_config(config: EvaluationConfig) -> Self {
149        Self {
150            custom_operators: HashMap::new(),
151            preserve_structure: false,
152            config,
153        }
154    }
155
156    /// Creates a new DataLogic engine with both configuration and structure preservation.
157    ///
158    /// # Arguments
159    ///
160    /// * `config` - The evaluation configuration
161    /// * `preserve_structure` - Whether to preserve object structure
162    ///
163    /// # Example
164    ///
165    /// ```rust
166    /// use datalogic_rs::{DataLogic, EvaluationConfig, NanHandling};
167    ///
168    /// let config = EvaluationConfig::default()
169    ///     .with_nan_handling(NanHandling::IgnoreValue);
170    /// let engine = DataLogic::with_config_and_structure(config, true);
171    /// ```
172    pub fn with_config_and_structure(config: EvaluationConfig, preserve_structure: bool) -> Self {
173        Self {
174            custom_operators: HashMap::new(),
175            preserve_structure,
176            config,
177        }
178    }
179
180    /// Gets a reference to the current evaluation configuration.
181    pub fn config(&self) -> &EvaluationConfig {
182        &self.config
183    }
184
185    /// Returns whether structure preservation is enabled.
186    pub fn preserve_structure(&self) -> bool {
187        self.preserve_structure
188    }
189
190    /// Registers a custom operator with the engine.
191    ///
192    /// Custom operators extend the engine's functionality with domain-specific logic.
193    /// They override built-in operators if the same name is used.
194    ///
195    /// # Arguments
196    ///
197    /// * `name` - The operator name (e.g., "custom_calc")
198    /// * `operator` - The operator implementation
199    ///
200    /// # Example
201    ///
202    /// ```rust
203    /// use datalogic_rs::{DataLogic, Operator, ContextStack, Evaluator, Result, Error};
204    /// use serde_json::{json, Value};
205    ///
206    /// struct DoubleOperator;
207    ///
208    /// impl Operator for DoubleOperator {
209    ///     fn evaluate(
210    ///         &self,
211    ///         args: &[Value],
212    ///         _context: &mut ContextStack,
213    ///         _evaluator: &dyn Evaluator,
214    ///     ) -> Result<Value> {
215    ///         if let Some(n) = args.first().and_then(|v| v.as_f64()) {
216    ///             Ok(json!(n * 2.0))
217    ///         } else {
218    ///             Err(Error::InvalidArguments("Argument must be a number".to_string()))
219    ///         }
220    ///     }
221    /// }
222    ///
223    /// let mut engine = DataLogic::new();
224    /// engine.add_operator("double".to_string(), Box::new(DoubleOperator));
225    /// ```
226    pub fn add_operator(&mut self, name: String, operator: Box<dyn Operator>) {
227        self.custom_operators.insert(name, operator);
228    }
229
230    /// Checks if a custom operator with the given name is registered.
231    ///
232    /// # Arguments
233    ///
234    /// * `name` - The operator name to check
235    ///
236    /// # Returns
237    ///
238    /// `true` if the operator exists, `false` otherwise.
239    pub fn has_custom_operator(&self, name: &str) -> bool {
240        self.custom_operators.contains_key(name)
241    }
242
243    /// Compiles a JSON logic expression into an optimized form.
244    ///
245    /// Compilation performs:
246    /// - Static evaluation of constant expressions
247    /// - OpCode assignment for built-in operators
248    /// - Structure analysis for templating
249    ///
250    /// The returned `Arc<CompiledLogic>` can be safely shared across threads.
251    ///
252    /// # Arguments
253    ///
254    /// * `logic` - The JSON logic expression to compile
255    ///
256    /// # Returns
257    ///
258    /// An `Arc`-wrapped compiled logic structure, or an error if compilation fails.
259    ///
260    /// # Example
261    ///
262    /// ```rust
263    /// use datalogic_rs::DataLogic;
264    /// use serde_json::json;
265    /// use std::sync::Arc;
266    ///
267    /// let engine = DataLogic::new();
268    /// let logic = json!({"==": [1, 1]});
269    /// let compiled: Arc<_> = engine.compile(&logic).unwrap();
270    /// ```
271    pub fn compile(&self, logic: &Value) -> Result<Arc<CompiledLogic>> {
272        let compiled = CompiledLogic::compile_with_static_eval(logic, self)?;
273        Ok(Arc::new(compiled))
274    }
275
276    /// Evaluates compiled logic with Arc-wrapped data.
277    ///
278    /// Use this method when you already have data in an `Arc` to avoid re-wrapping.
279    /// For owned data, use `evaluate_owned` instead.
280    ///
281    /// # Arguments
282    ///
283    /// * `compiled` - The compiled logic to evaluate
284    /// * `data` - The data context wrapped in an `Arc`
285    ///
286    /// # Returns
287    ///
288    /// The evaluation result, or an error if evaluation fails.
289    pub fn evaluate(&self, compiled: &CompiledLogic, data: Arc<Value>) -> Result<Value> {
290        let mut context = ContextStack::new(data);
291        self.evaluate_node(&compiled.root, &mut context)
292    }
293
294    /// Evaluates compiled logic with owned data.
295    ///
296    /// This is a convenience method that wraps the data in an `Arc` before evaluation.
297    /// If you already have Arc-wrapped data, use `evaluate` instead.
298    ///
299    /// # Arguments
300    ///
301    /// * `compiled` - The compiled logic to evaluate
302    /// * `data` - The owned data context
303    ///
304    /// # Returns
305    ///
306    /// The evaluation result, or an error if evaluation fails.
307    ///
308    /// # Example
309    ///
310    /// ```rust
311    /// use datalogic_rs::DataLogic;
312    /// use serde_json::json;
313    ///
314    /// let engine = DataLogic::new();
315    /// let logic = json!({"var": "name"});
316    /// let compiled = engine.compile(&logic).unwrap();
317    ///
318    /// let data = json!({"name": "Alice"});
319    /// let result = engine.evaluate_owned(&compiled, data).unwrap();
320    /// assert_eq!(result, json!("Alice"));
321    /// ```
322    pub fn evaluate_owned(&self, compiled: &CompiledLogic, data: Value) -> Result<Value> {
323        self.evaluate(compiled, Arc::new(data))
324    }
325
326    /// Convenience method for evaluating JSON strings directly.
327    ///
328    /// This method combines compilation and evaluation in a single call.
329    /// For repeated evaluations, compile once and reuse the compiled logic.
330    ///
331    /// # Arguments
332    ///
333    /// * `logic` - JSON logic as a string
334    /// * `data` - Data context as a JSON string
335    ///
336    /// # Returns
337    ///
338    /// The evaluation result, or an error if parsing or evaluation fails.
339    ///
340    /// # Example
341    ///
342    /// ```rust
343    /// use datalogic_rs::DataLogic;
344    ///
345    /// let engine = DataLogic::new();
346    /// let result = engine.evaluate_json(
347    ///     r#"{"==": [{"var": "x"}, 5]}"#,
348    ///     r#"{"x": 5}"#
349    /// ).unwrap();
350    /// assert_eq!(result, serde_json::json!(true));
351    /// ```
352    pub fn evaluate_json(&self, logic: &str, data: &str) -> Result<Value> {
353        let logic_value: Value = serde_json::from_str(logic)?;
354        let data_value: Value = serde_json::from_str(data)?;
355        let data_arc = Arc::new(data_value);
356
357        let compiled = self.compile(&logic_value)?;
358        self.evaluate(&compiled, data_arc)
359    }
360
361    /// Evaluates a compiled node using OpCode dispatch.
362    ///
363    /// This is the core evaluation method that handles:
364    /// - Static values
365    /// - Arrays
366    /// - Built-in operators (via OpCode)
367    /// - Custom operators
368    /// - Structured objects (in preserve mode)
369    ///
370    /// # Arguments
371    ///
372    /// * `node` - The compiled node to evaluate
373    /// * `context` - The context stack containing data and metadata
374    ///
375    /// # Returns
376    ///
377    /// The evaluation result, or an error if evaluation fails.
378    #[inline]
379    pub fn evaluate_node(&self, node: &CompiledNode, context: &mut ContextStack) -> Result<Value> {
380        match node {
381            CompiledNode::Value { value, .. } => Ok(value.clone()),
382
383            CompiledNode::Array { nodes, .. } => {
384                let mut results = Vec::with_capacity(nodes.len());
385                for node in nodes.iter() {
386                    results.push(self.evaluate_node(node, context)?);
387                }
388                Ok(Value::Array(results))
389            }
390
391            CompiledNode::BuiltinOperator { opcode, args, .. } => {
392                // Direct OpCode dispatch with CompiledNode args
393                opcode.evaluate_direct(args, context, self)
394            }
395
396            CompiledNode::CustomOperator(data) => {
397                // Custom operators still use dynamic dispatch
398                let operator = self
399                    .custom_operators
400                    .get(&data.name)
401                    .ok_or_else(|| Error::InvalidOperator(data.name.clone()))?;
402
403                let arg_values: Vec<Value> = data.args.iter().map(node_to_value).collect();
404                let evaluator = SimpleEvaluator::new(self);
405
406                operator.evaluate(&arg_values, context, &evaluator)
407            }
408
409            CompiledNode::StructuredObject(data) => {
410                let mut result = serde_json::Map::new();
411                for (key, node) in data.fields.iter() {
412                    let value = self.evaluate_node(node, context)?;
413                    result.insert(key.clone(), value);
414                }
415                Ok(Value::Object(result))
416            }
417
418            CompiledNode::CompiledVar {
419                scope_level,
420                segments,
421                reduce_hint,
422                metadata_hint,
423                default_value,
424            } => variable::evaluate_compiled_var(
425                *scope_level,
426                segments,
427                *reduce_hint,
428                *metadata_hint,
429                default_value.as_deref(),
430                context,
431                self,
432            ),
433
434            CompiledNode::CompiledExists(data) => {
435                variable::evaluate_compiled_exists(data.scope_level, &data.segments, context)
436            }
437
438            CompiledNode::CompiledSplitRegex(data) => {
439                use crate::operators::string;
440                string::evaluate_split_with_regex(
441                    &data.args,
442                    context,
443                    self,
444                    &data.regex,
445                    &data.capture_names,
446                )
447            }
448
449            CompiledNode::CompiledThrow(error_obj) => {
450                Err(Error::Thrown(error_obj.as_ref().clone()))
451            }
452        }
453    }
454
455    /// Evaluate a compiled node, returning a `Cow` to avoid cloning literal values.
456    ///
457    /// For `CompiledNode::Value` nodes (constants/literals), returns a borrowed reference
458    /// to the pre-compiled value without cloning. For all other node types, performs full
459    /// evaluation and returns the owned result.
460    #[inline]
461    pub fn evaluate_node_cow<'a>(
462        &self,
463        node: &'a CompiledNode,
464        context: &mut ContextStack,
465    ) -> Result<Cow<'a, Value>> {
466        match node {
467            CompiledNode::Value { value, .. } => Ok(Cow::Borrowed(value)),
468            _ => self.evaluate_node(node, context).map(Cow::Owned),
469        }
470    }
471
472    /// Evaluate JSON logic with execution trace for debugging.
473    ///
474    /// This method compiles and evaluates JSONLogic while recording each
475    /// execution step for replay in debugging UIs.
476    ///
477    /// # Arguments
478    ///
479    /// * `logic` - JSON logic as a string
480    /// * `data` - Data context as a JSON string
481    ///
482    /// # Returns
483    ///
484    /// A `TracedResult` containing the result, expression tree, and execution steps.
485    ///
486    /// # Example
487    ///
488    /// ```rust
489    /// use datalogic_rs::DataLogic;
490    ///
491    /// let engine = DataLogic::new();
492    /// let result = engine.evaluate_json_with_trace(
493    ///     r#"{">=": [{"var": "age"}, 18]}"#,
494    ///     r#"{"age": 25}"#
495    /// ).unwrap();
496    ///
497    /// println!("Result: {}", result.result);
498    /// println!("Steps: {}", result.steps.len());
499    /// ```
500    pub fn evaluate_json_with_trace(&self, logic: &str, data: &str) -> Result<TracedResult> {
501        let logic_value: Value = serde_json::from_str(logic)?;
502        let data_value: Value = serde_json::from_str(data)?;
503        let data_arc = Arc::new(data_value);
504
505        // Use compile_for_trace to avoid static evaluation, which would collapse
506        // operators into values and eliminate trace steps
507        let compiled = Arc::new(CompiledLogic::compile_for_trace(
508            &logic_value,
509            self.preserve_structure(),
510        )?);
511
512        // Build expression tree and node ID mapping
513        let (expression_tree, node_id_map) = ExpressionNode::build_from_compiled(&compiled.root);
514
515        // Create context and trace collector
516        let mut context = ContextStack::new(data_arc);
517        let mut collector = TraceCollector::new();
518
519        // Evaluate with tracing
520        let result =
521            self.evaluate_node_traced(&compiled.root, &mut context, &mut collector, &node_id_map);
522
523        match result {
524            Ok(value) => Ok(TracedResult {
525                result: value,
526                expression_tree,
527                steps: collector.into_steps(),
528                error: None,
529            }),
530            Err(e) => {
531                // Return error but include partial steps for debugging
532                Ok(TracedResult {
533                    result: Value::Null,
534                    expression_tree,
535                    steps: collector.into_steps(),
536                    error: Some(e.to_string()),
537                })
538            }
539        }
540    }
541
542    /// Evaluate a compiled node with tracing.
543    ///
544    /// This method records each step of the evaluation for debugging.
545    pub fn evaluate_node_traced(
546        &self,
547        node: &CompiledNode,
548        context: &mut ContextStack,
549        collector: &mut TraceCollector,
550        node_id_map: &HashMap<usize, u32>,
551    ) -> Result<Value> {
552        let node_ptr = node as *const CompiledNode as usize;
553        let node_id = node_id_map.get(&node_ptr).copied().unwrap_or(0);
554        let current_context = context.current().data().clone();
555
556        match node {
557            CompiledNode::Value { value, .. } => {
558                // Literal values don't generate steps per the proposal
559                Ok(value.clone())
560            }
561
562            CompiledNode::Array { nodes, .. } => {
563                let mut results = Vec::with_capacity(nodes.len());
564                for node in nodes.iter() {
565                    match self.evaluate_node_traced(node, context, collector, node_id_map) {
566                        Ok(val) => results.push(val),
567                        Err(err) => {
568                            collector.record_error(node_id, current_context, err.to_string());
569                            return Err(err);
570                        }
571                    }
572                }
573                let result = Value::Array(results);
574                collector.record_step(node_id, current_context, result.clone());
575                Ok(result)
576            }
577
578            CompiledNode::BuiltinOperator { opcode, args, .. } => {
579                // Use traced dispatch for operators that need special handling
580                match opcode.evaluate_traced(args, context, self, collector, node_id_map) {
581                    Ok(result) => {
582                        collector.record_step(node_id, current_context, result.clone());
583                        Ok(result)
584                    }
585                    Err(err) => {
586                        collector.record_error(node_id, current_context, err.to_string());
587                        Err(err)
588                    }
589                }
590            }
591
592            CompiledNode::CustomOperator(data) => {
593                let operator = self
594                    .custom_operators
595                    .get(&data.name)
596                    .ok_or_else(|| Error::InvalidOperator(data.name.clone()))?;
597
598                let arg_values: Vec<Value> = data.args.iter().map(node_to_value).collect();
599                let evaluator = SimpleEvaluator::new(self);
600
601                match operator.evaluate(&arg_values, context, &evaluator) {
602                    Ok(result) => {
603                        collector.record_step(node_id, current_context, result.clone());
604                        Ok(result)
605                    }
606                    Err(err) => {
607                        collector.record_error(node_id, current_context, err.to_string());
608                        Err(err)
609                    }
610                }
611            }
612
613            CompiledNode::StructuredObject(data) => {
614                let mut result = serde_json::Map::new();
615                for (key, node) in data.fields.iter() {
616                    match self.evaluate_node_traced(node, context, collector, node_id_map) {
617                        Ok(value) => {
618                            result.insert(key.clone(), value);
619                        }
620                        Err(err) => {
621                            collector.record_error(node_id, current_context, err.to_string());
622                            return Err(err);
623                        }
624                    }
625                }
626                let result = Value::Object(result);
627                collector.record_step(node_id, current_context, result.clone());
628                Ok(result)
629            }
630
631            CompiledNode::CompiledVar { .. }
632            | CompiledNode::CompiledExists(_)
633            | CompiledNode::CompiledSplitRegex(_)
634            | CompiledNode::CompiledThrow(_) => match self.evaluate_node(node, context) {
635                Ok(result) => {
636                    collector.record_step(node_id, current_context, result.clone());
637                    Ok(result)
638                }
639                Err(err) => {
640                    collector.record_error(node_id, current_context, err.to_string());
641                    Err(err)
642                }
643            },
644        }
645    }
646}
647
648// node_to_value, segments_to_dot_path, and segment_to_value are in node.rs
649use crate::node::node_to_value;
650
651/// Simple evaluator that compiles and evaluates without caching
652struct SimpleEvaluator<'e> {
653    engine: &'e DataLogic,
654}
655
656impl<'e> SimpleEvaluator<'e> {
657    /// Create a new SimpleEvaluator
658    fn new(engine: &'e DataLogic) -> Self {
659        Self { engine }
660    }
661}
662
663impl Evaluator for SimpleEvaluator<'_> {
664    fn evaluate(&self, logic: &Value, context: &mut ContextStack) -> Result<Value> {
665        // Compile and evaluate - compilation already handles simple values efficiently
666        match logic {
667            Value::Object(obj) if obj.len() == 1 => {
668                let compiled = CompiledLogic::compile_with_static_eval(logic, self.engine)?;
669                self.engine.evaluate_node(&compiled.root, context)
670            }
671            Value::Object(obj) if obj.len() > 1 && self.engine.preserve_structure => {
672                // Multi-key object in preserve_structure mode
673                let compiled = CompiledLogic::compile_with_static_eval(logic, self.engine)?;
674                self.engine.evaluate_node(&compiled.root, context)
675            }
676            Value::Array(_) => {
677                let compiled = CompiledLogic::compile_with_static_eval(logic, self.engine)?;
678                self.engine.evaluate_node(&compiled.root, context)
679            }
680            _ => Ok(logic.clone()),
681        }
682    }
683}