1use indexmap::IndexSet;
2use serde_json::Value;
3use std::collections::{HashMap, HashSet};
4
5#[derive(Default, Clone)]
7pub struct VersionTracker {
8 versions: HashMap<String, u64>,
9}
10
11impl VersionTracker {
12 pub fn new() -> Self {
13 Self {
14 versions: HashMap::new(),
15 }
16 }
17
18 #[inline]
19 pub fn get(&self, path: &str) -> u64 {
20 self.versions.get(path).copied().unwrap_or(0)
21 }
22
23 #[inline]
24 pub fn bump(&mut self, path: &str, source: &str) {
25 let current = self.get(path);
26 if crate::utils::is_debug_cache_enabled() {
27 println!(
28 "[store_cache] BUMPING for {} -> {} ({})",
29 path,
30 current + 1,
31 source
32 );
33 }
34 self.versions.insert(path.to_string(), current + 1);
36 }
37
38 pub fn merge_from(&mut self, other: &VersionTracker) {
42 for (k, v) in &other.versions {
43 let current = self.versions.get(k).copied().unwrap_or(0);
44 self.versions.insert(k.clone(), current.max(*v));
45 }
46 }
47
48 pub fn merge_from_params(&mut self, other: &VersionTracker) {
52 for (k, v) in &other.versions {
53 if k.starts_with("/$params") {
54 let current = self.versions.get(k).copied().unwrap_or(0);
55 self.versions.insert(k.clone(), current.max(*v));
56 }
57 }
58 }
59
60 pub(crate) fn merge_excluding_prefix(&mut self, other: &VersionTracker, excluded_prefix: &str) {
62 for (k, v) in &other.versions {
63 if !k.starts_with(excluded_prefix) {
64 let current = self.versions.get(k).copied().unwrap_or(0);
65 self.versions.insert(k.clone(), current.max(*v));
66 }
67 }
68 }
69
70 pub fn any_bumped_with_prefix(&self, prefix: &str) -> bool {
73 self.versions
74 .iter()
75 .any(|(k, &v)| k.starts_with(prefix) && v > 0)
76 }
77
78 pub fn any_newly_bumped_with_prefix(&self, prefix: &str, baseline: &VersionTracker) -> bool {
82 self.versions
83 .iter()
84 .any(|(k, &v)| k.starts_with(prefix) && v > baseline.get(k))
85 }
86
87 pub fn versions(&self) -> impl Iterator<Item = (&str, &u64)> {
89 self.versions.iter().map(|(k, v)| (k.as_str(), v))
90 }
91}
92
93#[derive(Clone)]
95pub struct CacheEntry {
96 pub dep_versions: HashMap<String, u64>,
97 pub result: Value,
98 pub computed_for_item: Option<usize>,
103}
104
105#[derive(Default, Clone)]
107pub struct SubformItemCache {
108 pub data_versions: VersionTracker,
109 pub entries: HashMap<String, CacheEntry>,
110 pub item_snapshot: Value,
111 pub evaluated_schema: Option<Value>,
115}
116
117impl SubformItemCache {
118 pub fn new() -> Self {
119 Self {
120 data_versions: VersionTracker::new(),
121 entries: HashMap::new(),
122 item_snapshot: Value::Null,
123 evaluated_schema: None,
124 }
125 }
126}
127
128#[derive(Clone)]
130pub struct EvalCache {
131 pub data_versions: VersionTracker,
132 pub params_versions: VersionTracker,
133 pub entries: HashMap<String, CacheEntry>,
134
135 pub active_item_index: Option<usize>,
136 pub subform_caches: HashMap<usize, SubformItemCache>,
137
138 pub eval_generation: u64,
142 pub last_evaluated_generation: u64,
143
144 pub main_form_snapshot: Option<Value>,
148}
149
150impl Default for EvalCache {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156impl EvalCache {
157 pub fn new() -> Self {
158 Self {
159 data_versions: VersionTracker::new(),
160 params_versions: VersionTracker::new(),
161 entries: HashMap::new(),
162 active_item_index: None,
163 subform_caches: HashMap::new(),
164 eval_generation: 0,
165 last_evaluated_generation: u64::MAX, main_form_snapshot: None,
167 }
168 }
169
170 pub fn clear(&mut self) {
171 self.data_versions = VersionTracker::new();
172 self.params_versions = VersionTracker::new();
173 self.entries.clear();
174 self.active_item_index = None;
175 self.subform_caches.clear();
176 self.eval_generation = 0;
177 self.last_evaluated_generation = u64::MAX;
178 self.main_form_snapshot = None;
179 }
180
181 pub fn prune_subform_caches(&mut self, current_count: usize) {
185 self.subform_caches.retain(|&idx, _| idx < current_count);
186 }
187
188 pub fn invalidate_params_tables_for_item(&mut self, idx: usize, table_keys: &[String]) {
195 for key in table_keys {
197 let data_path = crate::jsoneval::path_utils::schema_path_to_data_pointer(key);
198 self.params_versions
199 .bump(&data_path, "invalidate_params_tables_for_item");
200 self.eval_generation += 1;
201 }
202
203 if let Some(item_cache) = self.subform_caches.get_mut(&idx) {
205 for key in table_keys {
206 item_cache.entries.remove(key);
207 }
208 }
209 }
210
211 pub fn needs_full_evaluation(&self) -> bool {
213 self.eval_generation != self.last_evaluated_generation
214 }
215
216 pub fn mark_evaluated(&mut self) {
218 self.last_evaluated_generation = self.eval_generation;
219 }
220
221 pub(crate) fn ensure_active_item_cache(&mut self, idx: usize) {
222 self.subform_caches
223 .entry(idx)
224 .or_insert_with(SubformItemCache::new);
225 }
226
227 pub fn set_active_item(&mut self, idx: usize) {
228 self.active_item_index = Some(idx);
229 self.ensure_active_item_cache(idx);
230 }
231
232 pub fn clear_active_item(&mut self) {
233 self.active_item_index = None;
234 }
235
236 pub fn store_snapshot_and_diff_versions(&mut self, old: &Value, new: &Value) {
238 if let Some(idx) = self.active_item_index {
239 self.ensure_active_item_cache(idx);
240 let sub_cache = self.subform_caches.get_mut(&idx).unwrap();
241 diff_and_update_versions(
242 &mut sub_cache.data_versions,
243 "",
244 old,
245 new,
246 "subform store_snapshot_and_diff_versions",
247 );
248 sub_cache.item_snapshot = new.clone();
249 } else {
250 diff_and_update_versions(
251 &mut self.data_versions,
252 "",
253 old,
254 new,
255 "store_snapshot_and_diff_versions",
256 );
257 }
258 }
259
260 pub fn get_active_snapshot(&self) -> Value {
261 if let Some(idx) = self.active_item_index {
262 self.subform_caches
263 .get(&idx)
264 .map(|c| c.item_snapshot.clone())
265 .unwrap_or(Value::Null)
266 } else {
267 Value::Null
268 }
269 }
270
271 pub fn diff_active_item(
272 &mut self,
273 field_key: &str,
274 old_sub_data: &Value,
275 new_sub_data: &Value,
276 ) {
277 if let Some(idx) = self.active_item_index {
278 self.ensure_active_item_cache(idx);
279 let sub_cache = self.subform_caches.get_mut(&idx).unwrap();
280
281 let empty = Value::Null;
283 let old_item = old_sub_data.get(field_key).unwrap_or(&empty);
284 let new_item = new_sub_data.get(field_key).unwrap_or(&empty);
285
286 diff_and_update_versions(
287 &mut sub_cache.data_versions,
288 &format!("/{}", field_key),
289 old_item,
290 new_item,
291 format!("diff_active_item {}", field_key).as_str(),
292 );
293 sub_cache.item_snapshot = new_sub_data.clone();
294 }
295 }
296
297 pub fn bump_data_version(&mut self, data_path: &str) {
298 self.eval_generation += 1;
301 if let Some(idx) = self.active_item_index {
302 if let Some(cache) = self.subform_caches.get_mut(&idx) {
303 cache.data_versions.bump(data_path, "bump_data_version1");
304 }
305 } else {
306 self.data_versions.bump(data_path, "bump_data_version2");
307 }
308 }
309
310 pub fn bump_params_version(&mut self, data_path: &str) {
311 self.params_versions.bump(data_path, "bump_params_version");
312 self.eval_generation += 1;
313 }
314
315 pub fn check_cache(&self, eval_key: &str, deps: &IndexSet<String>) -> Option<Value> {
321 if let Some(idx) = self.active_item_index {
322 if let Some(cache) = self.subform_caches.get(&idx) {
324 if let Some(hit) =
325 self.validate_entry(eval_key, deps, &cache.entries, &cache.data_versions)
326 {
327 if crate::utils::is_debug_cache_enabled() {
328 println!("Cache HIT [T1 idx={}] {}", idx, eval_key);
329 }
330 return Some(hit);
331 }
332 }
333
334 let item_data_versions = self
340 .subform_caches
341 .get(&idx)
342 .map(|c| &c.data_versions)
343 .unwrap_or(&self.data_versions);
344
345 if let Some(entry) = self.entries.get(eval_key) {
346 let index_safe = match entry.computed_for_item {
347 None => entry.dep_versions.keys().all(|p| p.starts_with("/$params")),
352 Some(stored_idx) if stored_idx == idx => true,
353 _ => entry.dep_versions.keys().all(|p| p.starts_with("/$params")),
354 };
355 if index_safe {
356 let result =
357 self.validate_entry(eval_key, deps, &self.entries, item_data_versions);
358 if result.is_some() {
359 if crate::utils::is_debug_cache_enabled() {
360 println!(
361 "Cache HIT [T2 idx={} for={:?}] {}",
362 idx, entry.computed_for_item, eval_key
363 );
364 }
365 }
366 return result;
367 }
368 }
369
370 None
371 } else {
372 self.validate_entry(eval_key, deps, &self.entries, &self.data_versions)
373 }
374 }
375
376 pub fn check_table_cache(&self, eval_key: &str, deps: &IndexSet<String>) -> Option<Value> {
388 if let Some(idx) = self.active_item_index {
389 if let Some(cache) = self.subform_caches.get(&idx) {
391 if let Some(hit) =
392 self.validate_entry(eval_key, deps, &cache.entries, &cache.data_versions)
393 {
394 if crate::utils::is_debug_cache_enabled() {
395 println!("Cache HIT [T1 table idx={}] {}", idx, eval_key);
396 }
397 return Some(hit);
398 }
399 }
400
401 let has_item_data_dependency = deps.iter().any(|dep| {
405 !crate::jsoneval::path_utils::schema_path_to_data_pointer(dep)
406 .starts_with("/$params")
407 });
408 if has_item_data_dependency {
409 return None;
410 }
411
412 let result = self.validate_entry(eval_key, deps, &self.entries, &self.data_versions);
413 if result.is_some() {
414 if crate::utils::is_debug_cache_enabled() {
415 println!("Cache HIT [T2 table idx={}] {}", idx, eval_key);
416 }
417 }
418 result
419 } else {
420 self.validate_entry(eval_key, deps, &self.entries, &self.data_versions)
421 }
422 }
423
424 fn validate_entry(
425 &self,
426 eval_key: &str,
427 deps: &IndexSet<String>,
428 entries: &HashMap<String, CacheEntry>,
429 data_versions: &VersionTracker,
430 ) -> Option<Value> {
431 let entry = entries.get(eval_key)?;
432 for dep in deps {
433 let data_dep_path = crate::jsoneval::path_utils::schema_path_to_data_pointer(dep);
434
435 let current_ver = if data_dep_path.starts_with("/$params") {
436 self.params_versions.get(&data_dep_path)
437 } else {
438 data_versions.get(&data_dep_path)
439 };
440
441 if let Some(&cached_ver) = entry.dep_versions.get(data_dep_path.as_ref()) {
442 if current_ver != cached_ver {
443 if crate::utils::is_debug_cache_enabled() {
444 println!(
445 "Cache MISS {}: dep {} changed ({} -> {})",
446 eval_key, data_dep_path, cached_ver, current_ver
447 );
448 }
449 return None;
450 }
451 } else {
452 if crate::utils::is_debug_cache_enabled() {
453 println!(
454 "Cache MISS {}: dep {} missing from cache entry",
455 eval_key, data_dep_path
456 );
457 }
458 return None;
459 }
460 }
461 if crate::utils::is_debug_cache_enabled() {
462 println!("Cache HIT {}", eval_key);
463 }
464 Some(entry.result.clone())
465 }
466
467 pub fn store_cache(&mut self, eval_key: &str, deps: &IndexSet<String>, result: Value) {
475 let mut dep_versions = HashMap::with_capacity(deps.len());
479 {
480 let data_versions = if let Some(idx) = self.active_item_index {
481 self.ensure_active_item_cache(idx);
482 &self.subform_caches[&idx].data_versions
483 } else {
484 &self.data_versions
485 };
486
487 for dep in deps {
488 let data_dep_path = crate::jsoneval::path_utils::schema_path_to_data_pointer(dep);
489 let ver = if data_dep_path.starts_with("/$params") {
490 self.params_versions.get(&data_dep_path)
491 } else {
492 data_versions.get(&data_dep_path)
493 };
494 dep_versions.insert(data_dep_path.into_owned(), ver);
495 }
496 }
497
498 let computed_for_item = self.active_item_index;
500
501 if eval_key.starts_with("#/$params") {
510 let existing_result: Option<&Value> = if let Some(idx) = self.active_item_index {
511 self.entries.get(eval_key).map(|e| &e.result).or_else(|| {
513 self.subform_caches
514 .get(&idx)
515 .and_then(|c| c.entries.get(eval_key))
516 .map(|e| &e.result)
517 })
518 } else {
519 self.entries.get(eval_key).map(|e| &e.result)
520 };
521
522 let value_changed = existing_result.map_or(true, |r| r != &result);
523
524 if value_changed {
525 let data_path = crate::jsoneval::path_utils::schema_path_to_data_pointer(eval_key);
526
527 let mut current_path = data_path.as_ref();
530 let mut slash_count = current_path.matches('/').count();
531
532 while slash_count >= 3 {
533 self.params_versions.bump(current_path, "store_cache");
534 if let Some(last_slash) = current_path.rfind('/') {
535 current_path = ¤t_path[..last_slash];
536 slash_count -= 1;
537 } else {
538 break;
539 }
540 }
541
542 self.eval_generation += 1;
543 }
544 }
545
546 let entry = CacheEntry {
547 dep_versions,
548 result,
549 computed_for_item,
550 };
551
552 if let Some(idx) = self.active_item_index {
553 self.subform_caches
555 .get_mut(&idx)
556 .unwrap()
557 .entries
558 .insert(eval_key.to_string(), entry.clone());
559
560 if eval_key.starts_with("#/$params") {
569 let t2_dep_versions: HashMap<String, u64> = entry
570 .dep_versions
571 .iter()
572 .map(|(path, &item_ver)| {
573 let parent_ver = if path.starts_with("/$params") {
574 item_ver } else {
576 self.data_versions.get(path)
578 };
579 (path.clone(), parent_ver)
580 })
581 .collect();
582
583 let t2_entry = CacheEntry {
584 dep_versions: t2_dep_versions,
585 result: entry.result.clone(),
586 computed_for_item,
587 };
588 self.entries.insert(eval_key.to_string(), t2_entry);
589 }
590 } else {
591 self.entries.insert(eval_key.to_string(), entry);
592 }
593 }
594}
595
596pub(crate) fn diff_and_update_versions(
598 tracker: &mut VersionTracker,
599 pointer: &str,
600 old: &Value,
601 new: &Value,
602 source: &str,
603) {
604 let mut pointer_buf = String::with_capacity(128);
605 pointer_buf.push_str(pointer);
606 diff_and_update_versions_internal(tracker, &mut pointer_buf, old, new, source);
607}
608
609#[cfg(test)]
610mod cache_tests {
611 use super::{CacheEntry, EvalCache};
612 use indexmap::IndexSet;
613 use serde_json::json;
614 use std::collections::HashMap;
615
616 #[test]
617 fn active_item_does_not_reuse_global_table_with_item_dependency() {
618 let mut cache = EvalCache::new();
619 cache.set_active_item(1);
620
621 let eval_key = "#/$params/references/RIDER_RATE";
622 let deps = IndexSet::from_iter(["#/riders/properties/benefit".to_string()]);
623 cache.entries.insert(
624 eval_key.to_string(),
625 CacheEntry {
626 dep_versions: HashMap::from([("/riders/benefit".to_string(), 0)]),
627 result: json!([{"rate": 97}]),
628 computed_for_item: None,
629 },
630 );
631
632 assert!(
633 cache.check_table_cache(eval_key, &deps).is_none(),
634 "rider-dependent table rows must be scoped to active item"
635 );
636 }
637
638 #[test]
639 fn active_item_reuses_global_table_with_only_params_dependencies() {
640 let mut cache = EvalCache::new();
641 cache.set_active_item(1);
642
643 let eval_key = "#/$params/references/SHARED_RATE";
644 let deps = IndexSet::from_iter(["#/$params/others/currency".to_string()]);
645 cache.entries.insert(
646 eval_key.to_string(),
647 CacheEntry {
648 dep_versions: HashMap::from([("/$params/others/currency".to_string(), 0)]),
649 result: json!([{"rate": 10}]),
650 computed_for_item: None,
651 },
652 );
653
654 assert_eq!(
655 cache.check_table_cache(eval_key, &deps),
656 Some(json!([{"rate": 10}]))
657 );
658 }
659}
660
661fn diff_and_update_versions_internal(
662 tracker: &mut VersionTracker,
663 pointer: &mut String,
664 old: &Value,
665 new: &Value,
666 source: &str,
667) {
668 if old == new {
669 return;
670 }
671
672 if crate::utils::is_debug_cache_enabled() {
673 println!(
674 "[diff_and_update_versions_internal] {} pointer={}, old={:?}, new={:?}",
675 source, pointer, old, new
676 );
677 }
678
679 match (old, new) {
680 (Value::Object(a), Value::Object(b)) => {
681 let mut keys = HashSet::new();
682 for k in a.keys() {
683 keys.insert(k.as_str());
684 }
685 for k in b.keys() {
686 keys.insert(k.as_str());
687 }
688
689 for key in keys {
690 if key == "$params" {
694 continue;
695 }
696
697 let a_val = a.get(key).unwrap_or(&Value::Null);
698 let b_val = b.get(key).unwrap_or(&Value::Null);
699
700 let escaped_key = key.replace('~', "~0").replace('/', "~1");
701 let old_len = pointer.len();
702 pointer.push('/');
703 pointer.push_str(&escaped_key);
704 diff_and_update_versions_internal(tracker, pointer, a_val, b_val, source);
705 pointer.truncate(old_len);
706 }
707 }
708 (Value::Array(a), Value::Array(b)) => {
709 let max_len = a.len().max(b.len());
710 for i in 0..max_len {
711 let a_val = a.get(i).unwrap_or(&Value::Null);
712 let b_val = b.get(i).unwrap_or(&Value::Null);
713 let old_len = pointer.len();
714 use std::fmt::Write;
715 write!(pointer, "/{}", i).unwrap();
716 diff_and_update_versions_internal(tracker, pointer, a_val, b_val, source);
717 pointer.truncate(old_len);
718 }
719 }
720 (old_val, new_val) => {
721 if old_val != new_val {
722 if crate::utils::is_debug_cache_enabled() {
723 println!(
724 "[store_cache] Catch-all for {}: old={}, new={}",
725 pointer,
726 match old_val {
727 Value::Null => "Null",
728 Value::Bool(_) => "Bool",
729 Value::Number(_) => "Number",
730 Value::String(_) => "String",
731 Value::Array(_) => "Array",
732 Value::Object(_) => "Object",
733 },
734 match new_val {
735 Value::Null => "Null",
736 Value::Bool(_) => "Bool",
737 Value::Number(_) => "Number",
738 Value::String(_) => "String",
739 Value::Array(_) => "Array",
740 Value::Object(_) => "Object",
741 }
742 );
743 }
744 tracker.bump(pointer, "diff_and_update_versions_internal");
745
746 if old_val.is_object() || old_val.is_array() {
749 traverse_and_bump(tracker, pointer, old_val);
750 }
751 if new_val.is_object() || new_val.is_array() {
752 traverse_and_bump(tracker, pointer, new_val);
753 }
754 }
755 }
756 }
757}
758
759#[cfg(test)]
763mod tests {
764 use super::VersionTracker;
765
766 #[test]
767 fn merge_excluding_prefix_keeps_item_versions_isolated() {
768 let mut item = VersionTracker::new();
769 item.bump("/riders/wop_flag", "test");
770
771 let mut parent = VersionTracker::new();
772 parent.bump("/illustration/insured/phins_relation", "test");
773 parent.bump("/riders/wop_flag", "test");
774
775 item.merge_excluding_prefix(&parent, "/riders/");
776
777 assert_eq!(item.get("/illustration/insured/phins_relation"), 1);
778 assert_eq!(
779 item.get("/riders/wop_flag"),
780 1,
781 "another rider's parent-tracker bump must not alter this item's version"
782 );
783 }
784}
785
786fn traverse_and_bump(tracker: &mut VersionTracker, pointer: &mut String, val: &Value) {
787 match val {
788 Value::Object(map) => {
789 for (key, v) in map {
790 if key == "$params" {
791 continue; }
793 let escaped_key = key.replace('~', "~0").replace('/', "~1");
794 let old_len = pointer.len();
795 pointer.push('/');
796 pointer.push_str(&escaped_key);
797 tracker.bump(pointer, "traverse_and_bump1");
798 traverse_and_bump(tracker, pointer, v);
799 pointer.truncate(old_len);
800 }
801 }
802 Value::Array(arr) => {
803 for (i, v) in arr.iter().enumerate() {
804 let old_len = pointer.len();
805 use std::fmt::Write;
806 write!(pointer, "/{}", i).unwrap();
807 tracker.bump(pointer, "traverse_and_bump2");
808 traverse_and_bump(tracker, pointer, v);
809 pointer.truncate(old_len);
810 }
811 }
812 _ => {}
813 }
814}