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(¶ms_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}