json_eval_rs/jsoneval/evaluate.rs
1use std::sync::Arc;
2
3use super::JSONEval;
4use crate::jsoneval::cancellation::CancellationToken;
5use crate::jsoneval::eval_data::EvalData;
6use crate::jsoneval::json_parser;
7use crate::jsoneval::path_utils;
8use crate::jsoneval::table_evaluate;
9use crate::time_block;
10use crate::utils::clean_float_noise_scalar;
11
12use serde_json::Value;
13
14/// Returns `true` if `new_item` (raw user input) is identity-compatible with `old_item`
15/// (snapshot that may contain computed formula outputs alongside raw input fields).
16///
17/// A full `==` comparison fails when `old_item` has extra keys written by formula evaluation
18/// (e.g., computed fields, calculated formulas) that are absent from the raw `new_item`. This helper
19/// compares only the fields present in `new_item`, ignoring extra keys in `old_item`:
20///
21/// - If both are objects: every key in `new` must match the same key in `old`.
22/// - Otherwise: standard equality (covers Null, scalar, array cases).
23///
24/// Used by `invalidate_subform_caches_on_structural_change` to detect genuine order/identity
25/// shifts without false positives from computed formula output fields in the snapshot.
26fn items_same_input_identity(old: Option<&Value>, new: Option<&Value>) -> bool {
27 match (old, new) {
28 (Some(Value::Object(old_map)), Some(Value::Object(new_map))) => new_map
29 .iter()
30 .all(|(k, new_val)| old_map.get(k).map_or(false, |old_val| old_val == new_val)),
31 (old, new) => old == new,
32 }
33}
34
35impl JSONEval {
36 /// Evaluate the schema with the given data and context.
37 ///
38 /// # Arguments
39 ///
40 /// * `data` - The data to evaluate.
41 /// * `context` - The context to evaluate.
42 ///
43 /// # Returns
44 ///
45 /// A `Result` indicating success or an error message.
46 pub fn evaluate(
47 &mut self,
48 data: &str,
49 context: Option<&str>,
50 paths: Option<&[String]>,
51 token: Option<&CancellationToken>,
52 ) -> Result<(), String> {
53 if let Some(t) = token {
54 if t.is_cancelled() {
55 return Err("Cancelled".to_string());
56 }
57 }
58 time_block!("evaluate() [total]", {
59 // Use SIMD-accelerated JSON parsing
60 // Parse and update data/context
61 let data_value = time_block!(" parse data", { json_parser::parse_json_str(data)? });
62 let context_value = time_block!(" parse context", {
63 if let Some(ctx) = context {
64 json_parser::parse_json_str(ctx)?
65 } else {
66 Value::Object(serde_json::Map::new())
67 }
68 });
69 self.evaluate_internal_with_new_data(data_value, context_value, paths, token)
70 })
71 }
72
73 /// Internal helper to evaluate with all data/context provided as Values.
74 /// `pub(crate)` so the cache-swap path in `evaluate_subform` can call it directly
75 /// after swapping the parent cache in, bypassing the string-parsing overhead.
76 pub(crate) fn evaluate_internal_with_new_data(
77 &mut self,
78 data: Value,
79 context: Value,
80 paths: Option<&[String]>,
81 token: Option<&CancellationToken>,
82 ) -> Result<(), String> {
83 time_block!(" evaluate_internal_with_new_data", {
84 // Reuse the previously stored snapshot as `old_data` to avoid an O(n) deep clone
85 // on every main-form evaluation call.
86 let has_previous_eval = self.eval_cache.main_form_snapshot.is_some();
87 let old_data = self
88 .eval_cache
89 .main_form_snapshot
90 .take()
91 .unwrap_or_else(|| self.eval_data.snapshot_data());
92
93 let old_context = self
94 .eval_data
95 .data()
96 .get("$context")
97 .cloned()
98 .unwrap_or(Value::Null);
99
100 // Store data, context and replace in eval_data (clone once instead of twice)
101 self.data = data.clone();
102 self.context = context.clone();
103 time_block!(" replace_data_and_context", {
104 self.eval_data.replace_data_and_context(data, context);
105 });
106
107 let new_data = self.eval_data.snapshot_data();
108 let new_context = self
109 .eval_data
110 .data()
111 .get("$context")
112 .cloned()
113 .unwrap_or(Value::Null);
114
115 if has_previous_eval
116 && old_data == new_data
117 && old_context == new_context
118 && paths.is_none()
119 {
120 // Perfect cache hit for unmodified payload: fully skip tree traversal.
121 // Restore snapshot since nothing changed.
122 self.eval_cache.main_form_snapshot = Some(new_data);
123 return Ok(());
124 }
125
126 // Seed subform caches from loaded data.
127 for (subform_path, subform) in &mut self.subforms {
128 let subform_ptr =
129 crate::jsoneval::path_utils::schema_path_to_data_pointer(subform_path);
130 if let Some(items) = new_data.pointer(&subform_ptr).and_then(|v| v.as_array()) {
131 for (idx, item_val) in items.iter().enumerate() {
132 self.eval_cache.ensure_active_item_cache(idx);
133 if let Some(c) = self.eval_cache.subform_caches.get_mut(&idx) {
134 c.item_snapshot = item_val.clone();
135 }
136 subform.eval_cache.ensure_active_item_cache(idx);
137 if let Some(c) = subform.eval_cache.subform_caches.get_mut(&idx) {
138 c.item_snapshot = item_val.clone();
139 }
140 }
141 }
142 }
143
144 self.eval_cache
145 .store_snapshot_and_diff_versions(&old_data, &new_data);
146 // Save snapshot for the next evaluation cycle (O(1) Arc clone)
147 self.eval_cache.main_form_snapshot = Some(std::sync::Arc::clone(&new_data));
148
149 // Invalidate subform caches after structural changes.
150 self.invalidate_subform_caches_on_structural_change(&old_data, &new_data);
151
152 // Skip external traversal when cached dependencies are fresh.
153 if paths.is_none() && !self.eval_cache.needs_full_evaluation() {
154 self.evaluate_others(paths, token);
155 return Ok(());
156 }
157
158 // Apply visible defaults before final formula pass.
159 self.evaluate_internal(paths, token)?;
160 if self.apply_visible_static_defaults() {
161 self.evaluate_internal(paths, token)?;
162 }
163 Ok(())
164 })
165 }
166
167 /// Detect structural changes in subform arrays between `old_data` and `new_data`
168 /// and evict stale caches accordingly.
169 pub(crate) fn invalidate_subform_caches_on_structural_change(
170 &mut self,
171 old_data: &Value,
172 new_data: &Value,
173 ) {
174 for (subform_path, _) in &self.subforms {
175 // Resolve the data pointer for this subform
176 // (e.g., `/items/sub_items`)
177 let subform_ptr =
178 crate::jsoneval::path_utils::schema_path_to_data_pointer(subform_path).to_string();
179
180 let old_items = old_data.pointer(&subform_ptr).and_then(Value::as_array);
181 let new_items = new_data.pointer(&subform_ptr).and_then(Value::as_array);
182
183 let old_len = old_items.map(Vec::len).unwrap_or(0);
184 let new_len = new_items.map(Vec::len).unwrap_or(0);
185 let min_len = old_len.min(new_len);
186
187 // Detect reordered overlapping items.
188 let identities_shifted = (0..min_len).any(|i| {
189 let old_item = old_items.and_then(|a| a.get(i));
190 let new_item = new_items.and_then(|a| a.get(i));
191 !items_same_input_identity(old_item, new_item)
192 });
193
194 if old_len == new_len && !identities_shifted {
195 continue; // No structural change for this subform
196 }
197
198 // Build local subform path prefix.
199 let field_key = subform_ptr
200 .split('/')
201 .next_back()
202 .unwrap_or(subform_ptr.as_str());
203 let subform_dep_prefix = format!("/{}/", field_key);
204
205 // Evict affected T2 entries.
206 let mut evicted_paths: Vec<String> = Vec::new();
207 self.eval_cache.entries.retain(|eval_key, entry| {
208 let has_subform_dep = entry
209 .dep_versions
210 .keys()
211 .any(|dep| dep.starts_with(&subform_dep_prefix));
212
213 if has_subform_dep {
214 let normalized =
215 crate::jsoneval::path_utils::schema_path_to_data_pointer(eval_key);
216 evicted_paths.push(normalized.into_owned());
217 false // remove entry
218 } else {
219 true // keep
220 }
221 });
222
223 // Bump params_versions for every evicted T2 entry so downstream $params formulas
224 // (e.g., aggregate formulas over items) correctly miss their caches.
225 for path in &evicted_paths {
226 self.eval_cache
227 .params_versions
228 .bump(path, "invalidate_subform_caches_on_structural_change");
229 }
230
231 // Clear T1 per-item caches for indices where item identity has shifted.
232 // This prevents stale per-item results being reused for a different item
233 // occupying the same array slot after a reorder.
234 for idx in 0..min_len {
235 let old_item = old_items.and_then(|a| a.get(idx));
236 let new_item = new_items.and_then(|a| a.get(idx));
237 if !items_same_input_identity(old_item, new_item) {
238 if let Some(c) = self.eval_cache.subform_caches.get_mut(&idx) {
239 c.entries.clear();
240 c.data_versions = crate::jsoneval::eval_cache::VersionTracker::new();
241 }
242 }
243 }
244 // Prune T1 caches for indices that no longer exist (removed items)
245 self.eval_cache.prune_subform_caches(new_len);
246
247 if !evicted_paths.is_empty() || old_len != new_len {
248 self.eval_cache.eval_generation += 1;
249 }
250 }
251 }
252
253 /// Fast variant of `evaluate_internal_with_new_data` for the cache-swap path.
254 ///
255 /// The caller (e.g. `run_subform_pass` / `evaluate_subform_item`) has **already**:
256 /// 1. Called `replace_data_and_context` on `subform.eval_data` with the merged payload.
257 /// 2. Computed the item-level diff and bumped `subform_caches[idx].data_versions` accordingly.
258 /// 3. Swapped the parent cache into `subform.eval_cache` so Tier 2 entries are visible.
259 /// 4. Set `active_item_index = Some(idx)` on the swapped-in cache.
260 ///
261 /// Skipping the expensive `snapshot_data_clone()` × 2 and `diff_and_update_versions`
262 /// saves significant overhead per subform item on large payloads.
263 pub(crate) fn evaluate_internal_pre_diffed(
264 &mut self,
265 paths: Option<&[String]>,
266 token: Option<&CancellationToken>,
267 ) -> Result<(), String> {
268 debug_assert!(
269 self.eval_cache.active_item_index.is_some(),
270 "evaluate_internal_pre_diffed called without active_item_index — \
271 caller must set up the cache-swap before calling this method"
272 );
273
274 // Always delegate to evaluate_internal so that evaluated_schema is populated correctly
275 // for every item. The previous generation-based skip here left evaluated_schema stale
276 // (with the prior item's values) when no deps changed — causing get_evaluated_schema_subform
277 // to return wrong values for all but the last-evaluated item.
278 //
279 // evaluate_internal's all-hit fast path (lines ~314–338) handles the no-change case
280 // efficiently: it writes eval_data + evaluated_schema per formula from T1 cache and
281 // skips the expensive formula engine entirely.
282 self.evaluate_internal(paths, token)
283 }
284
285 /// Internal evaluate that can be called when data is already set
286 /// This avoids double-locking and unnecessary data cloning for re-evaluation from evaluate_dependents
287 pub(crate) fn evaluate_internal(
288 &mut self,
289 paths: Option<&[String]>,
290 token: Option<&CancellationToken>,
291 ) -> Result<(), String> {
292 if let Some(t) = token {
293 if t.is_cancelled() {
294 return Err("Cancelled".to_string());
295 }
296 }
297 time_block!(" evaluate_internal() [total]", {
298 if self.eval_cache.subform_roots.is_empty() && !self.subforms.is_empty() {
299 self.eval_cache.subform_roots = self
300 .subforms
301 .keys()
302 .map(|p| format!("/{}", crate::jsoneval::dependents::subform_field_key(p)))
303 .collect();
304 }
305
306 // Acquire lock for synchronous execution
307 let _lock = self.eval_lock.lock().unwrap();
308 let _static_guard = self
309 .engine
310 .bind_static_arrays_scope(Arc::clone(&self.static_arrays));
311
312 // Normalize paths to schema pointers for correct filtering
313 let normalized_paths_storage; // Keep alive
314 let normalized_paths = if let Some(p_list) = paths {
315 normalized_paths_storage = p_list
316 .iter()
317 .flat_map(|p| {
318 let normalized = if p.starts_with("#/") {
319 p.to_string()
320 } else if p.starts_with('/') {
321 format!("#{}", p)
322 } else {
323 format!("#/{}", p.replace('.', "/"))
324 };
325 vec![normalized]
326 })
327 .collect::<Vec<_>>();
328 Some(normalized_paths_storage.as_slice())
329 } else {
330 None
331 };
332
333 // Borrow sorted_evaluations via Arc (avoid deep-cloning Vec<Vec<String>>)
334 let eval_batches = self.sorted_evaluations.clone();
335
336 // Process each batch - sequentially
337 // Batches are processed sequentially to maintain dependency order
338 // Process value evaluations (simple computed fields with no dependencies)
339 time_block!(" evaluate values", {
340 for eval_key in self.value_evaluations.iter() {
341 if let Some(t) = token {
342 if t.is_cancelled() {
343 return Err("Cancelled".to_string());
344 }
345 }
346 // Skip if has dependencies (handled in sorted batches with correct ordering)
347 if let Some(deps) = self.dependencies.get(eval_key) {
348 if !deps.is_empty() {
349 continue;
350 }
351 }
352
353 // Filter items if paths are provided
354 if let Some(filter_paths) = normalized_paths {
355 if !filter_paths.is_empty()
356 && !filter_paths.iter().any(|p| {
357 eval_key.starts_with(p.as_str()) || p.starts_with(eval_key.as_str())
358 })
359 {
360 continue;
361 }
362 }
363
364 let pointer_path = path_utils::normalize_to_json_pointer(eval_key).into_owned();
365 let empty_deps = indexmap::IndexSet::new();
366 let deps = self.dependencies.get(eval_key).unwrap_or(&empty_deps);
367
368 // Cache hit check
369 if let Some(_cached_result) = self.eval_cache.check_cache(eval_key, deps) {
370 continue;
371 }
372
373 // Cache miss - evaluate
374 if let Some(logic_id) = self.evaluations.get(eval_key) {
375 let val = {
376 let snap = self.eval_data.snapshot_data();
377 self.engine.run(logic_id, &*snap)
378 };
379 match val {
380 Ok(val) => {
381 let cleaned_val = clean_float_noise_scalar(val);
382 self.eval_cache
383 .store_cache(eval_key, deps, cleaned_val.clone());
384
385 if let Some(pointer_value) =
386 self.evaluated_schema.pointer_mut(&pointer_path)
387 {
388 *pointer_value = cleaned_val;
389 }
390 }
391 Err(_) => {
392 // Formula failed — ensure no raw $evaluation object leaks.
393 // Write null only if the node still holds the unevaluated formula.
394 if let Some(node) = self.evaluated_schema.pointer_mut(&pointer_path)
395 {
396 if node.is_object() && node.get("$evaluation").is_some() {
397 *node = Value::Null;
398 }
399 }
400 }
401 }
402 }
403 }
404 });
405
406 time_block!(" process batches", {
407 for batch in eval_batches.iter() {
408 if let Some(t) = token {
409 if t.is_cancelled() {
410 return Err("Cancelled".to_string());
411 }
412 }
413 // Skip empty batches
414 if batch.is_empty() {
415 continue;
416 }
417
418 // Check if we can skip this entire batch optimization
419 let batch_skipped = time_block!(" batch filter check", {
420 if let Some(filter_paths) = normalized_paths {
421 if !filter_paths.is_empty() {
422 let batch_has_match = batch.iter().any(|eval_key| {
423 filter_paths.iter().any(|p| {
424 eval_key.starts_with(p.as_str())
425 || (p.starts_with(eval_key.as_str())
426 && !eval_key.contains("/$params/"))
427 })
428 });
429 !batch_has_match
430 } else {
431 false
432 }
433 } else {
434 false
435 }
436 });
437 if batch_skipped {
438 continue;
439 }
440
441 // Fast path: try to resolve every eval_key in this batch from cache.
442 // If all hit, skip the expensive exclusive_clone() of the full eval_data tree.
443 // This is critical for subforms where eval_data contains the full parent payload.
444 let mut batch_hits: Vec<(String, String, std::sync::Arc<Value>)> =
445 Vec::with_capacity(batch.len());
446 let all_hit = batch.iter().all(|eval_key| {
447 let empty_deps = indexmap::IndexSet::new();
448 let deps = self.dependencies.get(eval_key).unwrap_or(&empty_deps);
449 let cached = if self.table_metadata.contains_key(eval_key) {
450 self.eval_cache.check_table_cache(eval_key, deps)
451 } else {
452 self.eval_cache.check_cache_arc(eval_key, deps)
453 };
454 if let Some(cached_arc) = cached {
455 let pointer_path =
456 path_utils::normalize_to_json_pointer(eval_key).into_owned();
457 batch_hits.push((eval_key.clone(), pointer_path, cached_arc));
458 true
459 } else {
460 false
461 }
462 });
463
464 if all_hit {
465 // Populate eval_data AND evaluated_schema so both downstream batches
466 // and get_evaluated_schema callers see the correct per-item values.
467 for (eval_key, ptr, arc_val) in batch_hits {
468 if self.table_metadata.contains_key(&eval_key) {
469 let static_key = format!("/$table{}", ptr);
470 Arc::make_mut(&mut self.static_arrays)
471 .insert(static_key.clone(), std::sync::Arc::clone(&arc_val));
472 let marker = serde_json::json!({ "$static_array": static_key });
473 self.eval_data.set(&ptr, marker.clone());
474 self.engine
475 .set_static_arrays(std::sync::Arc::clone(&self.static_arrays));
476 if let Some(schema_value) = self.evaluated_schema.pointer_mut(&ptr)
477 {
478 *schema_value = marker;
479 }
480 } else {
481 self.eval_data.set(&ptr, (*arc_val).clone());
482 if let Some(schema_value) = self.evaluated_schema.pointer_mut(&ptr)
483 {
484 *schema_value = (*arc_val).clone();
485 }
486 }
487 }
488 continue;
489 }
490
491 // Sequential execution.
492 // For each formula miss, snapshot_data() gives an O(1) Arc::clone
493 // as a stable read view. The Arc is dropped before self.eval_data.set()
494 // so Arc::make_mut always finds rc=1 — zero deep copy, zero latency.
495 time_block!(" batch sequential eval", {
496 for eval_key in batch {
497 if let Some(t) = token {
498 if t.is_cancelled() {
499 return Err("Cancelled".to_string());
500 }
501 }
502 // Filter individual items if paths are provided
503 if let Some(filter_paths) = normalized_paths {
504 if !filter_paths.is_empty()
505 && !filter_paths.iter().any(|p| {
506 eval_key.starts_with(p.as_str())
507 || (p.starts_with(eval_key.as_str())
508 && !eval_key.contains("/$params/"))
509 })
510 {
511 continue;
512 }
513 }
514
515 let pointer_path =
516 path_utils::normalize_to_json_pointer(eval_key).into_owned();
517
518 // Cache miss - evaluate
519 let is_table = self.table_metadata.contains_key(eval_key);
520
521 if is_table {
522 let empty_deps = indexmap::IndexSet::new();
523 let deps = self.dependencies.get(eval_key).unwrap_or(&empty_deps);
524 if let Some(cached_arc) =
525 self.eval_cache.check_table_cache(eval_key, deps)
526 {
527 let static_key = format!("/$table{}", pointer_path);
528 Arc::make_mut(&mut self.static_arrays).insert(
529 static_key.clone(),
530 std::sync::Arc::clone(&cached_arc),
531 );
532 let marker = serde_json::json!({ "$static_array": static_key });
533 self.eval_data.set(&pointer_path, marker.clone());
534 self.engine.set_static_arrays(std::sync::Arc::clone(
535 &self.static_arrays,
536 ));
537 if let Some(schema_value) =
538 self.evaluated_schema.pointer_mut(&pointer_path)
539 {
540 *schema_value = marker;
541 }
542 continue;
543 }
544 time_block!(" table eval", {
545 // Snapshot for table read access: Arc::clone is O(1).
546 // Scoped so it's dropped before self.eval_data.set() below,
547 // keeping self.eval_data.data at rc=1 so Arc::make_mut is free.
548 let table_result = {
549 let table_scope =
550 EvalData::from_arc(self.eval_data.snapshot_data());
551 table_evaluate::evaluate_table(
552 self,
553 eval_key,
554 &table_scope,
555 token,
556 )
557 // table_scope dropped here → rc back to 1
558 };
559 if let Ok((arc_value, external_deps_opt)) = table_result {
560 if let Some(external_deps) = external_deps_opt {
561 self.eval_cache.store_cache_arc(
562 eval_key,
563 &external_deps,
564 std::sync::Arc::clone(&arc_value),
565 );
566 }
567
568 // NOTE: bump_params_version / bump_data_version for table results
569 // is now handled inside store_cache (conditional on value change).
570 // The separate bump here was double-counting: store_cache uses T2
571 // comparison while this block used eval_data as reference point,
572 // causing two version increments per changed table.
573
574 let static_key = format!("/$table{}", pointer_path);
575
576 Arc::make_mut(&mut self.static_arrays).insert(
577 static_key.clone(),
578 std::sync::Arc::clone(&arc_value),
579 );
580
581 let marker =
582 serde_json::json!({ "$static_array": static_key });
583 self.eval_data.set(&pointer_path, marker.clone());
584 self.engine
585 .set_static_arrays(Arc::clone(&self.static_arrays));
586
587 if let Some(schema_value) =
588 self.evaluated_schema.pointer_mut(&pointer_path)
589 {
590 *schema_value = marker;
591 }
592 }
593 });
594 } else {
595 let empty_deps = indexmap::IndexSet::new();
596 let deps = self.dependencies.get(eval_key).unwrap_or(&empty_deps);
597 let cached_result = self.eval_cache.check_cache(eval_key, &deps);
598
599 time_block!(" formula eval", {
600 if let Some(cached_result) = cached_result {
601 // Must still populate eval_data out of cache so subsequent formulas
602 // referencing this path in the same iteration can read the exact value
603 self.eval_data.set(&pointer_path, cached_result.clone());
604 if let Some(schema_value) =
605 self.evaluated_schema.pointer_mut(&pointer_path)
606 {
607 *schema_value = cached_result;
608 }
609 } else if let Some(logic_id) = self.evaluations.get(eval_key) {
610 // snapshot_data() is O(1) Arc::clone — no deep copy.
611 // Arc is moved into `snap` and lives only for the
612 // engine.run() call, then dropped before set() below.
613 // This keeps self.eval_data.data at rc=1 when set()
614 // calls Arc::make_mut, so no deep clone ever occurs.
615 let val = {
616 let snap = self.eval_data.snapshot_data();
617 self.engine.run(logic_id, &*snap)
618 // snap dropped here → rc back to 1
619 };
620 match val {
621 Ok(val) => {
622 let cleaned_val = clean_float_noise_scalar(val);
623 let data_path = crate::jsoneval::path_utils::schema_path_to_data_pointer(&pointer_path).into_owned();
624 self.eval_cache.store_cache(
625 eval_key,
626 &deps,
627 cleaned_val.clone(),
628 );
629
630 // Bump data_versions when non-$params field value changes.
631 // $params bumps are handled inside store_cache (conditional).
632 let old_val = self
633 .eval_data
634 .get(&data_path)
635 .cloned()
636 .unwrap_or(Value::Null);
637 if cleaned_val != old_val
638 && !data_path.starts_with("/$params")
639 {
640 self.eval_cache.bump_data_version(&data_path);
641 }
642
643 self.eval_data
644 .set(&pointer_path, cleaned_val.clone());
645 if let Some(schema_value) =
646 self.evaluated_schema.pointer_mut(&pointer_path)
647 {
648 *schema_value = cleaned_val;
649 }
650 }
651 Err(_) => {
652 // Formula failed — ensure no raw $evaluation object leaks.
653 // Write null only if the node still holds the unevaluated formula.
654 if let Some(node) =
655 self.evaluated_schema.pointer_mut(&pointer_path)
656 {
657 if node.is_object()
658 && node.get("$evaluation").is_some()
659 {
660 *node = Value::Null;
661 }
662 }
663 }
664 }
665 }
666 });
667 }
668 }
669 });
670 }
671 });
672
673 // Drop lock before calling evaluate_others
674 drop(_lock);
675
676 // Mark generation stable so the next evaluate_internal call can detect whether
677 // any formula was actually re-stored (via bump_data/params_version) since this run.
678 self.eval_cache.mark_evaluated();
679
680 self.evaluate_others(paths, token);
681
682 Ok(())
683 })
684 }
685
686 pub(crate) fn evaluate_others(
687 &mut self,
688 paths: Option<&[String]>,
689 token: Option<&CancellationToken>,
690 ) {
691 if let Some(t) = token {
692 if t.is_cancelled() {
693 return;
694 }
695 }
696 time_block!(" evaluate_others()", {
697 // Step 1: Evaluate "rules" and "others" categories with caching
698 // Rules are evaluated here so their values are available in evaluated_schema
699 let combined_count = self.rules_evaluations.len() + self.others_evaluations.len();
700 if combined_count > 0 {
701 time_block!(" evaluate rules+others", {
702 let eval_data_snapshot = self.eval_data.clone();
703
704 let normalized_paths: Option<Vec<String>> = paths.map(|p_list| {
705 p_list
706 .iter()
707 .flat_map(|p| {
708 let ptr = path_utils::dot_notation_to_schema_pointer(p);
709 // Also support version with /properties/ prefix for root match
710 let with_props = if ptr.starts_with("#/") {
711 format!("#/properties/{}", &ptr[2..])
712 } else {
713 ptr.clone()
714 };
715 vec![ptr, with_props]
716 })
717 .collect()
718 });
719
720 // Sequential evaluation
721 let combined_evals: Vec<&String> = self
722 .rules_evaluations
723 .iter()
724 .chain(self.others_evaluations.iter())
725 .collect();
726
727 for eval_key in combined_evals {
728 if let Some(t) = token {
729 if t.is_cancelled() {
730 return;
731 }
732 }
733
734 // // Defer options array evaluation — only the root /options field,
735 // // not its children (e.g. /options/0/label are still evaluated normally).
736 // // Call get_field_options() to resolve on demand.
737 // if eval_key.ends_with("/options") {
738 // continue;
739 // }
740
741 // Filter items if paths are provided
742 if let Some(filter_paths) = normalized_paths.as_ref() {
743 if !filter_paths.is_empty()
744 && !filter_paths.iter().any(|p| {
745 eval_key.starts_with(p.as_str())
746 || (p.starts_with(eval_key.as_str())
747 && !eval_key.contains("/$params/"))
748 })
749 {
750 continue;
751 }
752 }
753
754 let pointer_path =
755 path_utils::normalize_to_json_pointer(eval_key).into_owned();
756 let empty_deps = indexmap::IndexSet::new();
757 let deps = self.dependencies.get(eval_key).unwrap_or(&empty_deps);
758
759 if let Some(cached_result) = self.eval_cache.check_cache(eval_key, &deps) {
760 if let Some(pointer_value) =
761 self.evaluated_schema.pointer_mut(&pointer_path)
762 {
763 if !pointer_path.starts_with("$")
764 && pointer_path.contains("/rules/")
765 && !pointer_path.ends_with("/value")
766 {
767 if let Some(pointer_obj) = pointer_value.as_object_mut() {
768 pointer_obj.remove("$evaluation");
769 pointer_obj
770 .insert("value".to_string(), cached_result.clone());
771 }
772 } else {
773 *pointer_value = cached_result.clone();
774 }
775 }
776 continue;
777 }
778 if let Some(logic_id) = self.evaluations.get(eval_key) {
779 match self.engine.run(logic_id, eval_data_snapshot.data()) {
780 Ok(val) => {
781 let cleaned_val = clean_float_noise_scalar(val);
782 self.eval_cache.store_cache(
783 eval_key,
784 &deps,
785 cleaned_val.clone(),
786 );
787
788 if let Some(pointer_value) =
789 self.evaluated_schema.pointer_mut(&pointer_path)
790 {
791 if !pointer_path.starts_with("$")
792 && pointer_path.contains("/rules/")
793 && !pointer_path.ends_with("/value")
794 {
795 match pointer_value.as_object_mut() {
796 Some(pointer_obj) => {
797 pointer_obj.remove("$evaluation");
798 pointer_obj
799 .insert("value".to_string(), cleaned_val);
800 }
801 None => continue,
802 }
803 } else {
804 *pointer_value = cleaned_val;
805 }
806 }
807 }
808 Err(_) => {
809 // Formula failed — ensure no raw $evaluation object leaks.
810 // Write null only if the node still holds the unevaluated formula.
811 if let Some(node) =
812 self.evaluated_schema.pointer_mut(&pointer_path)
813 {
814 if node.is_object() && node.get("$evaluation").is_some() {
815 *node = Value::Null;
816 }
817 }
818 }
819 }
820 }
821 }
822 });
823 }
824 });
825
826 self.refresh_computed_value_dependents(token);
827 self.evaluate_options_templates(paths);
828
829 self.invalidate_layout_cache();
830 }
831
832 /// Re-evaluate direct dependents of computed fields against a temporary data overlay.
833 /// Computed values are exposed only for this refresh; shared form data, cache entries and
834 /// version trackers remain untouched, preventing subform/table cascade contamination.
835 fn refresh_computed_value_dependents(&mut self, token: Option<&CancellationToken>) {
836 let mut changed = Vec::new();
837 let mut dep_matchers = indexmap::IndexSet::new();
838
839 for key in self.evaluations.keys() {
840 let Some(field_path) = key.strip_suffix("/value") else {
841 continue;
842 };
843 if !field_path.contains("/properties/") || key.contains("/rules/") {
844 continue;
845 }
846 let schema_pointer = path_utils::normalize_to_json_pointer(key);
847 let Some(value) = self.evaluated_schema.pointer(&schema_pointer) else {
848 continue;
849 };
850 if value.is_object() && value.get("$evaluation").is_some() {
851 continue;
852 }
853 let data_path = path_utils::schema_path_to_data_pointer(field_path).into_owned();
854 if self.eval_data.get(&data_path) != Some(value) {
855 dep_matchers.insert(data_path.clone());
856 dep_matchers.insert(field_path.to_string());
857 dep_matchers.insert(field_path.trim_start_matches('#').to_string());
858 changed.push((data_path, value.clone()));
859 }
860 }
861
862 if changed.is_empty() {
863 return;
864 }
865
866 let targets: Vec<String> = self
867 .evaluations
868 .keys()
869 .filter(|key| {
870 !key.contains("/dependents/")
871 && !key.contains("/$params/")
872 && !self.tables.keys().any(|table| key.starts_with(table))
873 && self.dependencies.get(*key).is_some_and(|dependencies| {
874 dependencies.iter().any(|dependency| {
875 dep_matchers.contains(dependency.as_str())
876 || dep_matchers.contains(
877 path_utils::schema_path_to_data_pointer(dependency).as_ref(),
878 )
879 })
880 })
881 })
882 .cloned()
883 .collect();
884
885 if targets.is_empty() {
886 return;
887 }
888
889 let mut overlay = EvalData::new(self.eval_data.snapshot_data_clone());
890 for (data_path, value) in changed {
891 overlay.set(&data_path, value);
892 }
893
894 for key in targets {
895 if token.is_some_and(CancellationToken::is_cancelled) {
896 return;
897 }
898 let Some(logic_id) = self.evaluations.get(&key) else {
899 continue;
900 };
901 let Ok(value) = self.engine.run(logic_id, overlay.data()) else {
902 continue;
903 };
904 let pointer = path_utils::normalize_to_json_pointer(&key);
905 if let Some(node) = self.evaluated_schema.pointer_mut(&pointer) {
906 let value = clean_float_noise_scalar(value);
907 if pointer.contains("/rules/") && !pointer.ends_with("/value") {
908 if let Some(rule) = node.as_object_mut() {
909 rule.remove("$evaluation");
910 rule.insert("value".to_string(), value);
911 }
912 } else {
913 *node = value;
914 }
915 }
916 }
917 }
918
919 /// Evaluate options URL templates (handles {variable} patterns) — called on demand from get_field_options
920 #[allow(dead_code)]
921 pub(crate) fn evaluate_options_templates(&mut self, paths: Option<&[String]>) {
922 // Use pre-collected options templates from parsing (Arc clone is cheap)
923 let templates_to_eval = self.options_templates.clone();
924
925 // Evaluate each template
926 for (path, template_str, params_path) in templates_to_eval.iter() {
927 // Filter items if paths are provided
928 // 'path' here is the schema path to the field (dot notation or similar, need to check)
929 // It seems to be schema pointer based on usage in other methods
930 if let Some(filter_paths) = paths {
931 if !filter_paths.is_empty()
932 && !filter_paths
933 .iter()
934 .any(|p| path.starts_with(p.as_str()) || p.starts_with(path.as_str()))
935 {
936 continue;
937 }
938 }
939
940 if let Some(params) = self.evaluated_schema.pointer(¶ms_path) {
941 if let Ok(evaluated) = self.evaluate_template(&template_str, params) {
942 if let Some(target) = self.evaluated_schema.pointer_mut(&path) {
943 *target = Value::String(evaluated);
944 }
945 }
946 }
947 }
948 }
949
950 /// Evaluate a template string like "api/users/{id}" with params
951 pub(crate) fn evaluate_template(
952 &self,
953 template: &str,
954 params: &Value,
955 ) -> Result<String, String> {
956 let mut result = template.to_string();
957
958 // Simple template evaluation: replace {key} with params.key
959 if let Value::Object(params_map) = params {
960 for (key, value) in params_map {
961 let placeholder = format!("{{{}}}", key);
962 if let Some(str_val) = value.as_str() {
963 result = result.replace(&placeholder, str_val);
964 } else {
965 // Convert non-string values to strings
966 result = result.replace(&placeholder, &value.to_string());
967 }
968 }
969 }
970
971 Ok(result)
972 }
973}