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}