Skip to main content

sal_text/
template.rs

1use std::collections::HashMap;
2use std::fs;
3use std::io;
4use std::path::Path;
5use tera::{Context, Tera};
6
7/// A builder for creating and rendering templates using the Tera template engine.
8#[derive(Clone)]
9pub struct TemplateBuilder {
10    template_path: String,
11    context: Context,
12    tera: Option<Tera>,
13}
14
15impl TemplateBuilder {
16    /// Creates a new TemplateBuilder with the specified template path.
17    ///
18    /// # Arguments
19    ///
20    /// * `template_path` - The path to the template file
21    ///
22    /// # Returns
23    ///
24    /// A new TemplateBuilder instance
25    ///
26    /// # Example
27    ///
28    /// ```
29    /// use sal_text::TemplateBuilder;
30    ///
31    /// let builder = TemplateBuilder::open("templates/example.html");
32    /// ```
33    pub fn open<P: AsRef<Path>>(template_path: P) -> io::Result<Self> {
34        let path_str = template_path.as_ref().to_string_lossy().to_string();
35
36        // Verify the template file exists
37        if !Path::new(&path_str).exists() {
38            return Err(io::Error::new(
39                io::ErrorKind::NotFound,
40                format!("Template file not found: {}", path_str),
41            ));
42        }
43
44        Ok(Self {
45            template_path: path_str,
46            context: Context::new(),
47            tera: None,
48        })
49    }
50
51    /// Adds a variable to the template context.
52    ///
53    /// # Arguments
54    ///
55    /// * `name` - The name of the variable to add
56    /// * `value` - The value to associate with the variable
57    ///
58    /// # Returns
59    ///
60    /// The builder instance for method chaining
61    ///
62    /// # Example
63    ///
64    /// ```no_run
65    /// use sal_text::TemplateBuilder;
66    ///
67    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
68    ///     let builder = TemplateBuilder::open("templates/example.html")?
69    ///         .add_var("title", "Hello World")
70    ///         .add_var("username", "John Doe");
71    ///     Ok(())
72    /// }
73    /// ```
74    pub fn add_var<S, V>(mut self, name: S, value: V) -> Self
75    where
76        S: AsRef<str>,
77        V: serde::Serialize,
78    {
79        self.context.insert(name.as_ref(), &value);
80        self
81    }
82
83    /// Adds multiple variables to the template context from a HashMap.
84    ///
85    /// # Arguments
86    ///
87    /// * `vars` - A HashMap containing variable names and values
88    ///
89    /// # Returns
90    ///
91    /// The builder instance for method chaining
92    ///
93    /// # Example
94    ///
95    /// ```no_run
96    /// use sal_text::TemplateBuilder;
97    /// use std::collections::HashMap;
98    ///
99    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
100    ///     let mut vars = HashMap::new();
101    ///     vars.insert("title", "Hello World");
102    ///     vars.insert("username", "John Doe");
103    ///
104    ///     let builder = TemplateBuilder::open("templates/example.html")?
105    ///         .add_vars(vars);
106    ///     Ok(())
107    /// }
108    /// ```
109    pub fn add_vars<S, V>(mut self, vars: HashMap<S, V>) -> Self
110    where
111        S: AsRef<str>,
112        V: serde::Serialize,
113    {
114        for (name, value) in vars {
115            self.context.insert(name.as_ref(), &value);
116        }
117        self
118    }
119
120    /// Initializes the Tera template engine with the template file.
121    ///
122    /// This method is called automatically by render() if not called explicitly.
123    ///
124    /// # Returns
125    ///
126    /// The builder instance for method chaining
127    fn initialize_tera(&mut self) -> Result<(), tera::Error> {
128        if self.tera.is_none() {
129            // Create a new Tera instance with just this template
130            let mut tera = Tera::default();
131
132            // Read the template content
133            let template_content = fs::read_to_string(&self.template_path)
134                .map_err(|e| tera::Error::msg(format!("Failed to read template file: {}", e)))?;
135
136            // Add the template to Tera
137            let template_name = Path::new(&self.template_path)
138                .file_name()
139                .and_then(|n| n.to_str())
140                .unwrap_or("template");
141
142            tera.add_raw_template(template_name, &template_content)?;
143            self.tera = Some(tera);
144        }
145
146        Ok(())
147    }
148
149    /// Renders the template with the current context.
150    ///
151    /// # Returns
152    ///
153    /// The rendered template as a string
154    ///
155    /// # Example
156    ///
157    /// ```no_run
158    /// use sal_text::TemplateBuilder;
159    ///
160    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
161    ///     let result = TemplateBuilder::open("templates/example.html")?
162    ///         .add_var("title", "Hello World")
163    ///         .add_var("username", "John Doe")
164    ///         .render()?;
165    ///
166    ///     println!("Rendered template: {}", result);
167    ///     Ok(())
168    /// }
169    /// ```
170    pub fn render(&mut self) -> Result<String, tera::Error> {
171        // Initialize Tera if not already done
172        self.initialize_tera()?;
173
174        // Get the template name
175        let template_name = Path::new(&self.template_path)
176            .file_name()
177            .and_then(|n| n.to_str())
178            .unwrap_or("template");
179
180        // Render the template
181        let tera = self.tera.as_ref().unwrap();
182        tera.render(template_name, &self.context)
183    }
184
185    /// Renders the template and writes the result to a file.
186    ///
187    /// # Arguments
188    ///
189    /// * `output_path` - The path where the rendered template should be written
190    ///
191    /// # Returns
192    ///
193    /// Result indicating success or failure
194    ///
195    /// # Example
196    ///
197    /// ```no_run
198    /// use sal_text::TemplateBuilder;
199    ///
200    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
201    ///     TemplateBuilder::open("templates/example.html")?
202    ///         .add_var("title", "Hello World")
203    ///         .add_var("username", "John Doe")
204    ///         .render_to_file("output.html")?;
205    ///     Ok(())
206    /// }
207    /// ```
208    pub fn render_to_file<P: AsRef<Path>>(&mut self, output_path: P) -> io::Result<()> {
209        let rendered = self.render().map_err(|e| {
210            io::Error::new(
211                io::ErrorKind::Other,
212                format!("Template rendering error: {}", e),
213            )
214        })?;
215
216        fs::write(output_path, rendered)
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use std::io::Write;
224    use tempfile::NamedTempFile;
225
226    #[test]
227    fn test_template_rendering() -> Result<(), Box<dyn std::error::Error>> {
228        // Create a temporary template file
229        let temp_file = NamedTempFile::new()?;
230        let template_content = "Hello, {{ name }}! Welcome to {{ place }}.\n";
231        fs::write(temp_file.path(), template_content)?;
232
233        // Create a template builder and add variables
234        let mut builder = TemplateBuilder::open(temp_file.path())?;
235        builder = builder.add_var("name", "John").add_var("place", "Rust");
236
237        // Render the template
238        let result = builder.render()?;
239        assert_eq!(result, "Hello, John! Welcome to Rust.\n");
240
241        Ok(())
242    }
243
244    #[test]
245    fn test_template_with_multiple_vars() -> Result<(), Box<dyn std::error::Error>> {
246        // Create a temporary template file
247        let temp_file = NamedTempFile::new()?;
248        let template_content = "{% if show_greeting %}Hello, {{ name }}!{% endif %}\n{% for item in items %}{{ item }}{% if not loop.last %}, {% endif %}{% endfor %}\n";
249        fs::write(temp_file.path(), template_content)?;
250
251        // Create a template builder and add variables
252        let mut builder = TemplateBuilder::open(temp_file.path())?;
253
254        // Add variables including a boolean and a vector
255        builder = builder
256            .add_var("name", "Alice")
257            .add_var("show_greeting", true)
258            .add_var("items", vec!["apple", "banana", "cherry"]);
259
260        // Render the template
261        let result = builder.render()?;
262        assert_eq!(result, "Hello, Alice!\napple, banana, cherry\n");
263
264        Ok(())
265    }
266
267    #[test]
268    fn test_template_with_hashmap_vars() -> Result<(), Box<dyn std::error::Error>> {
269        // Create a temporary template file
270        let mut temp_file = NamedTempFile::new()?;
271        writeln!(temp_file, "{{{{ greeting }}}}, {{{{ name }}}}!")?;
272        temp_file.flush()?;
273
274        // Create a HashMap of variables
275        let mut vars = HashMap::new();
276        vars.insert("greeting", "Hi");
277        vars.insert("name", "Bob");
278
279        // Create a template builder and add variables from HashMap
280        let mut builder = TemplateBuilder::open(temp_file.path())?;
281        builder = builder.add_vars(vars);
282
283        // Render the template
284        let result = builder.render()?;
285        assert_eq!(result, "Hi, Bob!\n");
286
287        Ok(())
288    }
289    #[test]
290    fn test_render_to_file() -> Result<(), Box<dyn std::error::Error>> {
291        // Create a temporary template file
292        let temp_file = NamedTempFile::new()?;
293        let template_content = "{{ message }}\n";
294        fs::write(temp_file.path(), template_content)?;
295
296        // Create an output file
297        let output_file = NamedTempFile::new()?;
298
299        // Create a template builder, add a variable, and render to file
300        let mut builder = TemplateBuilder::open(temp_file.path())?;
301        builder = builder.add_var("message", "This is a test");
302        builder.render_to_file(output_file.path())?;
303
304        // Read the output file and verify its contents
305        let content = fs::read_to_string(output_file.path())?;
306        assert_eq!(content, "This is a test\n");
307
308        Ok(())
309    }
310}