ricecoder_generation/templates/
cache.rs1use crate::templates::error::TemplateError;
7use crate::templates::parser::ParsedTemplate;
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use std::time::UNIX_EPOCH;
11
12#[derive(Debug, Clone)]
14pub struct CacheStats {
15 pub total_templates: usize,
17 pub hits: u64,
19 pub misses: u64,
21 pub total_size_bytes: usize,
23}
24
25impl CacheStats {
26 pub fn hit_rate(&self) -> f64 {
28 let total = self.hits + self.misses;
29 if total == 0 {
30 0.0
31 } else {
32 (self.hits as f64 / total as f64) * 100.0
33 }
34 }
35}
36
37#[derive(Debug, Clone)]
39struct CacheEntry {
40 template: ParsedTemplate,
42 file_path: Option<PathBuf>,
44 last_modified: Option<u64>,
46 size_bytes: usize,
48}
49
50pub struct TemplateCache {
52 cache: HashMap<String, CacheEntry>,
54 stats: CacheStats,
56}
57
58impl TemplateCache {
59 pub fn new() -> Self {
61 Self {
62 cache: HashMap::new(),
63 stats: CacheStats {
64 total_templates: 0,
65 hits: 0,
66 misses: 0,
67 total_size_bytes: 0,
68 },
69 }
70 }
71
72 pub fn get(&mut self, key: &str) -> Option<ParsedTemplate> {
80 if let Some(entry) = self.cache.get(key) {
81 if let Some(file_path) = &entry.file_path {
83 if let Ok(modified_time) = self.get_file_modified_time(file_path) {
84 if Some(modified_time) != entry.last_modified {
85 self.cache.remove(key);
87 self.stats.misses += 1;
88 return None;
89 }
90 }
91 }
92
93 self.stats.hits += 1;
94 Some(entry.template.clone())
95 } else {
96 self.stats.misses += 1;
97 None
98 }
99 }
100
101 pub fn insert(&mut self, key: String, template: ParsedTemplate) {
107 self.insert_with_file(key, template, None);
108 }
109
110 pub fn insert_with_file(
117 &mut self,
118 key: String,
119 template: ParsedTemplate,
120 file_path: Option<PathBuf>,
121 ) {
122 let size_bytes = template.elements.len() * 8; let last_modified = file_path
124 .as_ref()
125 .and_then(|p| self.get_file_modified_time(p).ok());
126
127 let entry = CacheEntry {
128 template,
129 file_path,
130 last_modified,
131 size_bytes,
132 };
133
134 if !self.cache.contains_key(&key) {
135 self.stats.total_templates += 1;
136 self.stats.total_size_bytes += size_bytes;
137 }
138
139 self.cache.insert(key, entry);
140 }
141
142 pub fn remove(&mut self, key: &str) -> Option<ParsedTemplate> {
144 if let Some(entry) = self.cache.remove(key) {
145 self.stats.total_templates = self.stats.total_templates.saturating_sub(1);
146 self.stats.total_size_bytes =
147 self.stats.total_size_bytes.saturating_sub(entry.size_bytes);
148 Some(entry.template)
149 } else {
150 None
151 }
152 }
153
154 pub fn clear(&mut self) {
156 self.cache.clear();
157 self.stats.total_templates = 0;
158 self.stats.total_size_bytes = 0;
159 }
160
161 pub fn invalidate_file(&mut self, file_path: &Path) {
163 let keys_to_remove: Vec<String> = self
164 .cache
165 .iter()
166 .filter(|(_, entry)| entry.file_path.as_ref().is_some_and(|p| p == file_path))
167 .map(|(k, _)| k.clone())
168 .collect();
169
170 for key in keys_to_remove {
171 self.remove(&key);
172 }
173 }
174
175 pub fn stats(&self) -> CacheStats {
177 self.stats.clone()
178 }
179
180 pub fn contains(&self, key: &str) -> bool {
182 self.cache.contains_key(key)
183 }
184
185 pub fn len(&self) -> usize {
187 self.cache.len()
188 }
189
190 pub fn is_empty(&self) -> bool {
192 self.cache.is_empty()
193 }
194
195 fn get_file_modified_time(&self, path: &Path) -> Result<u64, TemplateError> {
197 let metadata = std::fs::metadata(path).map_err(TemplateError::IoError)?;
198
199 let modified = metadata.modified().map_err(TemplateError::IoError)?;
200
201 let duration = modified.duration_since(UNIX_EPOCH).map_err(|_| {
202 TemplateError::RenderError("Invalid file modification time".to_string())
203 })?;
204
205 Ok(duration.as_secs())
206 }
207}
208
209impl Default for TemplateCache {
210 fn default() -> Self {
211 Self::new()
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use crate::templates::parser::TemplateElement;
219
220 fn create_test_template() -> ParsedTemplate {
221 ParsedTemplate {
222 elements: vec![TemplateElement::Text("test".to_string())],
223 placeholders: vec![],
224 placeholder_names: Default::default(),
225 }
226 }
227
228 #[test]
229 fn test_cache_creation() {
230 let cache = TemplateCache::new();
231 assert!(cache.is_empty());
232 assert_eq!(cache.len(), 0);
233 }
234
235 #[test]
236 fn test_cache_insert_and_get() {
237 let mut cache = TemplateCache::new();
238 let template = create_test_template();
239
240 cache.insert("test".to_string(), template.clone());
241 assert_eq!(cache.len(), 1);
242
243 let retrieved = cache.get("test");
244 assert!(retrieved.is_some());
245 }
246
247 #[test]
248 fn test_cache_miss() {
249 let mut cache = TemplateCache::new();
250 let retrieved = cache.get("nonexistent");
251 assert!(retrieved.is_none());
252 assert_eq!(cache.stats().misses, 1);
253 }
254
255 #[test]
256 fn test_cache_hit() {
257 let mut cache = TemplateCache::new();
258 let template = create_test_template();
259
260 cache.insert("test".to_string(), template);
261 let _ = cache.get("test");
262 assert_eq!(cache.stats().hits, 1);
263 }
264
265 #[test]
266 fn test_cache_remove() {
267 let mut cache = TemplateCache::new();
268 let template = create_test_template();
269
270 cache.insert("test".to_string(), template);
271 assert_eq!(cache.len(), 1);
272
273 let removed = cache.remove("test");
274 assert!(removed.is_some());
275 assert_eq!(cache.len(), 0);
276 }
277
278 #[test]
279 fn test_cache_clear() {
280 let mut cache = TemplateCache::new();
281 let template = create_test_template();
282
283 cache.insert("test1".to_string(), template.clone());
284 cache.insert("test2".to_string(), template);
285
286 assert_eq!(cache.len(), 2);
287 cache.clear();
288 assert_eq!(cache.len(), 0);
289 }
290
291 #[test]
292 fn test_cache_contains() {
293 let mut cache = TemplateCache::new();
294 let template = create_test_template();
295
296 cache.insert("test".to_string(), template);
297 assert!(cache.contains("test"));
298 assert!(!cache.contains("nonexistent"));
299 }
300
301 #[test]
302 fn test_cache_stats_hit_rate() {
303 let mut cache = TemplateCache::new();
304 let template = create_test_template();
305
306 cache.insert("test".to_string(), template);
307
308 let _ = cache.get("test");
310 let _ = cache.get("test");
311 let _ = cache.get("nonexistent");
312
313 let stats = cache.stats();
314 assert_eq!(stats.hits, 2);
315 assert_eq!(stats.misses, 1);
316 assert!((stats.hit_rate() - 66.66).abs() < 1.0); }
318
319 #[test]
320 fn test_cache_multiple_templates() {
321 let mut cache = TemplateCache::new();
322 let template = create_test_template();
323
324 cache.insert("test1".to_string(), template.clone());
325 cache.insert("test2".to_string(), template.clone());
326 cache.insert("test3".to_string(), template);
327
328 assert_eq!(cache.len(), 3);
329 assert!(cache.contains("test1"));
330 assert!(cache.contains("test2"));
331 assert!(cache.contains("test3"));
332 }
333
334 #[test]
335 fn test_cache_stats_total_templates() {
336 let mut cache = TemplateCache::new();
337 let template = create_test_template();
338
339 cache.insert("test1".to_string(), template.clone());
340 cache.insert("test2".to_string(), template);
341
342 let stats = cache.stats();
343 assert_eq!(stats.total_templates, 2);
344 }
345
346 #[test]
347 fn test_cache_stats_zero_hit_rate() {
348 let cache = TemplateCache::new();
349 let stats = cache.stats();
350 assert_eq!(stats.hit_rate(), 0.0);
351 }
352
353 #[test]
354 fn test_cache_insert_with_file() {
355 let mut cache = TemplateCache::new();
356 let template = create_test_template();
357 let path = PathBuf::from("test.tmpl");
358
359 cache.insert_with_file("test".to_string(), template, Some(path.clone()));
360 assert!(cache.contains("test"));
361 }
362}