use std::collections::HashMap; use std::fs; use std::io; use std::path::Path; use tera::{Context, Tera}; /// A builder for creating and rendering templates using the Tera template engine. #[derive(Clone)] pub struct TemplateBuilder { template_path: String, context: Context, tera: Option, } impl TemplateBuilder { /// Creates a new TemplateBuilder with the specified template path. /// /// # Arguments /// /// * `template_path` - The path to the template file /// /// # Returns /// /// A new TemplateBuilder instance /// /// # Example /// /// ``` /// use sal_text::TemplateBuilder; /// /// let builder = TemplateBuilder::open("templates/example.html"); /// ``` pub fn open>(template_path: P) -> io::Result { let path_str = template_path.as_ref().to_string_lossy().to_string(); // Verify the template file exists if !Path::new(&path_str).exists() { return Err(io::Error::new( io::ErrorKind::NotFound, format!("Template file not found: {}", path_str), )); } Ok(Self { template_path: path_str, context: Context::new(), tera: None, }) } /// Adds a variable to the template context. /// /// # Arguments /// /// * `name` - The name of the variable to add /// * `value` - The value to associate with the variable /// /// # Returns /// /// The builder instance for method chaining /// /// # Example /// /// ```no_run /// use sal_text::TemplateBuilder; /// /// fn main() -> Result<(), Box> { /// let builder = TemplateBuilder::open("templates/example.html")? /// .add_var("title", "Hello World") /// .add_var("username", "John Doe"); /// Ok(()) /// } /// ``` pub fn add_var(mut self, name: S, value: V) -> Self where S: AsRef, V: serde::Serialize, { self.context.insert(name.as_ref(), &value); self } /// Adds multiple variables to the template context from a HashMap. /// /// # Arguments /// /// * `vars` - A HashMap containing variable names and values /// /// # Returns /// /// The builder instance for method chaining /// /// # Example /// /// ```no_run /// use sal_text::TemplateBuilder; /// use std::collections::HashMap; /// /// fn main() -> Result<(), Box> { /// let mut vars = HashMap::new(); /// vars.insert("title", "Hello World"); /// vars.insert("username", "John Doe"); /// /// let builder = TemplateBuilder::open("templates/example.html")? /// .add_vars(vars); /// Ok(()) /// } /// ``` pub fn add_vars(mut self, vars: HashMap) -> Self where S: AsRef, V: serde::Serialize, { for (name, value) in vars { self.context.insert(name.as_ref(), &value); } self } /// Initializes the Tera template engine with the template file. /// /// This method is called automatically by render() if not called explicitly. /// /// # Returns /// /// The builder instance for method chaining fn initialize_tera(&mut self) -> Result<(), tera::Error> { if self.tera.is_none() { // Create a new Tera instance with just this template let mut tera = Tera::default(); // Read the template content let template_content = fs::read_to_string(&self.template_path) .map_err(|e| tera::Error::msg(format!("Failed to read template file: {}", e)))?; // Add the template to Tera let template_name = Path::new(&self.template_path) .file_name() .and_then(|n| n.to_str()) .unwrap_or("template"); tera.add_raw_template(template_name, &template_content)?; self.tera = Some(tera); } Ok(()) } /// Renders the template with the current context. /// /// # Returns /// /// The rendered template as a string /// /// # Example /// /// ```no_run /// use sal_text::TemplateBuilder; /// /// fn main() -> Result<(), Box> { /// let result = TemplateBuilder::open("templates/example.html")? /// .add_var("title", "Hello World") /// .add_var("username", "John Doe") /// .render()?; /// /// println!("Rendered template: {}", result); /// Ok(()) /// } /// ``` pub fn render(&mut self) -> Result { // Initialize Tera if not already done self.initialize_tera()?; // Get the template name let template_name = Path::new(&self.template_path) .file_name() .and_then(|n| n.to_str()) .unwrap_or("template"); // Render the template let tera = self.tera.as_ref().unwrap(); tera.render(template_name, &self.context) } /// Renders the template and writes the result to a file. /// /// # Arguments /// /// * `output_path` - The path where the rendered template should be written /// /// # Returns /// /// Result indicating success or failure /// /// # Example /// /// ```no_run /// use sal_text::TemplateBuilder; /// /// fn main() -> Result<(), Box> { /// TemplateBuilder::open("templates/example.html")? /// .add_var("title", "Hello World") /// .add_var("username", "John Doe") /// .render_to_file("output.html")?; /// Ok(()) /// } /// ``` pub fn render_to_file>(&mut self, output_path: P) -> io::Result<()> { let rendered = self.render().map_err(|e| { io::Error::new( io::ErrorKind::Other, format!("Template rendering error: {}", e), ) })?; fs::write(output_path, rendered) } } #[cfg(test)] mod tests { use super::*; use std::io::Write; use tempfile::NamedTempFile; #[test] fn test_template_rendering() -> Result<(), Box> { // Create a temporary template file let temp_file = NamedTempFile::new()?; let template_content = "Hello, {{ name }}! Welcome to {{ place }}.\n"; fs::write(temp_file.path(), template_content)?; // Create a template builder and add variables let mut builder = TemplateBuilder::open(temp_file.path())?; builder = builder.add_var("name", "John").add_var("place", "Rust"); // Render the template let result = builder.render()?; assert_eq!(result, "Hello, John! Welcome to Rust.\n"); Ok(()) } #[test] fn test_template_with_multiple_vars() -> Result<(), Box> { // Create a temporary template file let temp_file = NamedTempFile::new()?; let template_content = "{% if show_greeting %}Hello, {{ name }}!{% endif %}\n{% for item in items %}{{ item }}{% if not loop.last %}, {% endif %}{% endfor %}\n"; fs::write(temp_file.path(), template_content)?; // Create a template builder and add variables let mut builder = TemplateBuilder::open(temp_file.path())?; // Add variables including a boolean and a vector builder = builder .add_var("name", "Alice") .add_var("show_greeting", true) .add_var("items", vec!["apple", "banana", "cherry"]); // Render the template let result = builder.render()?; assert_eq!(result, "Hello, Alice!\napple, banana, cherry\n"); Ok(()) } #[test] fn test_template_with_hashmap_vars() -> Result<(), Box> { // Create a temporary template file let mut temp_file = NamedTempFile::new()?; writeln!(temp_file, "{{{{ greeting }}}}, {{{{ name }}}}!")?; temp_file.flush()?; // Create a HashMap of variables let mut vars = HashMap::new(); vars.insert("greeting", "Hi"); vars.insert("name", "Bob"); // Create a template builder and add variables from HashMap let mut builder = TemplateBuilder::open(temp_file.path())?; builder = builder.add_vars(vars); // Render the template let result = builder.render()?; assert_eq!(result, "Hi, Bob!\n"); Ok(()) } #[test] fn test_render_to_file() -> Result<(), Box> { // Create a temporary template file let temp_file = NamedTempFile::new()?; let template_content = "{{ message }}\n"; fs::write(temp_file.path(), template_content)?; // Create an output file let output_file = NamedTempFile::new()?; // Create a template builder, add a variable, and render to file let mut builder = TemplateBuilder::open(temp_file.path())?; builder = builder.add_var("message", "This is a test"); builder.render_to_file(output_file.path())?; // Read the output file and verify its contents let content = fs::read_to_string(output_file.path())?; assert_eq!(content, "This is a test\n"); Ok(()) } }