Skip to main content

json_eval_rs/jsoneval/
evaluate.rs

1use std::sync::Arc;
2
3use super::JSONEval;
4use crate::jsoneval::cancellation::CancellationToken;
5use crate::jsoneval::json_parser;
6use crate::jsoneval::path_utils;
7use crate::jsoneval::table_evaluate;
8use crate::time_block;
9use crate::utils::clean_float_noise_scalar;
10
11use serde_json::Value;
12
13impl JSONEval {
14    /// Evaluate the schema with the given data and context.
15    ///
16    /// # Arguments
17    ///
18    /// * `data` - The data to evaluate.
19    /// * `context` - The context to evaluate.
20    ///
21    /// # Returns
22    ///
23    /// A `Result` indicating success or an error message.
24    pub fn evaluate(
25        &mut self,
26        data: &str,
27        context: Option<&str>,
28        paths: Option<&[String]>,
29        token: Option<&CancellationToken>,
30    ) -> Result<(), String> {
31        if let Some(t) = token {
32            if t.is_cancelled() {
33                return Err("Cancelled".to_string());
34            }
35        }
36        time_block!("evaluate() [total]", {
37            // Use SIMD-accelerated JSON parsing
38            // Parse and update data/context
39            let data_value = time_block!("  parse data", { json_parser::parse_json_str(data)? });
40            let context_value = time_block!("  parse context", {
41                if let Some(ctx) = context {
42                    json_parser::parse_json_str(ctx)?
43                } else {
44                    Value::Object(serde_json::Map::new())
45                }
46            });
47            self.evaluate_internal_with_new_data(data_value, context_value, paths, token)
48        })
49    }
50
51    /// Internal helper to evaluate with all data/context provided as Values.
52    /// `pub(crate)` so the cache-swap path in `evaluate_subform` can call it directly
53    /// after swapping the parent cache in, bypassing the string-parsing overhead.
54    pub(crate) fn evaluate_internal_with_new_data(
55        &mut self,
56        data: Value,
57        context: Value,
58        paths: Option<&[String]>,
59        token: Option<&CancellationToken>,
60    ) -> Result<(), String> {
61        time_block!("  evaluate_internal_with_new_data", {
62            // Reuse the previously stored snapshot as `old_data` to avoid an O(n) deep clone
63            // on every main-form evaluation call.
64            let has_previous_eval = self.eval_cache.main_form_snapshot.is_some();
65            let old_data = self
66                .eval_cache
67                .main_form_snapshot
68                .take()
69                .unwrap_or_else(|| self.eval_data.snapshot_data_clone());
70
71            let old_context = self
72                .eval_data
73                .data()
74                .get("$context")
75                .cloned()
76                .unwrap_or(Value::Null);
77
78            // Store data, context and replace in eval_data (clone once instead of twice)
79            self.data = data.clone();
80            self.context = context.clone();
81            time_block!("  replace_data_and_context", {
82                self.eval_data.replace_data_and_context(data, context);
83            });
84
85            let new_data = self.eval_data.snapshot_data_clone();
86            let new_context = self
87                .eval_data
88                .data()
89                .get("$context")
90                .cloned()
91                .unwrap_or(Value::Null);
92
93            if has_previous_eval
94                && old_data == new_data
95                && old_context == new_context
96                && paths.is_none()
97            {
98                // Perfect cache hit for unmodified payload: fully skip tree traversal.
99                // Restore snapshot since nothing changed.
100                self.eval_cache.main_form_snapshot = Some(new_data);
101                return Ok(());
102            }
103
104            self.eval_cache
105                .store_snapshot_and_diff_versions(&old_data, &new_data);
106            // Save snapshot for the next evaluation cycle (avoids one snapshot_data_clone() call).
107            self.eval_cache.main_form_snapshot = Some(new_data);
108
109            // Generation-based fast skip: diff_and_update_versions bumps data_versions.versions
110            // but does NOT increment eval_generation. Only bump_data_version / bump_params_version
111            // (called from formula stores) advance eval_generation.
112            // If eval_generation == last_evaluated_generation after the diff, no formula's cached
113            // deps are actually stale — all batches would be cache hits. Skip the full traversal.
114            // Safe only in the external evaluate() path; run_re_evaluate_pass must always evaluate.
115            if paths.is_none() && !self.eval_cache.needs_full_evaluation() {
116                self.evaluate_others(paths, token, false);
117                return Ok(());
118            }
119
120            // Call internal evaluate (uses existing data if not provided)
121            self.evaluate_internal(paths, token)
122        })
123    }
124
125    /// Fast variant of `evaluate_internal_with_new_data` for the cache-swap path.
126    ///
127    /// The caller (e.g. `run_subform_pass` / `evaluate_subform_item`) has **already**:
128    /// 1. Called `replace_data_and_context` on `subform.eval_data` with the merged payload.
129    /// 2. Computed the item-level diff and bumped `subform_caches[idx].data_versions` accordingly.
130    /// 3. Swapped the parent cache into `subform.eval_cache` so Tier 2 entries are visible.
131    /// 4. Set `active_item_index = Some(idx)` on the swapped-in cache.
132    ///
133    /// Skipping the expensive `snapshot_data_clone()` × 2 and `diff_and_update_versions`
134    /// saves ~40–80ms per rider on a 5 MB parent payload.
135    pub(crate) fn evaluate_internal_pre_diffed(
136        &mut self,
137        paths: Option<&[String]>,
138        token: Option<&CancellationToken>,
139    ) -> Result<(), String> {
140        debug_assert!(
141            self.eval_cache.active_item_index.is_some(),
142            "evaluate_internal_pre_diffed called without active_item_index — \
143             caller must set up the cache-swap before calling this method"
144        );
145
146        // Same generation-based fast skip as evaluate_internal_with_new_data:
147        // The diff_and_update_versions calls in with_item_cache_swap bump data_versions.versions
148        // but do NOT increment eval_generation. If nothing was re-stored since last evaluate, skip.
149        if paths.is_none() && !self.eval_cache.needs_full_evaluation() {
150            self.evaluate_others(paths, token, false);
151            return Ok(());
152        }
153
154        self.evaluate_internal(paths, token)
155    }
156
157    /// Internal evaluate that can be called when data is already set
158    /// This avoids double-locking and unnecessary data cloning for re-evaluation from evaluate_dependents
159    pub(crate) fn evaluate_internal(
160        &mut self,
161        paths: Option<&[String]>,
162        token: Option<&CancellationToken>,
163    ) -> Result<(), String> {
164        if let Some(t) = token {
165            if t.is_cancelled() {
166                return Err("Cancelled".to_string());
167            }
168        }
169        time_block!("  evaluate_internal() [total]", {
170            // Acquire lock for synchronous execution
171            let _lock = self.eval_lock.lock().unwrap();
172
173            // Normalize paths to schema pointers for correct filtering
174            let normalized_paths_storage; // Keep alive
175            let normalized_paths = if let Some(p_list) = paths {
176                normalized_paths_storage = p_list
177                    .iter()
178                    .flat_map(|p| {
179                        let normalized = if p.starts_with("#/") {
180                            p.to_string()
181                        } else if p.starts_with('/') {
182                            format!("#{}", p)
183                        } else {
184                            format!("#/{}", p.replace('.', "/"))
185                        };
186                        vec![normalized]
187                    })
188                    .collect::<Vec<_>>();
189                Some(normalized_paths_storage.as_slice())
190            } else {
191                None
192            };
193
194            // Borrow sorted_evaluations via Arc (avoid deep-cloning Vec<Vec<String>>)
195            let eval_batches = self.sorted_evaluations.clone();
196
197            // Track whether any entry was a cache miss (required an actual formula run).
198            // When false (all hits), evaluate_others can skip resolve_layout because no
199            // values changed and the layout state is guaranteed identical.
200            // On the very first evaluation (last_evaluated_generation == u64::MAX), we MUST
201            // force a cache miss so that static schemas (with no formulas) still process
202            // URL templates and layout resolution once.
203            let mut had_cache_miss = self.eval_cache.last_evaluated_generation == u64::MAX;
204
205            // Process each batch - sequentially
206            // Batches are processed sequentially to maintain dependency order
207            // Process value evaluations (simple computed fields with no dependencies)
208            let eval_data_values = self.eval_data.clone();
209            time_block!("      evaluate values", {
210                for eval_key in self.value_evaluations.iter() {
211                    if let Some(t) = token {
212                        if t.is_cancelled() {
213                            return Err("Cancelled".to_string());
214                        }
215                    }
216                    // Skip if has dependencies (handled in sorted batches with correct ordering)
217                    if let Some(deps) = self.dependencies.get(eval_key) {
218                        if !deps.is_empty() {
219                            continue;
220                        }
221                    }
222
223                    // Filter items if paths are provided
224                    if let Some(filter_paths) = normalized_paths {
225                        if !filter_paths.is_empty()
226                            && !filter_paths.iter().any(|p| {
227                                eval_key.starts_with(p.as_str()) || p.starts_with(eval_key.as_str())
228                            })
229                        {
230                            continue;
231                        }
232                    }
233
234                    let pointer_path = path_utils::normalize_to_json_pointer(eval_key).into_owned();
235                    let empty_deps = indexmap::IndexSet::new();
236                    let deps = self.dependencies.get(eval_key).unwrap_or(&empty_deps);
237
238                    // Cache hit check
239                    if let Some(_cached_result) = self.eval_cache.check_cache(eval_key, deps) {
240                        continue;
241                    }
242
243                    had_cache_miss = true;
244                    // Cache miss - evaluate
245                    if let Some(logic_id) = self.evaluations.get(eval_key) {
246                        if let Ok(val) = self.engine.run(logic_id, eval_data_values.data()) {
247                            let cleaned_val = clean_float_noise_scalar(val);
248                            self.eval_cache
249                                .store_cache(eval_key, deps, cleaned_val.clone());
250
251                            if let Some(pointer_value) =
252                                self.evaluated_schema.pointer_mut(&pointer_path)
253                            {
254                                *pointer_value = cleaned_val;
255                            }
256                        }
257                    }
258                }
259            });
260
261            time_block!("    process batches", {
262                for batch in eval_batches.iter() {
263                    if let Some(t) = token {
264                        if t.is_cancelled() {
265                            return Err("Cancelled".to_string());
266                        }
267                    }
268                    // Skip empty batches
269                    if batch.is_empty() {
270                        continue;
271                    }
272
273                    // Check if we can skip this entire batch optimization
274                    if let Some(filter_paths) = normalized_paths {
275                        if !filter_paths.is_empty() {
276                            let batch_has_match = batch.iter().any(|eval_key| {
277                                filter_paths.iter().any(|p| {
278                                    eval_key.starts_with(p.as_str())
279                                        || (p.starts_with(eval_key.as_str())
280                                            && !eval_key.contains("/$params/"))
281                                })
282                            });
283                            if !batch_has_match {
284                                continue;
285                            }
286                        }
287                    }
288
289                    // Fast path: try to resolve every eval_key in this batch from cache.
290                    // If all hit, skip the expensive exclusive_clone() of the full eval_data tree.
291                    // This is critical for subforms where eval_data contains the full parent payload.
292                    {
293                        let mut batch_hits: Vec<(String, Value)> = Vec::with_capacity(batch.len());
294                        let all_hit = batch.iter().all(|eval_key| {
295                            let empty_deps = indexmap::IndexSet::new();
296                            let deps = self.dependencies.get(eval_key).unwrap_or(&empty_deps);
297                            if let Some(cached) = self.eval_cache.check_cache(eval_key, deps) {
298                                let pointer_path =
299                                    path_utils::normalize_to_json_pointer(eval_key).into_owned();
300                                batch_hits.push((pointer_path, cached));
301                                true
302                            } else {
303                                false
304                            }
305                        });
306
307                        if all_hit {
308                            // Populate eval_data so downstream batches see these values
309                            for (ptr, val) in batch_hits {
310                                self.eval_data.set(&ptr, val);
311                            }
312                            continue;
313                        }
314                        had_cache_miss = true;
315                        // Partial or full miss — fall through to the normal exclusive_clone path below.
316                        // batch_hits is dropped here; cache lookups will repeat but that's cheap.
317                    }
318
319                    // Sequential execution
320                    // Use exclusive_clone() so self.eval_data.set() within this batch
321                    // is always zero-cost (Arc rc stays 1 on self.eval_data).
322                    let eval_data_snapshot = self.eval_data.exclusive_clone();
323
324                    for eval_key in batch {
325                        if let Some(t) = token {
326                            if t.is_cancelled() {
327                                return Err("Cancelled".to_string());
328                            }
329                        }
330                        // Filter individual items if paths are provided
331                        if let Some(filter_paths) = normalized_paths {
332                            if !filter_paths.is_empty()
333                                && !filter_paths.iter().any(|p| {
334                                    eval_key.starts_with(p.as_str())
335                                        || (p.starts_with(eval_key.as_str())
336                                            && !eval_key.contains("/$params/"))
337                                })
338                            {
339                                continue;
340                            }
341                        }
342
343                        let pointer_path =
344                            path_utils::normalize_to_json_pointer(eval_key).into_owned();
345
346                        // Cache miss - evaluate
347                        let is_table = self.table_metadata.contains_key(eval_key);
348
349                        if is_table {
350                            let mut external_deps = indexmap::IndexSet::new();
351                            let pointer_data_prefix = pointer_path.replace("/properties/", "/");
352                            let pointer_data_prefix_slash = format!("{}/", pointer_data_prefix);
353                            if let Some(deps) = self.dependencies.get(eval_key) {
354                                for dep in deps {
355                                    let dep_data_path =
356                                        crate::jsoneval::path_utils::normalize_to_json_pointer(dep)
357                                            .replace("/properties/", "/");
358                                    if dep_data_path != pointer_data_prefix
359                                        && !dep_data_path.starts_with(&pointer_data_prefix_slash)
360                                    {
361                                        external_deps.insert(dep.clone());
362                                    }
363                                }
364                            }
365
366                            #[cfg(debug_assertions)]
367                            if external_deps.is_empty() {
368                                if let Some(meta) = self.table_metadata.get(eval_key) {
369                                    if !meta.data_plans.is_empty() {
370                                        eprintln!(
371                                            "[jsoneval DEBUG] table {} has zero external_deps but \
372                                             non-empty data_plans — $params changes may not \
373                                             invalidate its cache",
374                                            eval_key
375                                        );
376                                    }
377                                }
378                            }
379
380                            if let Some(cached_result) =
381                                self.eval_cache.check_cache(eval_key, &external_deps)
382                            {
383                                let static_key = format!("/$table{}", pointer_path);
384                                let arc_value = std::sync::Arc::new(cached_result.clone());
385
386                                Arc::make_mut(&mut self.static_arrays)
387                                    .insert(static_key.clone(), std::sync::Arc::clone(&arc_value));
388
389                                self.eval_data.set(&pointer_path, Value::clone(&arc_value));
390
391                                let marker = serde_json::json!({ "$static_array": static_key });
392                                if let Some(schema_value) =
393                                    self.evaluated_schema.pointer_mut(&pointer_path)
394                                {
395                                    *schema_value = marker;
396                                }
397                                continue;
398                            }
399
400                            if let Ok(rows) = table_evaluate::evaluate_table(
401                                self,
402                                eval_key,
403                                &eval_data_snapshot,
404                                token,
405                            ) {
406                                let result_val = Value::Array(rows);
407                                self.eval_cache.store_cache(
408                                    eval_key,
409                                    &external_deps,
410                                    result_val.clone(),
411                                );
412
413                                // NOTE: bump_params_version / bump_data_version for table results
414                                // is now handled inside store_cache (conditional on value change).
415                                // The separate bump here was double-counting: store_cache uses T2
416                                // comparison while this block used eval_data as reference point,
417                                // causing two version increments per changed table.
418
419                                let static_key = format!("/$table{}", pointer_path);
420                                let arc_value = std::sync::Arc::new(result_val);
421
422                                Arc::make_mut(&mut self.static_arrays)
423                                    .insert(static_key.clone(), std::sync::Arc::clone(&arc_value));
424
425                                self.eval_data.set(&pointer_path, Value::clone(&arc_value));
426
427                                let marker = serde_json::json!({ "$static_array": static_key });
428                                if let Some(schema_value) =
429                                    self.evaluated_schema.pointer_mut(&pointer_path)
430                                {
431                                    *schema_value = marker;
432                                }
433                            }
434                        } else {
435                            let empty_deps = indexmap::IndexSet::new();
436                            let deps = self.dependencies.get(eval_key).unwrap_or(&empty_deps);
437                            if let Some(cached_result) =
438                                self.eval_cache.check_cache(eval_key, &deps)
439                            {
440                                // Must still populate eval_data out of cache so subsequent formulas
441                                // referencing this path in the same iteration can read the exact value
442                                self.eval_data.set(&pointer_path, cached_result.clone());
443                                if let Some(schema_value) =
444                                    self.evaluated_schema.pointer_mut(&pointer_path)
445                                {
446                                    *schema_value = cached_result;
447                                }
448                                continue;
449                            }
450
451                            if let Some(logic_id) = self.evaluations.get(eval_key) {
452                                if let Ok(val) =
453                                    self.engine.run(logic_id, eval_data_snapshot.data())
454                                {
455                                    let cleaned_val = clean_float_noise_scalar(val);
456                                    let data_path = pointer_path.replace("/properties/", "/");
457                                    self.eval_cache.store_cache(
458                                        eval_key,
459                                        &deps,
460                                        cleaned_val.clone(),
461                                    );
462
463                                    // Bump data_versions when non-$params field value changes.
464                                    // $params bumps are handled inside store_cache (conditional).
465                                    let old_val = self
466                                        .eval_data
467                                        .get(&data_path)
468                                        .cloned()
469                                        .unwrap_or(Value::Null);
470                                    if cleaned_val != old_val && !data_path.starts_with("/$params")
471                                    {
472                                        self.eval_cache.bump_data_version(&data_path);
473                                    }
474
475                                    self.eval_data.set(&pointer_path, cleaned_val.clone());
476                                    if let Some(schema_value) =
477                                        self.evaluated_schema.pointer_mut(&pointer_path)
478                                    {
479                                        *schema_value = cleaned_val;
480                                    }
481                                }
482                            }
483                        }
484                    }
485                }
486            });
487
488            // Drop lock before calling evaluate_others
489            drop(_lock);
490
491            // Mark generation stable so the next evaluate_internal call can detect whether
492            // any formula was actually re-stored (via bump_data/params_version) since this run.
493            self.eval_cache.mark_evaluated();
494
495            self.evaluate_others(paths, token, had_cache_miss);
496
497            Ok(())
498        })
499    }
500
501    pub(crate) fn evaluate_others(
502        &mut self,
503        paths: Option<&[String]>,
504        token: Option<&CancellationToken>,
505        had_cache_miss: bool,
506    ) {
507        if let Some(t) = token {
508            if t.is_cancelled() {
509                return;
510            }
511        }
512        time_block!("    evaluate_others()", {
513            // Step 1: Evaluate "rules" and "others" categories with caching
514            // Rules are evaluated here so their values are available in evaluated_schema
515            let combined_count = self.rules_evaluations.len() + self.others_evaluations.len();
516            if combined_count > 0 {
517                time_block!("      evaluate rules+others", {
518                    let eval_data_snapshot = self.eval_data.clone();
519
520                    let normalized_paths: Option<Vec<String>> = paths.map(|p_list| {
521                        p_list
522                            .iter()
523                            .flat_map(|p| {
524                                let ptr = path_utils::dot_notation_to_schema_pointer(p);
525                                // Also support version with /properties/ prefix for root match
526                                let with_props = if ptr.starts_with("#/") {
527                                    format!("#/properties/{}", &ptr[2..])
528                                } else {
529                                    ptr.clone()
530                                };
531                                vec![ptr, with_props]
532                            })
533                            .collect()
534                    });
535
536                    // Sequential evaluation
537                    let combined_evals: Vec<&String> = self
538                        .rules_evaluations
539                        .iter()
540                        .chain(self.others_evaluations.iter())
541                        .collect();
542
543                    for eval_key in combined_evals {
544                        if let Some(t) = token {
545                            if t.is_cancelled() {
546                                return;
547                            }
548                        }
549                        // Filter items if paths are provided
550                        if let Some(filter_paths) = normalized_paths.as_ref() {
551                            if !filter_paths.is_empty()
552                                && !filter_paths.iter().any(|p| {
553                                    eval_key.starts_with(p.as_str())
554                                        || (p.starts_with(eval_key.as_str())
555                                            && !eval_key.contains("/$params/"))
556                                })
557                            {
558                                continue;
559                            }
560                        }
561
562                        let pointer_path =
563                            path_utils::normalize_to_json_pointer(eval_key).into_owned();
564                        let empty_deps = indexmap::IndexSet::new();
565                        let deps = self.dependencies.get(eval_key).unwrap_or(&empty_deps);
566
567                        if let Some(cached_result) = self.eval_cache.check_cache(eval_key, &deps) {
568                            if let Some(pointer_value) =
569                                self.evaluated_schema.pointer_mut(&pointer_path)
570                            {
571                                if !pointer_path.starts_with("$")
572                                    && pointer_path.contains("/rules/")
573                                    && !pointer_path.ends_with("/value")
574                                {
575                                    if let Some(pointer_obj) = pointer_value.as_object_mut() {
576                                        pointer_obj.remove("$evaluation");
577                                        pointer_obj
578                                            .insert("value".to_string(), cached_result.clone());
579                                    }
580                                } else {
581                                    *pointer_value = cached_result.clone();
582                                }
583                            }
584                            continue;
585                        }
586                        if let Some(logic_id) = self.evaluations.get(eval_key) {
587                            if let Ok(val) = self.engine.run(logic_id, eval_data_snapshot.data()) {
588                                let cleaned_val = clean_float_noise_scalar(val);
589                                self.eval_cache
590                                    .store_cache(eval_key, &deps, cleaned_val.clone());
591
592                                if let Some(pointer_value) =
593                                    self.evaluated_schema.pointer_mut(&pointer_path)
594                                {
595                                    if !pointer_path.starts_with("$")
596                                        && pointer_path.contains("/rules/")
597                                        && !pointer_path.ends_with("/value")
598                                    {
599                                        match pointer_value.as_object_mut() {
600                                            Some(pointer_obj) => {
601                                                pointer_obj.remove("$evaluation");
602                                                pointer_obj
603                                                    .insert("value".to_string(), cleaned_val);
604                                            }
605                                            None => continue,
606                                        }
607                                    } else {
608                                        *pointer_value = cleaned_val;
609                                    }
610                                }
611                            }
612                        }
613                    }
614                });
615            }
616        });
617
618        // Step 2: Evaluate options URL templates (handles {variable} patterns)
619        // Skip when all entries were cache hits — template inputs can't have changed.
620        if had_cache_miss {
621            time_block!("      evaluate_options_templates", {
622                self.evaluate_options_templates(paths);
623            });
624
625            // Step 3: Resolve layout logic (metadata injection, hidden propagation)
626            // Skip when no values changed — layout state is guaranteed identical.
627            time_block!("      resolve_layout", {
628                let _ = self.resolve_layout(false);
629            });
630        }
631    }
632
633    /// Evaluate options URL templates (handles {variable} patterns)
634    fn evaluate_options_templates(&mut self, paths: Option<&[String]>) {
635        // Use pre-collected options templates from parsing (Arc clone is cheap)
636        let templates_to_eval = self.options_templates.clone();
637
638        // Evaluate each template
639        for (path, template_str, params_path) in templates_to_eval.iter() {
640            // Filter items if paths are provided
641            // 'path' here is the schema path to the field (dot notation or similar, need to check)
642            // It seems to be schema pointer based on usage in other methods
643            if let Some(filter_paths) = paths {
644                if !filter_paths.is_empty()
645                    && !filter_paths
646                        .iter()
647                        .any(|p| path.starts_with(p.as_str()) || p.starts_with(path.as_str()))
648                {
649                    continue;
650                }
651            }
652
653            if let Some(params) = self.evaluated_schema.pointer(&params_path) {
654                if let Ok(evaluated) = self.evaluate_template(&template_str, params) {
655                    if let Some(target) = self.evaluated_schema.pointer_mut(&path) {
656                        *target = Value::String(evaluated);
657                    }
658                }
659            }
660        }
661    }
662
663    /// Evaluate a template string like "api/users/{id}" with params
664    fn evaluate_template(&self, template: &str, params: &Value) -> Result<String, String> {
665        let mut result = template.to_string();
666
667        // Simple template evaluation: replace {key} with params.key
668        if let Value::Object(params_map) = params {
669            for (key, value) in params_map {
670                let placeholder = format!("{{{}}}", key);
671                if let Some(str_val) = value.as_str() {
672                    result = result.replace(&placeholder, str_val);
673                } else {
674                    // Convert non-string values to strings
675                    result = result.replace(&placeholder, &value.to_string());
676                }
677            }
678        }
679
680        Ok(result)
681    }
682}