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