use crate::utils::error::{Error, Result};
use lru::LruCache;
use serde_yaml::Value as YamlValue;
use std::collections::HashMap;
use std::num::NonZeroUsize;
use std::path::Path;
use std::sync::{Arc, Mutex};
use crate::template_types::Template;
pub struct TemplateCache {
cache: Arc<Mutex<LruCache<String, Arc<Template>>>>,
hits: Arc<Mutex<u64>>,
misses: Arc<Mutex<u64>>,
frontmatter_cache: Arc<Mutex<HashMap<String, Arc<YamlValue>>>>,
tera_cache: Arc<Mutex<HashMap<String, String>>>,
}
impl TemplateCache {
pub fn new(capacity: usize) -> Self {
let cap = NonZeroUsize::new(capacity).unwrap_or(NonZeroUsize::new(5000).unwrap());
Self {
cache: Arc::new(Mutex::new(LruCache::new(cap))),
hits: Arc::new(Mutex::new(0)),
misses: Arc::new(Mutex::new(0)),
frontmatter_cache: Arc::new(Mutex::new(HashMap::new())),
tera_cache: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn get_or_parse(&self, path: &Path) -> Result<Arc<Template>> {
let path_str = path.to_string_lossy().to_string();
let mut cache = self
.cache
.lock()
.map_err(|_| Error::new("Cache lock poisoned"))?;
if let Some(template) = cache.get(&path_str) {
if let Ok(mut hits) = self.hits.lock() {
*hits += 1;
}
return Ok(Arc::clone(template));
}
if let Ok(mut misses) = self.misses.lock() {
*misses += 1;
}
let content = std::fs::read_to_string(path).map_err(|e| {
Error::with_source(
&format!("Failed to read template {}", path.display()),
Box::new(e),
)
})?;
let template = Template::parse(&content)?;
let arc_template = Arc::new(template);
cache.put(path_str, Arc::clone(&arc_template));
Ok(arc_template)
}
pub fn get_or_parse_frontmatter(&self, content: &str, key: &str) -> Result<Arc<YamlValue>> {
{
let cache = self
.frontmatter_cache
.lock()
.map_err(|_| Error::new("Frontmatter cache lock poisoned"))?;
if let Some(fm) = cache.get(key) {
return Ok(Arc::clone(fm));
}
}
let matter = gray_matter::Matter::<gray_matter::engine::YAML>::new();
let parsed = matter
.parse(content)
.map_err(|e| Error::with_context("Failed to parse frontmatter", &e.to_string()))?;
let yaml_value = if let Some(data) = parsed.data {
data
} else {
YamlValue::Null
};
let arc_value = Arc::new(yaml_value);
{
let mut cache = self
.frontmatter_cache
.lock()
.map_err(|_| Error::new("Frontmatter cache lock poisoned"))?;
cache.insert(key.to_string(), Arc::clone(&arc_value));
}
Ok(arc_value)
}
pub fn get_tera_cached(&self, key: &str) -> Option<String> {
let cache = self.tera_cache.lock().ok()?;
cache.get(key).cloned()
}
pub fn cache_tera_template(&self, key: &str, content: String) {
if let Ok(mut cache) = self.tera_cache.lock() {
cache.insert(key.to_string(), content);
}
}
pub fn clear(&self) -> Result<()> {
let mut cache = self
.cache
.lock()
.map_err(|_| Error::new("Cache lock poisoned"))?;
cache.clear();
if let Ok(mut fm_cache) = self.frontmatter_cache.lock() {
fm_cache.clear();
}
if let Ok(mut tera_cache) = self.tera_cache.lock() {
tera_cache.clear();
}
if let Ok(mut hits) = self.hits.lock() {
*hits = 0;
}
if let Ok(mut misses) = self.misses.lock() {
*misses = 0;
}
Ok(())
}
pub fn stats(&self) -> Result<CacheStats> {
let cache = self
.cache
.lock()
.map_err(|_| Error::new("Cache lock poisoned"))?;
let hits = self.hits.lock().map(|h| *h).unwrap_or(0);
let misses = self.misses.lock().map(|m| *m).unwrap_or(0);
let frontmatter_size = self.frontmatter_cache.lock().map(|c| c.len()).unwrap_or(0);
let tera_size = self.tera_cache.lock().map(|c| c.len()).unwrap_or(0);
Ok(CacheStats {
size: cache.len(),
capacity: cache.cap().get(),
hits,
misses,
frontmatter_cache_size: frontmatter_size,
tera_cache_size: tera_size,
})
}
pub fn warm(&self, paths: &[&Path]) -> Result<usize> {
let mut loaded = 0;
for path in paths {
if self.get_or_parse(path).is_ok() {
loaded += 1;
}
}
Ok(loaded)
}
}
impl Default for TemplateCache {
fn default() -> Self {
Self::new(5000)
}
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub size: usize,
pub capacity: usize,
pub hits: u64,
pub misses: u64,
pub frontmatter_cache_size: usize,
pub tera_cache_size: usize,
}
impl CacheStats {
pub fn hit_rate(&self) -> f64 {
let total = self.total_accesses();
if total == 0 {
0.0
} else {
(self.hits as f64 / total as f64) * 100.0
}
}
pub fn total_accesses(&self) -> u64 {
self.hits + self.misses
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_template_cache_new() {
let cache = TemplateCache::new(50);
let stats = cache.stats().unwrap();
assert_eq!(stats.capacity, 50);
assert_eq!(stats.size, 0);
}
#[test]
fn test_template_cache_default() {
let cache = TemplateCache::default();
let stats = cache.stats().unwrap();
assert_eq!(stats.capacity, 5000);
assert_eq!(stats.size, 0);
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
}
#[test]
fn test_get_or_parse() -> Result<()> {
let cache = TemplateCache::new(10);
let mut temp = NamedTempFile::new()
.map_err(|e| Error::with_source("Failed to create temp file", Box::new(e)))?;
writeln!(
temp,
r#"---
to: "output.rs"
---
fn main() {{}}"#
)
.map_err(|e| Error::with_source("Failed to write temp file", Box::new(e)))?;
let template1 = cache.get_or_parse(temp.path())?;
assert_eq!(cache.stats()?.size, 1);
let template2 = cache.get_or_parse(temp.path())?;
assert_eq!(cache.stats()?.size, 1);
assert!(Arc::ptr_eq(&template1, &template2));
Ok(())
}
#[test]
fn test_cache_clear() -> Result<()> {
let cache = TemplateCache::new(10);
let mut temp = NamedTempFile::new()
.map_err(|e| Error::with_source("Failed to create temp file", Box::new(e)))?;
writeln!(
temp,
r#"---
to: "output.rs"
---
fn main() {{}}"#
)
.map_err(|e| Error::with_source("Failed to write temp file", Box::new(e)))?;
cache.get_or_parse(temp.path())?;
assert_eq!(cache.stats()?.size, 1);
cache.clear()?;
assert_eq!(cache.stats()?.size, 0);
Ok(())
}
#[test]
fn test_cache_eviction() -> Result<()> {
let cache = TemplateCache::new(2);
let mut temp1 = NamedTempFile::new()
.map_err(|e| Error::with_source("Failed to create temp file 1", Box::new(e)))?;
let mut temp2 = NamedTempFile::new()
.map_err(|e| Error::with_source("Failed to create temp file 2", Box::new(e)))?;
let mut temp3 = NamedTempFile::new()
.map_err(|e| Error::with_source("Failed to create temp file 3", Box::new(e)))?;
writeln!(temp1, "---\nto: '1.rs'\n---\nfile1")
.map_err(|e| Error::with_source("Failed to write temp file 1", Box::new(e)))?;
writeln!(temp2, "---\nto: '2.rs'\n---\nfile2")
.map_err(|e| Error::with_source("Failed to write temp file 2", Box::new(e)))?;
writeln!(temp3, "---\nto: '3.rs'\n---\nfile3")
.map_err(|e| Error::with_source("Failed to write temp file 3", Box::new(e)))?;
cache.get_or_parse(temp1.path())?;
cache.get_or_parse(temp2.path())?;
assert_eq!(cache.stats()?.size, 2);
cache.get_or_parse(temp3.path())?;
assert_eq!(cache.stats()?.size, 2);
Ok(())
}
}