//! Template service for handling localized template rendering #![allow(dead_code)] use crate::template::{ RenderedTemplate, Result, TemplateConfig, TemplateEngine, TemplateError, TemplateLoader, TemplatePageConfig, }; use std::collections::HashMap; use std::sync::{Arc, RwLock}; use tracing::{debug, info, warn}; /// Template service that manages template loading and rendering #[derive(Debug)] pub struct TemplateService { /// Template engine engine: Arc>, /// Template loader loader: Arc>, /// Template configuration config: TemplateConfig, /// Cache for rendered templates render_cache: Arc>>, /// Cache enabled flag cache_enabled: bool, } impl TemplateService { /// Create a new template service pub fn new(template_dir: impl Into, content_dir: impl Into) -> Result { let template_dir = template_dir.into(); let content_dir = content_dir.into(); let config = TemplateConfig::new(&template_dir, &content_dir); let mut engine = TemplateEngine::new(&template_dir)?; // Add default filters engine.add_default_filters(); let loader = TemplateLoader::new(config.clone()); Ok(Self { engine: Arc::new(RwLock::new(engine)), loader: Arc::new(RwLock::new(loader)), config, render_cache: Arc::new(RwLock::new(HashMap::new())), cache_enabled: true, }) } /// Create template service with custom configuration pub fn with_config(config: TemplateConfig) -> Result { let mut engine = TemplateEngine::new(&config.template_dir)?; engine.add_default_filters(); let loader = TemplateLoader::new(config.clone()); Ok(Self { engine: Arc::new(RwLock::new(engine)), loader: Arc::new(RwLock::new(loader)), config, render_cache: Arc::new(RwLock::new(HashMap::new())), cache_enabled: true, }) } /// Set available languages pub fn with_languages(self, languages: Vec) -> Self { if let Ok(mut loader) = self.loader.write() { *loader = std::mem::take(&mut *loader).with_languages(languages); } self } /// Set default language pub fn with_default_language(self, lang: impl Into) -> Self { if let Ok(mut loader) = self.loader.write() { *loader = std::mem::take(&mut *loader).with_default_lang(lang); } self } /// Enable or disable caching pub fn with_cache(mut self, enabled: bool) -> Self { self.cache_enabled = enabled; if let Ok(mut loader) = self.loader.write() { *loader = std::mem::take(&mut *loader).with_cache(enabled); } self } /// Render a template page /// /// For URL `/page:content-name` with language `lang`, this will: /// 1. Load the template configuration from `{lang}_content-name.tpl.toml` /// 2. Render the template using the Tera engine /// 3. Return the rendered content pub async fn render_page(&self, content_name: &str, lang: &str) -> Result { let cache_key = format!("{}_{}", lang, content_name); // Check render cache first if self.cache_enabled { if let Ok(cache) = self.render_cache.read() { if let Some(cached) = cache.get(&cache_key) { debug!("Serving cached template for {}", cache_key); return Ok(cached.clone()); } } } // Load template configuration let config = { let mut loader = self.loader.write().map_err(|_| { TemplateError::RenderError("Failed to acquire loader lock".to_string()) })?; loader.load_page_config_with_fallback(content_name, lang)? }; // Render template let source_path = { let loader = self.loader.read().map_err(|_| { TemplateError::RenderError("Failed to acquire loader lock".to_string()) })?; loader.get_template_file_path(content_name, lang) }; let rendered = { let engine = self.engine.read().map_err(|_| { TemplateError::RenderError("Failed to acquire engine lock".to_string()) })?; engine.render_template(&config, &source_path)? }; // Cache the result if self.cache_enabled { if let Ok(mut cache) = self.render_cache.write() { cache.insert(cache_key, rendered.clone()); } } info!( "Rendered template '{}' for content '{}' in language '{}'", config.template_name, content_name, lang ); Ok(rendered) } /// Check if a template page exists pub fn page_exists(&self, content_name: &str, lang: &str) -> bool { let loader = match self.loader.read() { Ok(loader) => loader, Err(_) => return false, }; loader.exists(content_name, lang) } /// Get all available content for a specific language pub async fn get_available_content(&self, lang: &str) -> Result> { let loader = self .loader .read() .map_err(|_| TemplateError::RenderError("Failed to acquire loader lock".to_string()))?; loader.get_available_content(lang) } /// Get available languages pub fn get_available_languages(&self) -> Vec { let loader = match self.loader.read() { Ok(loader) => loader, Err(_) => return vec!["en".to_string()], }; loader.get_available_languages().to_vec() } /// Get default language pub fn get_default_language(&self) -> String { let loader = match self.loader.read() { Ok(loader) => loader, Err(_) => return "en".to_string(), }; loader.get_default_language().to_string() } /// Parse page URL to extract content name /// /// Converts `/page:content-name` to `content-name` pub fn parse_page_url(&self, url_path: &str) -> Option { let loader = self.loader.read().ok()?; loader.parse_page_url(url_path) } /// Generate page URL from content name /// /// Converts `content-name` to `/page:content-name` pub fn generate_page_url(&self, content_name: &str) -> String { let loader = match self.loader.read() { Ok(loader) => loader, Err(_) => return format!("/page:{}", content_name), }; loader.generate_page_url(content_name) } /// Clear all caches pub async fn clear_cache(&self) -> Result<()> { // Clear render cache if let Ok(mut cache) = self.render_cache.write() { cache.clear(); } // Clear loader cache if let Ok(mut loader) = self.loader.write() { loader.clear_cache(); } info!("Template caches cleared"); Ok(()) } /// Reload templates from disk pub async fn reload_templates(&self) -> Result<()> { // Reload engine templates if let Ok(mut engine) = self.engine.write() { engine.reload_templates()?; } // Clear all caches self.clear_cache().await?; info!("Templates reloaded from disk"); Ok(()) } /// Get template engine statistics pub fn get_engine_stats(&self) -> HashMap { let mut stats = HashMap::new(); if let Ok(engine) = self.engine.read() { let template_names = engine.get_template_names(); stats.insert("template_count".to_string(), template_names.len().into()); stats.insert("template_names".to_string(), template_names.into()); } if let Ok(cache) = self.render_cache.read() { stats.insert("render_cache_size".to_string(), cache.len().into()); } if let Ok(loader) = self.loader.read() { let (cache_size, cache_enabled) = loader.get_cache_stats(); stats.insert("loader_cache_size".to_string(), cache_size.into()); stats.insert("loader_cache_enabled".to_string(), cache_enabled.into()); } stats.insert("cache_enabled".to_string(), self.cache_enabled.into()); stats } /// Render a template with custom context (for testing or special cases) pub async fn render_with_context( &self, template_name: &str, context: HashMap, ) -> Result { let engine = self .engine .read() .map_err(|_| TemplateError::RenderError("Failed to acquire engine lock".to_string()))?; let tera_context = TemplateEngine::create_context(&context); engine.render_with_context(template_name, &tera_context) } /// Add a custom filter to the template engine pub fn add_filter(&self, name: &str, filter: F) -> Result<()> where F: tera::Filter + 'static, { let mut engine = self .engine .write() .map_err(|_| TemplateError::RenderError("Failed to acquire engine lock".to_string()))?; engine.add_filter(name, filter); Ok(()) } /// Add a custom function to the template engine pub fn add_function(&self, name: &str, func: F) -> Result<()> where F: tera::Function + 'static, { let mut engine = self .engine .write() .map_err(|_| TemplateError::RenderError("Failed to acquire engine lock".to_string()))?; engine.add_function(name, func); Ok(()) } /// Preload templates for a specific language pub async fn preload_language(&self, lang: &str) -> Result { let content_names = self.get_available_content(lang).await?; let mut loaded_count = 0; for content_name in content_names { match self.render_page(&content_name, lang).await { Ok(_) => { loaded_count += 1; debug!("Preloaded template: {}_{}", lang, content_name); } Err(e) => { warn!( "Failed to preload template {}_{}: {}", lang, content_name, e ); } } } info!( "Preloaded {} templates for language '{}'", loaded_count, lang ); Ok(loaded_count) } /// Get template configuration for a specific page pub async fn get_page_config( &self, content_name: &str, lang: &str, ) -> Result { let mut loader = self .loader .write() .map_err(|_| TemplateError::RenderError("Failed to acquire loader lock".to_string()))?; loader.load_page_config_with_fallback(content_name, lang) } /// Check if template engine has a specific template pub fn has_template(&self, template_name: &str) -> bool { let engine = match self.engine.read() { Ok(engine) => engine, Err(_) => return false, }; engine.template_exists(template_name) } } impl Clone for TemplateService { fn clone(&self) -> Self { Self { engine: Arc::clone(&self.engine), loader: Arc::clone(&self.loader), config: self.config.clone(), render_cache: Arc::clone(&self.render_cache), cache_enabled: self.cache_enabled, } } } #[cfg(test)] mod tests { use super::*; use std::fs; use tempfile::TempDir; fn create_test_template_file( dir: &std::path::Path, filename: &str, content: &str, ) -> std::io::Result<()> { let file_path = dir.join(filename); fs::write(file_path, content) } #[tokio::test] async fn test_template_service_creation() { let temp_dir = TempDir::new().unwrap(); let template_dir = temp_dir.path().join("templates"); let content_dir = temp_dir.path().join("content"); fs::create_dir_all(&template_dir).unwrap(); fs::create_dir_all(&content_dir).unwrap(); // Create a test template create_test_template_file(&template_dir, "test.html", "

{{title}}

").unwrap(); let service = TemplateService::new( template_dir.to_str().unwrap(), content_dir.to_str().unwrap(), ); assert!(service.is_ok()); } #[tokio::test] async fn test_render_page() { let temp_dir = TempDir::new().unwrap(); let template_dir = temp_dir.path().join("templates"); let content_dir = temp_dir.path().join("content"); fs::create_dir_all(&template_dir).unwrap(); fs::create_dir_all(&content_dir).unwrap(); // Create template create_test_template_file( &template_dir, "blog-post.html", "

{{title}}

{{content}}

", ) .unwrap(); // Create template config let config_content = r#" template_name = "blog-post" [values] title = "Test Blog Post" content = "This is test content" "#; create_test_template_file(&content_dir, "en_my-blog.tpl.toml", config_content).unwrap(); let service = TemplateService::new( template_dir.to_str().unwrap(), content_dir.to_str().unwrap(), ) .unwrap(); let result = service.render_page("my-blog", "en").await.unwrap(); assert!(result.content.contains("Test Blog Post")); assert!(result.content.contains("This is test content")); } #[tokio::test] async fn test_language_fallback() { let temp_dir = TempDir::new().unwrap(); let template_dir = temp_dir.path().join("templates"); let content_dir = temp_dir.path().join("content"); fs::create_dir_all(&template_dir).unwrap(); fs::create_dir_all(&content_dir).unwrap(); // Create template create_test_template_file(&template_dir, "blog-post.html", "

{{title}}

").unwrap(); // Create only English config let config_content = r#" template_name = "blog-post" [values] title = "English Title" "#; create_test_template_file(&content_dir, "en_my-blog.tpl.toml", config_content).unwrap(); let service = TemplateService::new( template_dir.to_str().unwrap(), content_dir.to_str().unwrap(), ) .unwrap(); // Try to render in French, should fallback to English let result = service.render_page("my-blog", "fr").await.unwrap(); assert!(result.content.contains("English Title")); } #[tokio::test] async fn test_parse_page_url() { let temp_dir = TempDir::new().unwrap(); let template_dir = temp_dir.path().join("templates"); let content_dir = temp_dir.path().join("content"); fs::create_dir_all(&template_dir).unwrap(); fs::create_dir_all(&content_dir).unwrap(); let service = TemplateService::new( template_dir.to_str().unwrap(), content_dir.to_str().unwrap(), ) .unwrap(); assert_eq!( service.parse_page_url("/page:my-blog"), Some("my-blog".to_string()) ); assert_eq!( service.parse_page_url("/page:about-us"), Some("about-us".to_string()) ); assert_eq!(service.parse_page_url("/other-path"), None); } #[tokio::test] async fn test_generate_page_url() { let temp_dir = TempDir::new().unwrap(); let template_dir = temp_dir.path().join("templates"); let content_dir = temp_dir.path().join("content"); fs::create_dir_all(&template_dir).unwrap(); fs::create_dir_all(&content_dir).unwrap(); let service = TemplateService::new( template_dir.to_str().unwrap(), content_dir.to_str().unwrap(), ) .unwrap(); assert_eq!(service.generate_page_url("my-blog"), "/page:my-blog"); assert_eq!(service.generate_page_url("about-us"), "/page:about-us"); } }