json_eval_rs/parsed_schema_cache.rs
1/// Built-in cache store for Arc<ParsedSchema> instances
2///
3/// Provides thread-safe caching of parsed schemas with caller-controlled lifecycle:
4/// - Caller provides unique keys
5/// - Caller decides when to re-parse
6/// - Caller controls cache clearing
7/// - Caller manages memory release
8
9use std::sync::{Arc, RwLock};
10use indexmap::IndexMap;
11use crate::ParsedSchema;
12
13/// Thread-safe cache for storing and reusing ParsedSchema instances
14///
15/// # Example
16/// ```
17/// use json_eval_rs::{ParsedSchemaCache, ParsedSchema};
18/// use std::sync::Arc;
19///
20/// let cache = ParsedSchemaCache::new();
21///
22/// // Parse and cache a schema
23/// let schema_json = r#"{"type": "object"}"#;
24/// let schema = ParsedSchema::parse(schema_json).unwrap();
25/// cache.insert("my-schema".to_string(), Arc::new(schema));
26///
27/// // Retrieve from cache
28/// if let Some(cached) = cache.get("my-schema") {
29/// // Use cached schema for evaluation
30/// }
31///
32/// // Remove specific entry
33/// cache.remove("my-schema");
34///
35/// // Clear all entries
36/// cache.clear();
37/// ```
38#[derive(Clone)]
39pub struct ParsedSchemaCache {
40 cache: Arc<RwLock<IndexMap<String, Arc<ParsedSchema>>>>,
41}
42
43impl ParsedSchemaCache {
44 /// Create a new empty cache
45 pub fn new() -> Self {
46 Self {
47 cache: Arc::new(RwLock::new(IndexMap::new())),
48 }
49 }
50
51 /// Insert or update a parsed schema with the given key
52 ///
53 /// Returns the previous value if the key already existed
54 pub fn insert(&self, key: String, schema: Arc<ParsedSchema>) -> Option<Arc<ParsedSchema>> {
55 let mut cache = self.cache.write().unwrap();
56 cache.insert(key, schema)
57 }
58
59 /// Get a cloned Arc reference to the cached schema
60 ///
61 /// Returns None if the key doesn't exist
62 pub fn get(&self, key: &str) -> Option<Arc<ParsedSchema>> {
63 let cache = self.cache.read().unwrap();
64 cache.get(key).cloned()
65 }
66
67 /// Remove and return the schema for the given key
68 ///
69 /// Returns None if the key doesn't exist
70 pub fn remove(&self, key: &str) -> Option<Arc<ParsedSchema>> {
71 let mut cache = self.cache.write().unwrap();
72 cache.shift_remove(key)
73 }
74
75 /// Clear all cached schemas
76 pub fn clear(&self) {
77 let mut cache = self.cache.write().unwrap();
78 cache.clear();
79 }
80
81 /// Check if a key exists in the cache
82 pub fn contains_key(&self, key: &str) -> bool {
83 let cache = self.cache.read().unwrap();
84 cache.contains_key(key)
85 }
86
87 /// Get the number of cached schemas
88 pub fn len(&self) -> usize {
89 let cache = self.cache.read().unwrap();
90 cache.len()
91 }
92
93 /// Check if the cache is empty
94 pub fn is_empty(&self) -> bool {
95 self.len() == 0
96 }
97
98 /// Get all keys currently in the cache
99 pub fn keys(&self) -> Vec<String> {
100 let cache = self.cache.read().unwrap();
101 cache.keys().cloned().collect()
102 }
103
104 /// Get cache statistics
105 pub fn stats(&self) -> ParsedSchemaCacheStats {
106 let cache = self.cache.read().unwrap();
107 ParsedSchemaCacheStats {
108 entry_count: cache.len(),
109 keys: cache.keys().cloned().collect(),
110 }
111 }
112
113 /// Get or insert a schema using a factory function
114 ///
115 /// If the key exists, returns the cached value.
116 /// Otherwise, calls the factory function to create a new value,
117 /// inserts it, and returns it.
118 ///
119 /// # Example
120 /// ```
121 /// use json_eval_rs::{ParsedSchemaCache, ParsedSchema};
122 /// use std::sync::Arc;
123 ///
124 /// let cache = ParsedSchemaCache::new();
125 /// let json = r#"{"type": "object"}"#;
126 /// let schema = cache.get_or_insert_with("my-schema", || {
127 /// Arc::new(ParsedSchema::parse(json).unwrap())
128 /// });
129 /// ```
130 pub fn get_or_insert_with<F>(&self, key: &str, factory: F) -> Arc<ParsedSchema>
131 where
132 F: FnOnce() -> Arc<ParsedSchema>,
133 {
134 // Try read first (fast path)
135 {
136 let cache = self.cache.read().unwrap();
137 if let Some(schema) = cache.get(key) {
138 return schema.clone();
139 }
140 }
141
142 // Need to insert (slow path)
143 let mut cache = self.cache.write().unwrap();
144 // Double-check in case another thread inserted while we waited for write lock
145 if let Some(schema) = cache.get(key) {
146 return schema.clone();
147 }
148
149 let schema = factory();
150 cache.insert(key.to_string(), schema.clone());
151 schema
152 }
153
154 /// Batch insert multiple schemas at once
155 pub fn insert_batch(&self, entries: Vec<(String, Arc<ParsedSchema>)>) {
156 let mut cache = self.cache.write().unwrap();
157 for (key, schema) in entries {
158 cache.insert(key, schema);
159 }
160 }
161
162 /// Remove multiple keys at once
163 pub fn remove_batch(&self, keys: &[String]) -> Vec<(String, Arc<ParsedSchema>)> {
164 let mut cache = self.cache.write().unwrap();
165 let mut removed = Vec::new();
166 for key in keys {
167 if let Some(schema) = cache.shift_remove(key) {
168 removed.push((key.clone(), schema));
169 }
170 }
171 removed
172 }
173}
174
175impl Default for ParsedSchemaCache {
176 fn default() -> Self {
177 Self::new()
178 }
179}
180
181/// Statistics about the cache state
182#[derive(Debug, Clone)]
183pub struct ParsedSchemaCacheStats {
184 /// Number of entries in the cache
185 pub entry_count: usize,
186 /// List of all keys
187 pub keys: Vec<String>,
188}
189
190impl std::fmt::Display for ParsedSchemaCacheStats {
191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 write!(f, "ParsedSchemaCache: {} entries", self.entry_count)?;
193 if !self.keys.is_empty() {
194 write!(f, " (keys: {})", self.keys.join(", "))?;
195 }
196 Ok(())
197 }
198}
199
200// Optional: Global cache instance for convenience
201use once_cell::sync::Lazy;
202
203/// Global ParsedSchema cache instance
204///
205/// Convenient for applications that want a single global cache
206/// without managing their own instance.
207///
208/// # Example
209/// ```
210/// use json_eval_rs::{PARSED_SCHEMA_CACHE, ParsedSchema};
211/// use std::sync::Arc;
212///
213/// let schema = Arc::new(ParsedSchema::parse(r#"{"type": "object"}"#).unwrap());
214/// PARSED_SCHEMA_CACHE.insert("global-schema".to_string(), schema);
215/// let cached = PARSED_SCHEMA_CACHE.get("global-schema");
216/// ```
217pub static PARSED_SCHEMA_CACHE: Lazy<ParsedSchemaCache> = Lazy::new(ParsedSchemaCache::new);
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn test_cache_basic_operations() {
225 let cache = ParsedSchemaCache::new();
226 assert_eq!(cache.len(), 0);
227 assert!(cache.is_empty());
228
229 // Insert doesn't require a real ParsedSchema for this test
230 // In real usage, you'd use ParsedSchema::from_json
231
232 assert!(!cache.contains_key("test"));
233 assert_eq!(cache.keys().len(), 0);
234 }
235
236 #[test]
237 fn test_cache_clone() {
238 let cache1 = ParsedSchemaCache::new();
239 let cache2 = cache1.clone();
240
241 // Both should share the same underlying cache
242 assert_eq!(cache1.len(), cache2.len());
243 }
244}