1use crate::context::TemplateContext;
7use crate::error::Result;
8use crate::renderer::TemplateRenderer;
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use std::sync::{Arc, RwLock};
12use std::time::{Duration, SystemTime};
13
14#[derive(Debug)]
19pub struct TemplateCache {
20 templates: Arc<RwLock<HashMap<String, CachedTemplate>>>,
22 file_mtimes: Arc<RwLock<HashMap<PathBuf, SystemTime>>>,
24 stats: Arc<RwLock<CacheStats>>,
26 hot_reload: bool,
28 ttl: Duration,
30}
31
32#[derive(Debug, Clone)]
34struct CachedTemplate {
35 content: String,
37 #[allow(dead_code)]
39 modified: SystemTime,
40 compiled_at: SystemTime,
42 size: usize,
44}
45
46#[derive(Debug, Clone, Default)]
48pub struct CacheStats {
49 pub hits: u64,
51 pub misses: u64,
53 pub evictions: u64,
55 pub total_size: usize,
57 pub template_count: usize,
59}
60
61impl TemplateCache {
62 pub fn new(hot_reload: bool, ttl: Duration) -> Self {
68 Self {
69 templates: Arc::new(RwLock::new(HashMap::new())),
70 file_mtimes: Arc::new(RwLock::new(HashMap::new())),
71 stats: Arc::new(RwLock::new(CacheStats::default())),
72 hot_reload,
73 ttl,
74 }
75 }
76
77 pub fn with_defaults() -> Self {
79 Self::new(true, Duration::from_secs(3600))
80 }
81
82 pub fn get_or_compile(
89 &self,
90 template_name: &str,
91 template_content: &str,
92 file_path: Option<&Path>,
93 ) -> Result<String> {
94 if let Some(cached) = self.templates.read().unwrap().get(template_name) {
96 if self.is_cache_valid(cached, file_path)? {
97 self.record_hit();
99 return Ok(cached.content.clone());
100 }
101 }
102
103 self.record_miss();
105
106 let compiled = self.compile_template(template_content)?;
107
108 self.cache_template(template_name, template_content, &compiled)?;
110
111 if let Some(path) = file_path {
113 if let Ok(metadata) = std::fs::metadata(path) {
114 if let Ok(mtime) = metadata.modified() {
115 self.file_mtimes
116 .write()
117 .unwrap()
118 .insert(path.to_path_buf(), mtime);
119 }
120 }
121 }
122
123 Ok(compiled)
124 }
125
126 fn is_cache_valid(&self, cached: &CachedTemplate, file_path: Option<&Path>) -> Result<bool> {
128 let age = SystemTime::now()
130 .duration_since(cached.compiled_at)
131 .unwrap_or(Duration::from_secs(0));
132
133 if age > self.ttl {
134 return Ok(false);
135 }
136
137 if self.hot_reload {
139 if let Some(path) = file_path {
140 if let Ok(metadata) = std::fs::metadata(path) {
141 if let Ok(mtime) = metadata.modified() {
142 if let Some(cached_mtime) = self.file_mtimes.read().unwrap().get(path) {
143 if mtime > *cached_mtime {
144 return Ok(false); }
146 }
147 }
148 }
149 }
150 }
151
152 Ok(true)
153 }
154
155 fn compile_template(&self, content: &str) -> Result<String> {
157 Ok(content.to_string())
160 }
161
162 fn cache_template(&self, name: &str, _content: &str, compiled: &str) -> Result<()> {
164 let now = SystemTime::now();
165 let cached = CachedTemplate {
166 content: compiled.to_string(),
167 modified: now,
168 compiled_at: now,
169 size: compiled.len(),
170 };
171
172 self.templates
174 .write()
175 .unwrap()
176 .insert(name.to_string(), cached);
177
178 let mut stats = self.stats.write().unwrap();
180 stats.total_size += compiled.len();
181 stats.template_count += 1;
182
183 Ok(())
184 }
185
186 fn record_hit(&self) {
188 self.stats.write().unwrap().hits += 1;
189 }
190
191 fn record_miss(&self) {
193 self.stats.write().unwrap().misses += 1;
194 }
195
196 pub fn stats(&self) -> CacheStats {
198 self.stats.read().unwrap().clone()
199 }
200
201 pub fn clear(&self) {
203 self.templates.write().unwrap().clear();
204 self.file_mtimes.write().unwrap().clear();
205
206 let mut stats = self.stats.write().unwrap();
207 stats.total_size = 0;
208 stats.template_count = 0;
209 stats.evictions = 0;
210 }
211
212 pub fn evict_expired(&self) -> usize {
214 let now = SystemTime::now();
215 let mut templates = self.templates.write().unwrap();
216 let mut file_mtimes = self.file_mtimes.write().unwrap();
217 let mut stats = self.stats.write().unwrap();
218
219 let initial_count = templates.len();
220 templates.retain(|_name, cached| {
221 let age = now
222 .duration_since(cached.compiled_at)
223 .unwrap_or(Duration::from_secs(0));
224 if age > self.ttl {
225 stats.total_size -= cached.size;
227 stats.evictions += 1;
228 false
229 } else {
230 true
231 }
232 });
233
234 file_mtimes.retain(|path, _| {
236 path.exists()
238 });
239
240 stats.template_count = templates.len();
241 initial_count - templates.len()
242 }
243}
244
245pub struct CachedRenderer {
250 renderer: TemplateRenderer,
252 cache: TemplateCache,
254}
255
256impl CachedRenderer {
257 pub fn new(context: TemplateContext, hot_reload: bool) -> Result<Self> {
263 let renderer = TemplateRenderer::new()?.with_context(context);
264 let cache = TemplateCache::new(hot_reload, Duration::from_secs(3600));
265
266 Ok(Self { renderer, cache })
267 }
268
269 pub fn render_cached(
276 &mut self,
277 template: &str,
278 name: &str,
279 file_path: Option<&Path>,
280 ) -> Result<String> {
281 if let Ok(cached) = self.cache.get_or_compile(name, template, file_path) {
283 return Ok(cached);
284 }
285
286 self.renderer.render_str(template, name)
288 }
289
290 pub fn cache_stats(&self) -> CacheStats {
292 self.cache.stats()
293 }
294
295 pub fn clear_cache(&self) {
297 self.cache.clear();
298 }
299
300 pub fn evict_expired(&self) -> usize {
302 self.cache.evict_expired()
303 }
304
305 pub fn renderer(&self) -> &TemplateRenderer {
307 &self.renderer
308 }
309
310 pub fn renderer_mut(&mut self) -> &mut TemplateRenderer {
312 &mut self.renderer
313 }
314}
315
316pub struct HotReloadWatcher {
321 watched_dirs: Vec<PathBuf>,
323 #[allow(dead_code)]
325 cache: Arc<TemplateCache>,
326 _watcher: Option<Box<dyn Watcher>>,
328}
329
330impl HotReloadWatcher {
331 pub fn new(cache: Arc<TemplateCache>) -> Self {
336 Self {
337 watched_dirs: Vec::new(),
338 cache,
339 _watcher: None,
340 }
341 }
342
343 pub fn watch_directory<P: AsRef<Path>>(mut self, path: P) -> Self {
348 self.watched_dirs.push(path.as_ref().to_path_buf());
349 self
350 }
351
352 pub fn start(self) -> Result<()> {
357 Ok(())
360 }
361
362 pub fn stop(&self) -> Result<()> {
364 Ok(())
365 }
366}
367
368trait Watcher {}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 use tempfile::tempdir;
375
376 #[test]
377 fn test_template_cache_basic() {
378 let cache = TemplateCache::default();
379 let template = "Hello {{ name }}";
380
381 let result = cache.get_or_compile("test", template, None).unwrap();
383 assert_eq!(result, template);
384
385 let stats = cache.stats();
386 assert_eq!(stats.misses, 1);
387 assert_eq!(stats.hits, 0);
388
389 let result = cache.get_or_compile("test", template, None).unwrap();
391 assert_eq!(result, template);
392
393 let stats = cache.stats();
394 assert_eq!(stats.misses, 1);
395 assert_eq!(stats.hits, 1);
396 }
397
398 #[test]
399 fn test_cached_renderer() {
400 let context = TemplateContext::with_defaults();
401 let mut renderer = CachedRenderer::new(context, false).unwrap();
402
403 let template = "service = \"{{ svc }}\"";
404 let result = renderer.render_cached(template, "test", None).unwrap();
405 assert_eq!(result, template);
406
407 let stats = renderer.cache_stats();
408 assert_eq!(stats.misses, 1);
409 }
410
411 #[test]
412 fn test_cache_eviction() {
413 let cache = TemplateCache::new(false, Duration::from_millis(1));
414
415 cache.get_or_compile("test", "Hello", None).unwrap();
417
418 std::thread::sleep(Duration::from_millis(10));
420
421 let evicted = cache.evict_expired();
423 assert_eq!(evicted, 1);
424 }
425}