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) {
25 let current = self.get(path);
26 self.versions.insert(path.to_string(), current + 1);
28 }
29
30 pub fn merge_from(&mut self, other: &VersionTracker) {
34 for (k, v) in &other.versions {
35 let current = self.versions.get(k).copied().unwrap_or(0);
36 self.versions.insert(k.clone(), current.max(*v));
37 }
38 }
39
40 pub fn merge_from_params(&mut self, other: &VersionTracker) {
44 for (k, v) in &other.versions {
45 if k.starts_with("/$params") {
46 let current = self.versions.get(k).copied().unwrap_or(0);
47 self.versions.insert(k.clone(), current.max(*v));
48 }
49 }
50 }
51
52 pub fn any_bumped_with_prefix(&self, prefix: &str) -> bool {
55 self.versions
56 .iter()
57 .any(|(k, &v)| k.starts_with(prefix) && v > 0)
58 }
59
60 pub fn any_newly_bumped_with_prefix(&self, prefix: &str, baseline: &VersionTracker) -> bool {
64 self.versions
65 .iter()
66 .any(|(k, &v)| k.starts_with(prefix) && v > baseline.get(k))
67 }
68
69 pub fn versions(&self) -> impl Iterator<Item = (&str, &u64)> {
71 self.versions.iter().map(|(k, v)| (k.as_str(), v))
72 }
73}
74
75#[derive(Clone)]
77pub struct CacheEntry {
78 pub dep_versions: HashMap<String, u64>,
79 pub result: Value,
80 pub computed_for_item: Option<usize>,
85}
86
87#[derive(Default, Clone)]
89pub struct SubformItemCache {
90 pub data_versions: VersionTracker,
91 pub entries: HashMap<String, CacheEntry>,
92 pub item_snapshot: Value,
93}
94
95impl SubformItemCache {
96 pub fn new() -> Self {
97 Self {
98 data_versions: VersionTracker::new(),
99 entries: HashMap::new(),
100 item_snapshot: Value::Null,
101 }
102 }
103}
104
105#[derive(Clone)]
107pub struct EvalCache {
108 pub data_versions: VersionTracker,
109 pub params_versions: VersionTracker,
110 pub entries: HashMap<String, CacheEntry>,
111
112 pub active_item_index: Option<usize>,
113 pub subform_caches: HashMap<usize, SubformItemCache>,
114
115 pub eval_generation: u64,
119 pub last_evaluated_generation: u64,
120
121 pub main_form_snapshot: Option<Value>,
125}
126
127impl Default for EvalCache {
128 fn default() -> Self {
129 Self::new()
130 }
131}
132
133impl EvalCache {
134 pub fn new() -> Self {
135 Self {
136 data_versions: VersionTracker::new(),
137 params_versions: VersionTracker::new(),
138 entries: HashMap::new(),
139 active_item_index: None,
140 subform_caches: HashMap::new(),
141 eval_generation: 0,
142 last_evaluated_generation: u64::MAX, main_form_snapshot: None,
144 }
145 }
146
147 pub fn clear(&mut self) {
148 self.data_versions = VersionTracker::new();
149 self.params_versions = VersionTracker::new();
150 self.entries.clear();
151 self.active_item_index = None;
152 self.subform_caches.clear();
153 self.eval_generation = 0;
154 self.last_evaluated_generation = u64::MAX;
155 self.main_form_snapshot = None;
156 }
157
158 pub fn prune_subform_caches(&mut self, current_count: usize) {
162 self.subform_caches.retain(|&idx, _| idx < current_count);
163 }
164
165 pub fn invalidate_params_tables_for_item(&mut self, idx: usize, table_keys: &[String]) {
172 for key in table_keys {
174 let data_path = crate::jsoneval::path_utils::normalize_to_json_pointer(key)
175 .replace("/properties/", "/");
176 let data_path = data_path.trim_start_matches('#');
177 let data_path = if data_path.starts_with('/') {
178 data_path.to_string()
179 } else {
180 format!("/{}", data_path)
181 };
182 self.params_versions.bump(&data_path);
183 self.eval_generation += 1;
184 }
185
186 if let Some(item_cache) = self.subform_caches.get_mut(&idx) {
188 for key in table_keys {
189 item_cache.entries.remove(key);
190 }
191 }
192 }
193
194 pub fn needs_full_evaluation(&self) -> bool {
196 self.eval_generation != self.last_evaluated_generation
197 }
198
199 pub fn mark_evaluated(&mut self) {
201 self.last_evaluated_generation = self.eval_generation;
202 }
203
204 pub(crate) fn ensure_active_item_cache(&mut self, idx: usize) {
205 self.subform_caches
206 .entry(idx)
207 .or_insert_with(SubformItemCache::new);
208 }
209
210 pub fn set_active_item(&mut self, idx: usize) {
211 self.active_item_index = Some(idx);
212 self.ensure_active_item_cache(idx);
213 }
214
215 pub fn clear_active_item(&mut self) {
216 self.active_item_index = None;
217 }
218
219 pub fn store_snapshot_and_diff_versions(&mut self, old: &Value, new: &Value) {
221 if let Some(idx) = self.active_item_index {
222 self.ensure_active_item_cache(idx);
223 let sub_cache = self.subform_caches.get_mut(&idx).unwrap();
224 diff_and_update_versions(&mut sub_cache.data_versions, "", old, new);
225 sub_cache.item_snapshot = new.clone();
226 } else {
227 diff_and_update_versions(&mut self.data_versions, "", old, new);
228 }
229 }
230
231 pub fn get_active_snapshot(&self) -> Value {
232 if let Some(idx) = self.active_item_index {
233 self.subform_caches
234 .get(&idx)
235 .map(|c| c.item_snapshot.clone())
236 .unwrap_or(Value::Null)
237 } else {
238 Value::Null
239 }
240 }
241
242 pub fn diff_active_item(
243 &mut self,
244 field_key: &str,
245 old_sub_data: &Value,
246 new_sub_data: &Value,
247 ) {
248 if let Some(idx) = self.active_item_index {
249 self.ensure_active_item_cache(idx);
250 let sub_cache = self.subform_caches.get_mut(&idx).unwrap();
251
252 let empty = Value::Null;
254 let old_item = old_sub_data.get(field_key).unwrap_or(&empty);
255 let new_item = new_sub_data.get(field_key).unwrap_or(&empty);
256
257 diff_and_update_versions(
258 &mut sub_cache.data_versions,
259 &format!("/{}", field_key),
260 old_item,
261 new_item,
262 );
263 sub_cache.item_snapshot = new_sub_data.clone();
264 }
265 }
266
267 pub fn bump_data_version(&mut self, data_path: &str) {
268 self.eval_generation += 1;
271 if let Some(idx) = self.active_item_index {
272 if let Some(cache) = self.subform_caches.get_mut(&idx) {
273 cache.data_versions.bump(data_path);
274 }
275 } else {
276 self.data_versions.bump(data_path);
277 }
278 }
279
280 pub fn bump_params_version(&mut self, data_path: &str) {
281 self.params_versions.bump(data_path);
282 self.eval_generation += 1;
283 }
284
285 pub fn check_cache(&self, eval_key: &str, deps: &IndexSet<String>) -> Option<Value> {
291 if let Some(idx) = self.active_item_index {
292 if let Some(cache) = self.subform_caches.get(&idx) {
294 if let Some(hit) =
295 self.validate_entry(eval_key, deps, &cache.entries, &cache.data_versions)
296 {
297 if std::env::var("JSONEVAL_DEBUG_CACHE").is_ok() {
298 println!("Cache HIT [T1 idx={}] {}", idx, eval_key);
299 }
300 return Some(hit);
301 }
302 }
303
304 let item_data_versions = self
310 .subform_caches
311 .get(&idx)
312 .map(|c| &c.data_versions)
313 .unwrap_or(&self.data_versions);
314
315 if let Some(entry) = self.entries.get(eval_key) {
316 let index_safe = match entry.computed_for_item {
317 None => entry.dep_versions.keys().all(|p| p.starts_with("/$params")),
322 Some(stored_idx) if stored_idx == idx => true,
323 _ => entry.dep_versions.keys().all(|p| p.starts_with("/$params")),
324 };
325 if index_safe {
326 let result =
327 self.validate_entry(eval_key, deps, &self.entries, item_data_versions);
328 if result.is_some() {
329 if std::env::var("JSONEVAL_DEBUG_CACHE").is_ok() {
330 println!(
331 "Cache HIT [T2 idx={} for={:?}] {}",
332 idx, entry.computed_for_item, eval_key
333 );
334 }
335 }
336 return result;
337 }
338 }
339
340 None
341 } else {
342 self.validate_entry(eval_key, deps, &self.entries, &self.data_versions)
343 }
344 }
345
346 fn validate_entry(
347 &self,
348 eval_key: &str,
349 deps: &IndexSet<String>,
350 entries: &HashMap<String, CacheEntry>,
351 data_versions: &VersionTracker,
352 ) -> Option<Value> {
353 let entry = entries.get(eval_key)?;
354 for dep in deps {
355 let data_dep_path = crate::jsoneval::path_utils::normalize_to_json_pointer(dep)
356 .replace("/properties/", "/");
357
358 let current_ver = if data_dep_path.starts_with("/$params") {
359 self.params_versions.get(&data_dep_path)
360 } else {
361 data_versions.get(&data_dep_path)
362 };
363
364 if let Some(&cached_ver) = entry.dep_versions.get(&data_dep_path) {
365 if current_ver != cached_ver {
366 if std::env::var("JSONEVAL_DEBUG_CACHE").is_ok() {
367 println!(
368 "Cache MISS {}: dep {} changed ({} -> {})",
369 eval_key, data_dep_path, cached_ver, current_ver
370 );
371 }
372 return None;
373 }
374 } else {
375 if std::env::var("JSONEVAL_DEBUG_CACHE").is_ok() {
376 println!(
377 "Cache MISS {}: dep {} missing from cache entry",
378 eval_key, data_dep_path
379 );
380 }
381 return None;
382 }
383 }
384 if std::env::var("JSONEVAL_DEBUG_CACHE").is_ok() {
385 println!("Cache HIT {}", eval_key);
386 }
387 Some(entry.result.clone())
388 }
389
390 pub fn store_cache(&mut self, eval_key: &str, deps: &IndexSet<String>, result: Value) {
398 let mut dep_versions = HashMap::with_capacity(deps.len());
400 {
401 let data_versions = if let Some(idx) = self.active_item_index {
402 self.ensure_active_item_cache(idx);
403 &self.subform_caches[&idx].data_versions
404 } else {
405 &self.data_versions
406 };
407
408 for dep in deps {
409 let data_dep_path = crate::jsoneval::path_utils::normalize_to_json_pointer(dep)
410 .replace("/properties/", "/");
411 let ver = if data_dep_path.starts_with("/$params") {
412 self.params_versions.get(&data_dep_path)
413 } else {
414 data_versions.get(&data_dep_path)
415 };
416 dep_versions.insert(data_dep_path, ver);
417 }
418 }
419
420 let computed_for_item = self.active_item_index;
422
423 if eval_key.starts_with("#/$params") {
432 let existing_result: Option<&Value> = if let Some(idx) = self.active_item_index {
433 self.entries.get(eval_key).map(|e| &e.result).or_else(|| {
435 self.subform_caches
436 .get(&idx)
437 .and_then(|c| c.entries.get(eval_key))
438 .map(|e| &e.result)
439 })
440 } else {
441 self.entries.get(eval_key).map(|e| &e.result)
442 };
443
444 let value_changed = existing_result.map_or(true, |r| r != &result);
445
446 if value_changed {
447 let data_path = crate::jsoneval::path_utils::normalize_to_json_pointer(eval_key)
448 .replace("/properties/", "/");
449 let data_path = data_path.trim_start_matches('#').to_string();
450 let data_path = if data_path.starts_with('/') {
451 data_path
452 } else {
453 format!("/{}", data_path)
454 };
455
456 let mut current_path = data_path.as_str();
459 let mut slash_count = current_path.matches('/').count();
460
461 while slash_count >= 3 {
462 self.params_versions.bump(current_path);
463 if let Some(last_slash) = current_path.rfind('/') {
464 current_path = ¤t_path[..last_slash];
465 slash_count -= 1;
466 } else {
467 break;
468 }
469 }
470
471 self.eval_generation += 1;
472 }
473 }
474
475 let entry = CacheEntry {
476 dep_versions,
477 result,
478 computed_for_item,
479 };
480
481 if let Some(idx) = self.active_item_index {
482 self.subform_caches
484 .get_mut(&idx)
485 .unwrap()
486 .entries
487 .insert(eval_key.to_string(), entry.clone());
488
489 if eval_key.starts_with("#/$params") {
495 self.entries.insert(eval_key.to_string(), entry);
496 }
497 } else {
498 self.entries.insert(eval_key.to_string(), entry);
499 }
500 }
501}
502
503pub(crate) fn diff_and_update_versions(
505 tracker: &mut VersionTracker,
506 pointer: &str,
507 old: &Value,
508 new: &Value,
509) {
510 if pointer.is_empty() {
511 diff_and_update_versions_internal(tracker, "", old, new);
512 } else {
513 diff_and_update_versions_internal(tracker, pointer, old, new);
514 }
515}
516
517fn diff_and_update_versions_internal(
518 tracker: &mut VersionTracker,
519 pointer: &str,
520 old: &Value,
521 new: &Value,
522) {
523 if old == new {
524 return;
525 }
526
527 match (old, new) {
528 (Value::Object(a), Value::Object(b)) => {
529 let mut keys = HashSet::new();
530 for k in a.keys() {
531 keys.insert(k.as_str());
532 }
533 for k in b.keys() {
534 keys.insert(k.as_str());
535 }
536
537 for key in keys {
538 if key == "$params" {
542 continue;
543 }
544
545 let a_val = a.get(key).unwrap_or(&Value::Null);
546 let b_val = b.get(key).unwrap_or(&Value::Null);
547
548 let escaped_key = key.replace('~', "~0").replace('/', "~1");
549 let next_path = format!("{}/{}", pointer, escaped_key);
550 diff_and_update_versions_internal(tracker, &next_path, a_val, b_val);
551 }
552 }
553 (Value::Array(a), Value::Array(b)) => {
554 let max_len = a.len().max(b.len());
555 for i in 0..max_len {
556 let a_val = a.get(i).unwrap_or(&Value::Null);
557 let b_val = b.get(i).unwrap_or(&Value::Null);
558 let next_path = format!("{}/{}", pointer, i);
559 diff_and_update_versions_internal(tracker, &next_path, a_val, b_val);
560 }
561 }
562 (old_val, new_val) => {
563 if old_val != new_val {
564 tracker.bump(pointer);
565
566 if old_val.is_object() || old_val.is_array() {
569 traverse_and_bump(tracker, pointer, old_val);
570 }
571 if new_val.is_object() || new_val.is_array() {
572 traverse_and_bump(tracker, pointer, new_val);
573 }
574 }
575 }
576 }
577}
578
579fn traverse_and_bump(tracker: &mut VersionTracker, pointer: &str, val: &Value) {
583 match val {
584 Value::Object(map) => {
585 for (key, v) in map {
586 if key == "$params" {
587 continue; }
589 let escaped_key = key.replace('~', "~0").replace('/', "~1");
590 let next_path = format!("{}/{}", pointer, escaped_key);
591 tracker.bump(&next_path);
592 traverse_and_bump(tracker, &next_path, v);
593 }
594 }
595 Value::Array(arr) => {
596 for (i, v) in arr.iter().enumerate() {
597 let next_path = format!("{}/{}", pointer, i);
598 tracker.bump(&next_path);
599 traverse_and_bump(tracker, &next_path, v);
600 }
601 }
602 _ => {}
603 }
604}