use crate::context::TemplateContext;
use crate::error::Result;
use crate::renderer::TemplateRenderer;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use std::time::{Duration, SystemTime};
#[derive(Debug)]
pub struct TemplateCache {
templates: Arc<RwLock<HashMap<String, CachedTemplate>>>,
file_mtimes: Arc<RwLock<HashMap<PathBuf, SystemTime>>>,
stats: Arc<RwLock<CacheStats>>,
hot_reload: bool,
ttl: Duration,
}
#[derive(Debug, Clone)]
struct CachedTemplate {
content: String,
#[allow(dead_code)]
modified: SystemTime,
compiled_at: SystemTime,
size: usize,
}
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub total_size: usize,
pub template_count: usize,
}
impl TemplateCache {
pub fn new(hot_reload: bool, ttl: Duration) -> Self {
Self {
templates: Arc::new(RwLock::new(HashMap::new())),
file_mtimes: Arc::new(RwLock::new(HashMap::new())),
stats: Arc::new(RwLock::new(CacheStats::default())),
hot_reload,
ttl,
}
}
pub fn with_defaults() -> Self {
Self::new(true, Duration::from_secs(3600))
}
pub fn get_or_compile(
&self,
template_name: &str,
template_content: &str,
file_path: Option<&Path>,
) -> Result<String> {
if let Some(cached) = self.templates.read().unwrap().get(template_name) {
if self.is_cache_valid(cached, file_path)? {
self.record_hit();
return Ok(cached.content.clone());
}
}
self.record_miss();
let compiled = self.compile_template(template_content)?;
self.cache_template(template_name, template_content, &compiled)?;
if let Some(path) = file_path {
if let Ok(metadata) = std::fs::metadata(path) {
if let Ok(mtime) = metadata.modified() {
self.file_mtimes
.write()
.unwrap()
.insert(path.to_path_buf(), mtime);
}
}
}
Ok(compiled)
}
fn is_cache_valid(&self, cached: &CachedTemplate, file_path: Option<&Path>) -> Result<bool> {
let age = SystemTime::now()
.duration_since(cached.compiled_at)
.unwrap_or(Duration::from_secs(0));
if age > self.ttl {
return Ok(false);
}
if self.hot_reload {
if let Some(path) = file_path {
if let Ok(metadata) = std::fs::metadata(path) {
if let Ok(mtime) = metadata.modified() {
if let Some(cached_mtime) = self.file_mtimes.read().unwrap().get(path) {
if mtime > *cached_mtime {
return Ok(false); }
}
}
}
}
}
Ok(true)
}
fn compile_template(&self, content: &str) -> Result<String> {
Ok(content.to_string())
}
fn cache_template(&self, name: &str, _content: &str, compiled: &str) -> Result<()> {
let now = SystemTime::now();
let cached = CachedTemplate {
content: compiled.to_string(),
modified: now,
compiled_at: now,
size: compiled.len(),
};
self.templates
.write()
.unwrap()
.insert(name.to_string(), cached);
let mut stats = self.stats.write().unwrap();
stats.total_size += compiled.len();
stats.template_count += 1;
Ok(())
}
fn record_hit(&self) {
self.stats.write().unwrap().hits += 1;
}
fn record_miss(&self) {
self.stats.write().unwrap().misses += 1;
}
pub fn stats(&self) -> CacheStats {
self.stats.read().unwrap().clone()
}
pub fn clear(&self) {
self.templates.write().unwrap().clear();
self.file_mtimes.write().unwrap().clear();
let mut stats = self.stats.write().unwrap();
stats.total_size = 0;
stats.template_count = 0;
stats.evictions = 0;
}
pub fn evict_expired(&self) -> usize {
let now = SystemTime::now();
let mut templates = self.templates.write().unwrap();
let mut file_mtimes = self.file_mtimes.write().unwrap();
let mut stats = self.stats.write().unwrap();
let initial_count = templates.len();
templates.retain(|_name, cached| {
let age = now
.duration_since(cached.compiled_at)
.unwrap_or(Duration::from_secs(0));
if age > self.ttl {
stats.total_size -= cached.size;
stats.evictions += 1;
false
} else {
true
}
});
file_mtimes.retain(|path, _| {
path.exists()
});
stats.template_count = templates.len();
initial_count - templates.len()
}
}
pub struct CachedRenderer {
renderer: TemplateRenderer,
cache: TemplateCache,
}
impl CachedRenderer {
pub fn new(context: TemplateContext, hot_reload: bool) -> Result<Self> {
let renderer = TemplateRenderer::new()?.with_context(context);
let cache = TemplateCache::new(hot_reload, Duration::from_secs(3600));
Ok(Self { renderer, cache })
}
pub fn render_cached(
&mut self,
template: &str,
name: &str,
file_path: Option<&Path>,
) -> Result<String> {
if let Ok(cached) = self.cache.get_or_compile(name, template, file_path) {
return Ok(cached);
}
self.renderer.render_str(template, name)
}
pub fn cache_stats(&self) -> CacheStats {
self.cache.stats()
}
pub fn clear_cache(&self) {
self.cache.clear();
}
pub fn evict_expired(&self) -> usize {
self.cache.evict_expired()
}
pub fn renderer(&self) -> &TemplateRenderer {
&self.renderer
}
pub fn renderer_mut(&mut self) -> &mut TemplateRenderer {
&mut self.renderer
}
}
pub struct HotReloadWatcher {
watched_dirs: Vec<PathBuf>,
#[allow(dead_code)]
cache: Arc<TemplateCache>,
_watcher: Option<Box<dyn Watcher>>,
}
impl HotReloadWatcher {
pub fn new(cache: Arc<TemplateCache>) -> Self {
Self {
watched_dirs: Vec::new(),
cache,
_watcher: None,
}
}
pub fn watch_directory<P: AsRef<Path>>(mut self, path: P) -> Self {
self.watched_dirs.push(path.as_ref().to_path_buf());
self
}
pub fn start(self) -> Result<()> {
Ok(())
}
pub fn stop(&self) -> Result<()> {
Ok(())
}
}
trait Watcher {}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_template_cache_basic() {
let cache = TemplateCache::default();
let template = "Hello {{ name }}";
let result = cache.get_or_compile("test", template, None).unwrap();
assert_eq!(result, template);
let stats = cache.stats();
assert_eq!(stats.misses, 1);
assert_eq!(stats.hits, 0);
let result = cache.get_or_compile("test", template, None).unwrap();
assert_eq!(result, template);
let stats = cache.stats();
assert_eq!(stats.misses, 1);
assert_eq!(stats.hits, 1);
}
#[test]
fn test_cached_renderer() {
let context = TemplateContext::with_defaults();
let mut renderer = CachedRenderer::new(context, false).unwrap();
let template = "service = \"{{ svc }}\"";
let result = renderer.render_cached(template, "test", None).unwrap();
assert_eq!(result, template);
let stats = renderer.cache_stats();
assert_eq!(stats.misses, 1);
}
#[test]
fn test_cache_eviction() {
let cache = TemplateCache::new(false, Duration::from_millis(1));
cache.get_or_compile("test", "Hello", None).unwrap();
std::thread::sleep(Duration::from_millis(10));
let evicted = cache.evict_expired();
assert_eq!(evicted, 1);
}
}