json_eval_rs/jsoneval/
cache.rs

1use super::JSONEval;
2use crate::jsoneval::eval_cache::{CacheKey, CacheStats};
3use crate::jsoneval::eval_data::EvalData;
4
5
6use indexmap::IndexSet;
7use serde_json::Value;
8
9
10impl JSONEval {
11    /// Check if a dependency should be part of the cache key
12    /// Check if a dependency should be cached
13    /// Caches everything except keys starting with $ (except $context)
14    #[inline]
15    pub fn should_cache_dependency(&self, key: &str) -> bool {
16        if key.starts_with("/$") || key.starts_with('$') {
17            // Only cache $context, exclude other $ keys like $params
18            key == "$context" || key.starts_with("$context.") || key.starts_with("/$context")
19        } else {
20            true
21        }
22    }
23
24    /// Try to get a result from cache
25    /// Helper: Try to get cached result for an evaluation (thread-safe)
26    /// Helper: Try to get cached result (zero-copy via Arc)
27    pub(crate) fn try_get_cached(
28        &self,
29        eval_key: &str,
30        eval_data_snapshot: &EvalData,
31    ) -> Option<Value> {
32        // Skip cache lookup if caching is disabled
33        if !self.cache_enabled {
34            return None;
35        }
36
37        // Get dependencies for this evaluation
38        let deps = self.dependencies.get(eval_key)?;
39
40        // If no dependencies, use simple cache key
41        let cache_key = if deps.is_empty() {
42            CacheKey::simple(eval_key.to_string())
43        } else {
44            // Filter dependencies (exclude $ keys except $context)
45            let filtered_deps: IndexSet<String> = deps
46                .iter()
47                .filter(|dep_key| self.should_cache_dependency(dep_key))
48                .cloned()
49                .collect();
50
51            // Collect dependency values
52            let dep_values: Vec<(String, &Value)> = filtered_deps
53                .iter()
54                .filter_map(|dep_key| eval_data_snapshot.get(dep_key).map(|v| (dep_key.clone(), v)))
55                .collect();
56
57            CacheKey::new(eval_key.to_string(), &filtered_deps, &dep_values)
58        };
59
60        // Try cache lookup (zero-copy via Arc, thread-safe)
61        self.eval_cache
62            .get(&cache_key)
63            .map(|arc_val| (*arc_val).clone())
64    }
65
66    /// Cache a result
67    /// Helper: Store evaluation result in cache (thread-safe)
68    pub(crate) fn cache_result(
69        &self,
70        eval_key: &str,
71        value: Value,
72        eval_data_snapshot: &EvalData,
73    ) {
74        // Skip cache insertion if caching is disabled
75        if !self.cache_enabled {
76            return;
77        }
78
79        // Get dependencies for this evaluation
80        let deps = match self.dependencies.get(eval_key) {
81            Some(d) => d,
82            None => {
83                // No dependencies - use simple cache key
84                let cache_key = CacheKey::simple(eval_key.to_string());
85                self.eval_cache.insert(cache_key, value);
86                return;
87            }
88        };
89
90        // Filter and collect dependency values (exclude $ keys except $context)
91        let filtered_deps: IndexSet<String> = deps
92            .iter()
93            .filter(|dep_key| self.should_cache_dependency(dep_key))
94            .cloned()
95            .collect();
96
97        let dep_values: Vec<(String, &Value)> = filtered_deps
98            .iter()
99            .filter_map(|dep_key| eval_data_snapshot.get(dep_key).map(|v| (dep_key.clone(), v)))
100            .collect();
101
102        let cache_key = CacheKey::new(eval_key.to_string(), &filtered_deps, &dep_values);
103        self.eval_cache.insert(cache_key, value);
104    }
105
106    /// Purge cache entries affected by changed data paths, comparing old and new values
107    /// Selectively purge cache entries that depend on changed data paths
108    /// Only removes cache entries whose dependencies intersect with changed_paths
109    /// Compares old vs new values and only purges if values actually changed
110    pub fn purge_cache_for_changed_data_with_comparison(
111        &self,
112        changed_data_paths: &[String],
113        old_data: &Value,
114        new_data: &Value,
115    ) {
116        if changed_data_paths.is_empty() {
117            return;
118        }
119
120        // Check which paths actually have different values
121        let mut actually_changed_paths = Vec::new();
122        for path in changed_data_paths {
123            let old_val = old_data.pointer(path);
124            let new_val = new_data.pointer(path);
125
126            // Only add to changed list if values differ
127            if old_val != new_val {
128                actually_changed_paths.push(path.clone());
129            }
130        }
131
132        // If no values actually changed, no need to purge
133        if actually_changed_paths.is_empty() {
134            return;
135        }
136
137        // Find all eval_keys that depend on the actually changed data paths
138        let mut affected_eval_keys = IndexSet::new();
139
140        for (eval_key, deps) in self.dependencies.iter() {
141            // Check if this evaluation depends on any of the changed paths
142            let is_affected = deps.iter().any(|dep| {
143                // Check if the dependency matches any changed path
144                actually_changed_paths.iter().any(|changed_path| {
145                    // Exact match or prefix match (for nested fields)
146                    dep == changed_path
147                        || dep.starts_with(&format!("{}/", changed_path))
148                        || changed_path.starts_with(&format!("{}/", dep))
149                })
150            });
151
152            if is_affected {
153                affected_eval_keys.insert(eval_key.clone());
154            }
155        }
156
157        // Remove all cache entries for affected eval_keys using retain
158        // Keep entries whose eval_key is NOT in the affected set
159        self.eval_cache
160            .retain(|cache_key, _| !affected_eval_keys.contains(&cache_key.eval_key));
161    }
162
163    /// Selectively purge cache entries that depend on changed data paths
164    /// Finds all eval_keys that depend on the changed paths and removes them
165    /// Selectively purge cache entries that depend on changed data paths
166    /// Simpler version without value comparison for cases where we don't have old data
167    pub fn purge_cache_for_changed_data(&self, changed_data_paths: &[String]) {
168        if changed_data_paths.is_empty() {
169            return;
170        }
171
172        // Find all eval_keys that depend on the changed paths
173        let mut affected_eval_keys = IndexSet::new();
174
175        for (eval_key, deps) in self.dependencies.iter() {
176            // Check if this evaluation depends on any of the changed paths
177            let is_affected = deps.iter().any(|dep| {
178                // Check if dependency path matches any changed data path using flexible matching
179                changed_data_paths.iter().any(|changed_for_purge| {
180                    // Check both directions:
181                    // 1. Dependency matches changed data (dependency is child of change)
182                    // 2. Changed data matches dependency (change is child of dependency)
183                    Self::paths_match_flexible(dep, changed_for_purge)
184                        || Self::paths_match_flexible(changed_for_purge, dep)
185                })
186            });
187
188            if is_affected {
189                affected_eval_keys.insert(eval_key.clone());
190            }
191        }
192
193        // Remove all cache entries for affected eval_keys using retain
194        // Keep entries whose eval_key is NOT in the affected set
195        self.eval_cache
196            .retain(|cache_key, _| !affected_eval_keys.contains(&cache_key.eval_key));
197    }
198
199    /// Flexible path matching that handles structural schema keywords (e.g. properties, oneOf)
200    /// Returns true if schema_path structurally matches data_path
201    fn paths_match_flexible(schema_path: &str, data_path: &str) -> bool {
202        let s_segs: Vec<&str> = schema_path
203            .trim_start_matches('#')
204            .trim_start_matches('/')
205            .split('/')
206            .filter(|s| !s.is_empty())
207            .collect();
208        let d_segs: Vec<&str> = data_path
209            .trim_start_matches('/')
210            .split('/')
211            .filter(|s| !s.is_empty())
212            .collect();
213
214        let mut d_idx = 0;
215
216        for s_seg in s_segs {
217            // If we matched all data segments, we are good (schema is deeper/parent)
218            if d_idx >= d_segs.len() {
219                return true;
220            }
221
222            let d_seg = d_segs[d_idx];
223
224            if s_seg == d_seg {
225                // Exact match, advance data pointer
226                d_idx += 1;
227            } else if s_seg == "items"
228                || s_seg == "additionalProperties"
229                || s_seg == "patternProperties"
230            {
231                // Wildcard match for arrays/maps - consume data segment if it looks valid
232                // Note: items matches array index (numeric). additionalProperties matches any key.
233                if s_seg == "items" {
234                    // Only match if data segment is numeric (array index)
235                    if d_seg.chars().all(|c| c.is_ascii_digit()) {
236                        d_idx += 1;
237                    }
238                } else {
239                    // additionalProperties/patternProperties matches any string key
240                    d_idx += 1;
241                }
242            } else if Self::is_structural_keyword(s_seg)
243                || s_seg.chars().all(|c| c.is_ascii_digit())
244            {
245                // Skip structural keywords (properties, oneOf, etc) and numeric indices in schema (e.g. oneOf/0)
246                continue;
247            } else {
248                // Mismatch: schema has a named segment that data doesn't have
249                return false;
250            }
251        }
252
253        // Return true if we consumed all data segments
254        true
255    }
256    
257    /// Purge cache entries affected by context changes
258    /// Purge cache entries that depend on context
259    pub fn purge_cache_for_context_change(&self) {
260        // Find all eval_keys that depend on $context
261        let mut affected_eval_keys = IndexSet::new();
262
263        for (eval_key, deps) in self.dependencies.iter() {
264            let is_affected = deps.iter().any(|dep| {
265                dep == "$context" || dep.starts_with("$context.") || dep.starts_with("/$context")
266            });
267
268            if is_affected {
269                affected_eval_keys.insert(eval_key.clone());
270            }
271        }
272
273        self.eval_cache
274            .retain(|cache_key, _| !affected_eval_keys.contains(&cache_key.eval_key));
275    }
276
277    /// Get cache statistics
278    pub fn cache_stats(&self) -> CacheStats {
279        self.eval_cache.stats()
280    }
281
282    /// Clear the cache manually
283    pub fn clear_cache(&self) {
284        self.eval_cache.clear();
285    }
286
287    /// Enable caching
288    pub fn enable_cache(&mut self) {
289        self.cache_enabled = true;
290        for subform in self.subforms.values_mut() {
291            subform.enable_cache();
292        }
293    }
294
295    /// Disable caching
296    pub fn disable_cache(&mut self) {
297        self.cache_enabled = false;
298        self.eval_cache.clear();
299        for subform in self.subforms.values_mut() {
300            subform.disable_cache();
301        }
302    }
303    
304    /// Check if cache is enabled
305    pub fn is_cache_enabled(&self) -> bool {
306        self.cache_enabled
307    }
308
309    /// Helper to check if a key is a structural JSON Schema keyword
310    /// Helper to check if a key is a structural JSON Schema keyword
311    fn is_structural_keyword(key: &str) -> bool {
312        matches!(
313            key,
314            "properties"
315                | "definitions"
316                | "$defs"
317                | "allOf"
318                | "anyOf"
319                | "oneOf"
320                | "not"
321                | "if"
322                | "then"
323                | "else"
324                | "dependentSchemas"
325                | "$params"
326                | "dependencies"
327        )
328    }
329    
330    /// Get cache size
331    pub fn cache_len(&self) -> usize {
332        self.eval_cache.len()
333    }
334}