sal-text 0.1.0

SAL Text - Text processing and manipulation utilities with regex, templating, and normalization
Documentation
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<Tera>,
}

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<P: AsRef<Path>>(template_path: P) -> io::Result<Self> {
        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<dyn std::error::Error>> {
    ///     let builder = TemplateBuilder::open("templates/example.html")?
    ///         .add_var("title", "Hello World")
    ///         .add_var("username", "John Doe");
    ///     Ok(())
    /// }
    /// ```
    pub fn add_var<S, V>(mut self, name: S, value: V) -> Self
    where
        S: AsRef<str>,
        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<dyn std::error::Error>> {
    ///     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<S, V>(mut self, vars: HashMap<S, V>) -> Self
    where
        S: AsRef<str>,
        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<dyn std::error::Error>> {
    ///     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<String, tera::Error> {
        // 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<dyn std::error::Error>> {
    ///     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<P: AsRef<Path>>(&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<dyn std::error::Error>> {
        // 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<dyn std::error::Error>> {
        // 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<dyn std::error::Error>> {
        // 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<dyn std::error::Error>> {
        // 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(())
    }
}