json_eval_rs/jsoneval/
evaluate.rs

1use super::JSONEval;
2use crate::jsoneval::json_parser;
3use crate::jsoneval::path_utils;
4use crate::jsoneval::table_evaluate;
5use crate::utils::clean_float_noise;
6use crate::time_block;
7
8use serde_json::Value;
9
10#[cfg(feature = "parallel")]
11use rayon::prelude::*;
12
13#[cfg(feature = "parallel")]
14use std::sync::Mutex;
15
16impl JSONEval {
17    /// Evaluate the schema with the given data and context.
18    ///
19    /// # Arguments
20    ///
21    /// * `data` - The data to evaluate.
22    /// * `context` - The context to evaluate.
23    ///
24    /// # Returns
25    ///
26    /// A `Result` indicating success or an error message.
27    pub fn evaluate(
28        &mut self,
29        data: &str,
30        context: Option<&str>,
31        paths: Option<&[String]>,
32    ) -> Result<(), String> {
33        time_block!("evaluate() [total]", {
34            let context_provided = context.is_some();
35
36            // Use SIMD-accelerated JSON parsing
37            let data: Value = time_block!("  parse data", { json_parser::parse_json_str(data)? });
38            let context: Value = time_block!("  parse context", {
39                json_parser::parse_json_str(context.unwrap_or("{}"))?
40            });
41
42            self.data = data.clone();
43
44            // Collect top-level data keys to selectively purge cache
45            let changed_data_paths: Vec<String> = if let Some(obj) = data.as_object() {
46                obj.keys().map(|k| format!("/{}", k)).collect()
47            } else {
48                Vec::new()
49            };
50
51            // Replace data and context in existing eval_data
52            time_block!("  replace_data_and_context", {
53                self.eval_data.replace_data_and_context(data, context);
54            });
55
56            // Selectively purge cache entries that depend on changed top-level data keys
57            // This is more efficient than clearing entire cache
58            time_block!("  purge_cache", {
59                self.purge_cache_for_changed_data(&changed_data_paths);
60
61                // Also purge context-dependent cache if context was provided
62                if context_provided {
63                    self.purge_cache_for_context_change();
64                }
65            });
66
67            // Call internal evaluate (uses existing data if not provided)
68            self.evaluate_internal(paths)
69        })
70    }
71
72    /// Internal evaluate that can be called when data is already set
73    /// This avoids double-locking and unnecessary data cloning for re-evaluation from evaluate_dependents
74    pub(crate) fn evaluate_internal(&mut self, paths: Option<&[String]>) -> Result<(), String> {
75        time_block!("  evaluate_internal() [total]", {
76            // Acquire lock for synchronous execution
77            let _lock = self.eval_lock.lock().unwrap();
78
79            // Normalize paths to schema pointers for correct filtering
80            let normalized_paths_storage; // Keep alive
81            let normalized_paths = if let Some(p_list) = paths {
82                normalized_paths_storage = p_list
83                    .iter()
84                    .flat_map(|p| {
85                        let normalized = if p.starts_with("#/") {
86                            // Case 1: JSON Schema path (e.g. #/properties/foo) - keep as is
87                            p.to_string()
88                        } else if p.starts_with('/') {
89                            // Case 2: Rust Pointer path (e.g. /properties/foo) - ensure # prefix
90                            format!("#{}", p)
91                        } else {
92                            // Case 3: Dot notation (e.g. properties.foo) - replace dots with slashes and add prefix
93                            format!("#/{}", p.replace('.', "/"))
94                        };
95
96                        vec![normalized]
97                    })
98                    .collect::<Vec<_>>();
99                Some(normalized_paths_storage.as_slice())
100            } else {
101                None
102            };
103
104            // Clone sorted_evaluations (Arc clone is cheap, then clone inner Vec)
105            let eval_batches: Vec<Vec<String>> = (*self.sorted_evaluations).clone();
106
107            // Process each batch - parallelize evaluations within each batch
108            // Batches are processed sequentially to maintain dependency order
109            // Process value evaluations (simple computed fields)
110            // These are independent of rule batches and should always run
111            let eval_data_values = self.eval_data.clone();
112            time_block!("      evaluate values", {
113                #[cfg(feature = "parallel")]
114                if self.value_evaluations.len() > 100 {
115                    let value_results: Mutex<Vec<(String, Value)>> =
116                        Mutex::new(Vec::with_capacity(self.value_evaluations.len()));
117
118                    self.value_evaluations.par_iter().for_each(|eval_key| {
119                        // Skip if has dependencies (will be handled in sorted batches)
120                        if let Some(deps) = self.dependencies.get(eval_key) {
121                            if !deps.is_empty() {
122                                return;
123                            }
124                        }
125
126                        // Filter items if paths are provided
127                        if let Some(filter_paths) = normalized_paths {
128                            if !filter_paths.is_empty()
129                                && !filter_paths.iter().any(|p| {
130                                    eval_key.starts_with(p.as_str())
131                                        || p.starts_with(eval_key.as_str())
132                                })
133                            {
134                                return;
135                            }
136                        }
137
138                        // For value evaluations (e.g. /properties/foo/value), we want the value at that path
139                        // The path in eval_key is like "#/properties/foo/value"
140                        let pointer_path = path_utils::normalize_to_json_pointer(eval_key);
141
142                        // Try cache first (thread-safe)
143                        if let Some(_) = self.try_get_cached(eval_key, &eval_data_values) {
144                            return;
145                        }
146
147                        // Cache miss - evaluate
148                        if let Some(logic_id) = self.evaluations.get(eval_key) {
149                            if let Ok(val) = self.engine.run(logic_id, eval_data_values.data()) {
150                                let cleaned_val = clean_float_noise(val);
151                                // Cache result (thread-safe)
152                                self.cache_result(eval_key, Value::Null, &eval_data_values);
153                                value_results
154                                    .lock()
155                                    .unwrap()
156                                    .push((pointer_path, cleaned_val));
157                            }
158                        }
159                    });
160
161                    // Write results to evaluated_schema
162                    for (result_path, value) in value_results.into_inner().unwrap() {
163                        if let Some(pointer_value) = self.evaluated_schema.pointer_mut(&result_path)
164                        {
165                            *pointer_value = value;
166                        }
167                    }
168                }
169
170                // Sequential execution for values (if not parallel or small count)
171                #[cfg(feature = "parallel")]
172                let value_eval_items = if self.value_evaluations.len() > 100 {
173                    &self.value_evaluations[0..0]
174                } else {
175                    &self.value_evaluations
176                };
177
178                #[cfg(not(feature = "parallel"))]
179                let value_eval_items = &self.value_evaluations;
180
181                for eval_key in value_eval_items.iter() {
182                    // Skip if has dependencies (will be handled in sorted batches)
183                    if let Some(deps) = self.dependencies.get(eval_key) {
184                        if !deps.is_empty() {
185                            continue;
186                        }
187                    }
188
189                    // Filter items if paths are provided
190                    if let Some(filter_paths) = normalized_paths {
191                        if !filter_paths.is_empty()
192                            && !filter_paths.iter().any(|p| {
193                                eval_key.starts_with(p.as_str()) || p.starts_with(eval_key.as_str())
194                            })
195                        {
196                            continue;
197                        }
198                    }
199
200                    let pointer_path = path_utils::normalize_to_json_pointer(eval_key);
201
202                    // Try cache first
203                    if let Some(_) = self.try_get_cached(eval_key, &eval_data_values) {
204                        continue;
205                    }
206
207                    // Cache miss - evaluate
208                    if let Some(logic_id) = self.evaluations.get(eval_key) {
209                        if let Ok(val) = self.engine.run(logic_id, eval_data_values.data()) {
210                            let cleaned_val = clean_float_noise(val);
211                            // Cache result
212                            self.cache_result(eval_key, Value::Null, &eval_data_values);
213
214                            if let Some(pointer_value) =
215                                self.evaluated_schema.pointer_mut(&pointer_path)
216                            {
217                                *pointer_value = cleaned_val;
218                            }
219                        }
220                    }
221                }
222            });
223
224            time_block!("    process batches", {
225                for batch in eval_batches {
226                    // Skip empty batches
227                    if batch.is_empty() {
228                        continue;
229                    }
230
231                    // Check if we can skip this entire batch optimization
232                    // If paths are provided, we can check if ANY item in batch matches ANY path
233                    if let Some(filter_paths) = normalized_paths {
234                        if !filter_paths.is_empty() {
235                            let batch_has_match = batch.iter().any(|eval_key| {
236                                filter_paths.iter().any(|p| {
237                                    eval_key.starts_with(p.as_str())
238                                        || p.starts_with(eval_key.as_str())
239                                })
240                            });
241                            if !batch_has_match {
242                                continue;
243                            }
244                        }
245                    }
246
247                    // No pre-checking cache - we'll check inside parallel execution
248                    // This allows thread-safe cache access during parallel evaluation
249
250                    // Parallel execution within batch (no dependencies between items)
251                    // Use Mutex for thread-safe result collection
252                    // Store both eval_key and result for cache storage
253                    let eval_data_snapshot = self.eval_data.clone();
254
255                    // Parallelize only if batch has multiple items (overhead not worth it for single item)
256
257                    #[cfg(feature = "parallel")]
258                    if batch.len() > 1000 {
259                        let results: Mutex<Vec<(String, String, Value)>> =
260                            Mutex::new(Vec::with_capacity(batch.len()));
261                        batch.par_iter().for_each(|eval_key| {
262                            // Filter individual items if paths are provided
263                            if let Some(filter_paths) = normalized_paths {
264                                if !filter_paths.is_empty()
265                                    && !filter_paths.iter().any(|p| {
266                                        eval_key.starts_with(p.as_str())
267                                            || p.starts_with(eval_key.as_str())
268                                    })
269                                {
270                                    return;
271                                }
272                            }
273
274                            let pointer_path = path_utils::normalize_to_json_pointer(eval_key);
275
276                            // Try cache first (thread-safe)
277                            if let Some(_) = self.try_get_cached(eval_key, &eval_data_snapshot) {
278                                return;
279                            }
280
281                            // Cache miss - evaluate
282                            let is_table = self.table_metadata.contains_key(eval_key);
283
284                            if is_table {
285                                // Evaluate table using sandboxed metadata (parallel-safe, immutable parent scope)
286                                if let Ok(rows) = table_evaluate::evaluate_table(
287                                    self,
288                                    eval_key,
289                                    &eval_data_snapshot,
290                                ) {
291                                    let value = Value::Array(rows);
292                                    // Cache result (thread-safe)
293                                    self.cache_result(eval_key, Value::Null, &eval_data_snapshot);
294                                    results.lock().unwrap().push((
295                                        eval_key.clone(),
296                                        pointer_path,
297                                        value,
298                                    ));
299                                }
300                            } else {
301                                if let Some(logic_id) = self.evaluations.get(eval_key) {
302                                    // Evaluate directly with snapshot
303                                    if let Ok(val) =
304                                        self.engine.run(logic_id, eval_data_snapshot.data())
305                                    {
306                                        let cleaned_val = clean_float_noise(val);
307                                        // Cache result (thread-safe)
308                                        self.cache_result(
309                                            eval_key,
310                                            Value::Null,
311                                            &eval_data_snapshot,
312                                        );
313                                        results.lock().unwrap().push((
314                                            eval_key.clone(),
315                                            pointer_path,
316                                            cleaned_val,
317                                        ));
318                                    }
319                                }
320                            }
321                        });
322
323                        // Write all results back sequentially (already cached in parallel execution)
324                        for (_eval_key, path, value) in results.into_inner().unwrap() {
325                            let cleaned_value = clean_float_noise(value);
326
327                            self.eval_data.set(&path, cleaned_value.clone());
328                            // Also write to evaluated_schema
329                            if let Some(schema_value) = self.evaluated_schema.pointer_mut(&path) {
330                                *schema_value = cleaned_value;
331                            }
332                        }
333                        continue;
334                    }
335
336                    // Sequential execution (single item or parallel feature disabled)
337                    #[cfg(not(feature = "parallel"))]
338                    let batch_items = &batch;
339
340                    #[cfg(feature = "parallel")]
341                    let batch_items = if batch.len() > 1000 {
342                        &batch[0..0]
343                    } else {
344                        &batch
345                    }; // Empty slice if already processed in parallel
346
347                    for eval_key in batch_items {
348                        // Filter individual items if paths are provided
349                        if let Some(filter_paths) = normalized_paths {
350                            if !filter_paths.is_empty()
351                                && !filter_paths.iter().any(|p| {
352                                    eval_key.starts_with(p.as_str())
353                                        || p.starts_with(eval_key.as_str())
354                                })
355                            {
356                                continue;
357                            }
358                        }
359
360                        let pointer_path = path_utils::normalize_to_json_pointer(eval_key);
361
362                        // Try cache first
363                        if let Some(_) = self.try_get_cached(eval_key, &eval_data_snapshot) {
364                            continue;
365                        }
366
367                        // Cache miss - evaluate
368                        let is_table = self.table_metadata.contains_key(eval_key);
369
370                        if is_table {
371                            if let Ok(rows) =
372                                table_evaluate::evaluate_table(self, eval_key, &eval_data_snapshot)
373                            {
374                                let value = Value::Array(rows);
375                                // Cache result
376                                self.cache_result(eval_key, Value::Null, &eval_data_snapshot);
377
378                                let cleaned_value = clean_float_noise(value);
379                                self.eval_data.set(&pointer_path, cleaned_value.clone());
380                                if let Some(schema_value) =
381                                    self.evaluated_schema.pointer_mut(&pointer_path)
382                                {
383                                    *schema_value = cleaned_value;
384                                }
385                            }
386                        } else {
387                            if let Some(logic_id) = self.evaluations.get(eval_key) {
388                                if let Ok(val) =
389                                    self.engine.run(logic_id, eval_data_snapshot.data())
390                                {
391                                    let cleaned_val = clean_float_noise(val);
392                                    // Cache result
393                                    self.cache_result(eval_key, Value::Null, &eval_data_snapshot);
394
395                                    self.eval_data.set(&pointer_path, cleaned_val.clone());
396                                    if let Some(schema_value) =
397                                        self.evaluated_schema.pointer_mut(&pointer_path)
398                                    {
399                                        *schema_value = cleaned_val;
400                                    }
401                                }
402                            }
403                        }
404                    }
405                }
406            });
407
408            // Drop lock before calling evaluate_others
409            drop(_lock);
410
411            self.evaluate_others(paths);
412
413            Ok(())
414        })
415    }
416
417    pub(crate) fn evaluate_others(&mut self, paths: Option<&[String]>) {
418        time_block!("    evaluate_others()", {
419            // Step 1: Evaluate "rules" and "others" categories with caching
420            // Rules are evaluated here so their values are available in evaluated_schema
421            let combined_count = self.rules_evaluations.len() + self.others_evaluations.len();
422            if combined_count > 0 {
423                time_block!("      evaluate rules+others", {
424                    let eval_data_snapshot = self.eval_data.clone();
425
426                    let normalized_paths: Option<Vec<String>> = paths.map(|p_list| {
427                        p_list
428                            .iter()
429                            .flat_map(|p| {
430                                let ptr = path_utils::dot_notation_to_schema_pointer(p);
431                                // Also support version with /properties/ prefix for root match
432                                let with_props = if ptr.starts_with("#/") {
433                                    format!("#/properties/{}", &ptr[2..])
434                                } else {
435                                    ptr.clone()
436                                };
437                                vec![ptr, with_props]
438                            })
439                            .collect()
440                    });
441
442                    #[cfg(feature = "parallel")]
443                    {
444                        let combined_results: Mutex<Vec<(String, Value)>> =
445                            Mutex::new(Vec::with_capacity(combined_count));
446
447                        self.rules_evaluations
448                            .par_iter()
449                            .chain(self.others_evaluations.par_iter())
450                            .for_each(|eval_key| {
451                                // Filter items if paths are provided
452                                if let Some(filter_paths) = normalized_paths.as_ref() {
453                                    if !filter_paths.is_empty()
454                                        && !filter_paths.iter().any(|p| {
455                                            eval_key.starts_with(p.as_str())
456                                                || p.starts_with(eval_key.as_str())
457                                        })
458                                    {
459                                        return;
460                                    }
461                                }
462
463                                let pointer_path = path_utils::normalize_to_json_pointer(eval_key);
464
465                                // Try cache first (thread-safe)
466                                if let Some(_) = self.try_get_cached(eval_key, &eval_data_snapshot)
467                                {
468                                    return;
469                                }
470
471                                // Cache miss - evaluate
472                                if let Some(logic_id) = self.evaluations.get(eval_key) {
473                                    if let Ok(val) =
474                                        self.engine.run(logic_id, eval_data_snapshot.data())
475                                    {
476                                        let cleaned_val = clean_float_noise(val);
477                                        // Cache result (thread-safe)
478                                        self.cache_result(
479                                            eval_key,
480                                            Value::Null,
481                                            &eval_data_snapshot,
482                                        );
483                                        combined_results
484                                            .lock()
485                                            .unwrap()
486                                            .push((pointer_path, cleaned_val));
487                                    }
488                                }
489                            });
490
491                        // Write results to evaluated_schema
492                        for (result_path, value) in combined_results.into_inner().unwrap() {
493                            if let Some(pointer_value) =
494                                self.evaluated_schema.pointer_mut(&result_path)
495                            {
496                                // Special handling for rules with $evaluation
497                                // This includes both direct rules and array items: /rules/evaluation/0/$evaluation
498                                if !result_path.starts_with("$")
499                                    && result_path.contains("/rules/")
500                                    && !result_path.ends_with("/value")
501                                {
502                                    match pointer_value.as_object_mut() {
503                                        Some(pointer_obj) => {
504                                            pointer_obj.remove("$evaluation");
505                                            pointer_obj.insert("value".to_string(), value);
506                                        }
507                                        None => continue,
508                                    }
509                                } else {
510                                    *pointer_value = value;
511                                }
512                            }
513                        }
514                    }
515
516                    #[cfg(not(feature = "parallel"))]
517                    {
518                        // Sequential evaluation
519                        let combined_evals: Vec<&String> = self
520                            .rules_evaluations
521                            .iter()
522                            .chain(self.others_evaluations.iter())
523                            .collect();
524
525                        for eval_key in combined_evals {
526                            // Filter items if paths are provided
527                            if let Some(filter_paths) = normalized_paths.as_ref() {
528                                if !filter_paths.is_empty()
529                                    && !filter_paths.iter().any(|p| {
530                                        eval_key.starts_with(p.as_str())
531                                            || p.starts_with(eval_key.as_str())
532                                    })
533                                {
534                                    continue;
535                                }
536                            }
537
538                            let pointer_path = path_utils::normalize_to_json_pointer(eval_key);
539
540                            // Try cache first
541                            if let Some(_) = self.try_get_cached(eval_key, &eval_data_snapshot) {
542                                continue;
543                            }
544
545                            // Cache miss - evaluate
546                            if let Some(logic_id) = self.evaluations.get(eval_key) {
547                                if let Ok(val) =
548                                    self.engine.run(logic_id, eval_data_snapshot.data())
549                                {
550                                    let cleaned_val = clean_float_noise(val);
551                                    // Cache result
552                                    self.cache_result(eval_key, Value::Null, &eval_data_snapshot);
553
554                                    if let Some(pointer_value) =
555                                        self.evaluated_schema.pointer_mut(&pointer_path)
556                                    {
557                                        if !pointer_path.starts_with("$")
558                                            && pointer_path.contains("/rules/")
559                                            && !pointer_path.ends_with("/value")
560                                        {
561                                            match pointer_value.as_object_mut() {
562                                                Some(pointer_obj) => {
563                                                    pointer_obj.remove("$evaluation");
564                                                    pointer_obj
565                                                        .insert("value".to_string(), cleaned_val);
566                                                }
567                                                None => continue,
568                                            }
569                                        } else {
570                                            *pointer_value = cleaned_val;
571                                        }
572                                    }
573                                }
574                            }
575                        }
576                    }
577                });
578            }
579        });
580
581        // Step 2: Evaluate options URL templates (handles {variable} patterns)
582        time_block!("      evaluate_options_templates", {
583            self.evaluate_options_templates(paths);
584        });
585
586        // Step 3: Resolve layout logic (metadata injection, hidden propagation)
587        time_block!("      resolve_layout", {
588            let _ = self.resolve_layout(false);
589        });
590    }
591
592    /// Evaluate options URL templates (handles {variable} patterns)
593    fn evaluate_options_templates(&mut self, paths: Option<&[String]>) {
594        // Use pre-collected options templates from parsing (Arc clone is cheap)
595        let templates_to_eval = self.options_templates.clone();
596
597        // Evaluate each template
598        for (path, template_str, params_path) in templates_to_eval.iter() {
599            // Filter items if paths are provided
600            // 'path' here is the schema path to the field (dot notation or similar, need to check)
601            // It seems to be schema pointer based on usage in other methods
602            if let Some(filter_paths) = paths {
603                if !filter_paths.is_empty()
604                    && !filter_paths
605                        .iter()
606                        .any(|p| path.starts_with(p.as_str()) || p.starts_with(path.as_str()))
607                {
608                    continue;
609                }
610            }
611
612            if let Some(params) = self.evaluated_schema.pointer(&params_path) {
613                if let Ok(evaluated) = self.evaluate_template(&template_str, params) {
614                    if let Some(target) = self.evaluated_schema.pointer_mut(&path) {
615                        *target = Value::String(evaluated);
616                    }
617                }
618            }
619        }
620    }
621
622    /// Evaluate a template string like "api/users/{id}" with params
623    fn evaluate_template(&self, template: &str, params: &Value) -> Result<String, String> {
624        let mut result = template.to_string();
625
626        // Simple template evaluation: replace {key} with params.key
627        if let Value::Object(params_map) = params {
628            for (key, value) in params_map {
629                let placeholder = format!("{{{}}}", key);
630                if let Some(str_val) = value.as_str() {
631                    result = result.replace(&placeholder, str_val);
632                } else {
633                    // Convert non-string values to strings
634                    result = result.replace(&placeholder, &value.to_string());
635                }
636            }
637        }
638
639        Ok(result)
640    }
641}