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}