torrust_tracker_deployer_lib/domain/template/
engine.rs1use serde::Serialize;
6use std::error::Error as StdError;
7use tera::Tera;
8use thiserror::Error;
9
10fn tera_error_chain(err: &tera::Error) -> String {
16 let mut messages = vec![err.to_string()];
17 let mut current: Option<&(dyn StdError + 'static)> = err.source();
18
19 while let Some(source) = current {
20 messages.push(source.to_string());
21 current = source.source();
22 }
23
24 messages.join(" -> ")
25}
26
27#[derive(Debug, Error)]
29pub enum TemplateEngineError {
30 #[error(
31 "Failed to parse template '{template_name}': {}",
32 tera_error_chain(source)
33 )]
34 TemplateParse {
35 template_name: String,
36 #[source]
37 source: tera::Error,
38 },
39
40 #[error("Failed to serialize template context: {}", tera_error_chain(source))]
41 ContextSerialization {
42 #[source]
43 source: tera::Error,
44 },
45
46 #[error(
47 "Failed to render template '{template_name}': {}",
48 tera_error_chain(source)
49 )]
50 TemplateRender {
51 template_name: String,
52 #[source]
53 source: tera::Error,
54 },
55}
56
57#[derive(Debug, Default)]
59pub struct TemplateEngine {
60 tera: Tera,
61}
62
63impl TemplateEngine {
64 #[must_use]
66 pub fn new() -> Self {
67 Self {
68 tera: Tera::default(),
69 }
70 }
71
72 pub fn render<T: Serialize>(
83 &mut self,
84 template_name: &str,
85 template_content: &str,
86 context: &T,
87 ) -> Result<String, TemplateEngineError> {
88 self.add_template(template_name, template_content)?;
90
91 let validated_content = self.render_template(template_name, context)?;
93
94 Ok(validated_content)
95 }
96
97 fn add_template(
102 &mut self,
103 template_name: &str,
104 template_content: &str,
105 ) -> Result<(), TemplateEngineError> {
106 self.tera
107 .add_raw_template(template_name, template_content)
108 .map_err(|source| TemplateEngineError::TemplateParse {
109 template_name: template_name.to_string(),
110 source,
111 })?;
112
113 Ok(())
114 }
115
116 fn render_template<T: Serialize>(
121 &self,
122 template_name: &str,
123 context: &T,
124 ) -> Result<String, TemplateEngineError> {
125 let tera_context = tera::Context::from_serialize(context)
126 .map_err(|source| TemplateEngineError::ContextSerialization { source })?;
127
128 let rendered_content =
129 self.tera
130 .render(template_name, &tera_context)
131 .map_err(|source| TemplateEngineError::TemplateRender {
132 template_name: template_name.to_string(),
133 source,
134 })?;
135
136 Ok(rendered_content)
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use serde::Serialize;
144
145 #[derive(Serialize)]
146 struct TestContext {
147 name: String,
148 value: u32,
149 }
150
151 #[derive(Serialize)]
152 struct PartialContext {
153 name: String,
154 }
155
156 #[test]
158 fn it_should_create_new_validator_instance() {
159 let validator = TemplateEngine::new();
160
161 assert!(format!("{validator:?}").contains("TemplateEngine"));
164 }
165
166 #[test]
167 fn it_should_create_multiple_independent_validators() {
168 let validator1 = TemplateEngine::new();
169 let validator2 = TemplateEngine::new();
170
171 assert!(format!("{validator1:?}").contains("TemplateEngine"));
173 assert!(format!("{validator2:?}").contains("TemplateEngine"));
174 }
175
176 #[test]
178 fn it_should_render_simple_template_successfully() -> Result<(), TemplateEngineError> {
179 let mut validator = TemplateEngine::new();
180 let template_content = "Hello {{name}}! Value: {{value}}";
181 let context = TestContext {
182 name: "World".to_string(),
183 value: 42,
184 };
185
186 let rendered_content = validator.render("test_template", template_content, &context)?;
187
188 assert_eq!(rendered_content, "Hello World! Value: 42");
189 Ok(())
190 }
191
192 #[test]
193 fn it_should_render_template_with_no_variables() -> Result<(), TemplateEngineError> {
194 let mut validator = TemplateEngine::new();
195 let template_content = "This is a static template with no variables.";
196 let context = TestContext {
197 name: "unused".to_string(),
198 value: 0,
199 };
200
201 let rendered_content = validator.render("static_template", template_content, &context)?;
202
203 assert_eq!(
204 rendered_content,
205 "This is a static template with no variables."
206 );
207 Ok(())
208 }
209
210 #[test]
211 fn it_should_render_empty_template() -> Result<(), TemplateEngineError> {
212 let mut validator = TemplateEngine::new();
213 let template_content = "";
214 let context = TestContext {
215 name: "test".to_string(),
216 value: 42,
217 };
218
219 let rendered_content = validator.render("empty_template", template_content, &context)?;
220
221 assert_eq!(rendered_content, "");
222 Ok(())
223 }
224
225 #[test]
226 fn it_should_handle_empty_template_name() -> Result<(), TemplateEngineError> {
227 let mut validator = TemplateEngine::new();
228 let template_content = "Hello {{name}}!";
229 let context = TestContext {
230 name: "World".to_string(),
231 value: 42,
232 };
233
234 let rendered_content = validator.render("", template_content, &context)?;
235
236 assert_eq!(rendered_content, "Hello World!");
237 Ok(())
238 }
239
240 #[test]
241 fn it_should_fail_when_template_has_malformed_syntax() {
242 let mut validator = TemplateEngine::new();
243 let template_content = "Hello {{name! Invalid syntax";
244 let context = TestContext {
245 name: "World".to_string(),
246 value: 42,
247 };
248
249 let result = validator.render("malformed_template", template_content, &context);
250
251 assert!(result.is_err());
252 match result.unwrap_err() {
253 TemplateEngineError::TemplateParse { template_name, .. } => {
254 assert_eq!(template_name, "malformed_template");
255 }
256 other => panic!("Expected TemplateParse error, got: {other:?}"),
257 }
258 }
259
260 #[test]
261 fn it_should_fail_when_template_references_undefined_variable() {
262 let mut validator = TemplateEngine::new();
263 let template_content = "Hello {{name}}! Value: {{undefined_variable}}";
264 let context = PartialContext {
265 name: "World".to_string(),
266 };
267
268 let result = validator.render("undefined_var_template", template_content, &context);
269
270 assert!(result.is_err());
271 match result.unwrap_err() {
272 TemplateEngineError::TemplateRender { template_name, .. } => {
273 assert_eq!(template_name, "undefined_var_template");
274 }
275 other => panic!("Expected TemplateRender error, got: {other:?}"),
276 }
277 }
278
279 #[test]
280 fn it_should_fail_when_context_serialization_fails() {
281 struct FailingSerialize;
282
283 impl Serialize for FailingSerialize {
284 fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
285 where
286 S: serde::Serializer,
287 {
288 Err(serde::ser::Error::custom(
289 "Intentional serialization failure",
290 ))
291 }
292 }
293
294 let mut validator = TemplateEngine::new();
295 let template_content = "Hello {{name}}!";
296 let context = FailingSerialize;
297
298 let result = validator.render("serialize_fail_template", template_content, &context);
299
300 assert!(result.is_err());
301 match result.unwrap_err() {
302 TemplateEngineError::ContextSerialization { .. } => {
303 }
305 other => panic!("Expected ContextSerialization error, got: {other:?}"),
306 }
307 }
308
309 #[test]
310 fn it_should_render_yaml_like_template() -> Result<(), TemplateEngineError> {
311 let mut validator = TemplateEngine::new();
312 let template_content = "name: {{name}}\nvalue: {{value}}\nstatic: true";
313 let context = TestContext {
314 name: "test".to_string(),
315 value: 42,
316 };
317
318 let rendered_content = validator.render("yaml_template", template_content, &context)?;
319
320 assert!(rendered_content.contains("name: test"));
321 assert!(rendered_content.contains("value: 42"));
322 assert!(rendered_content.contains("static: true"));
323 Ok(())
324 }
325
326 #[test]
327 fn it_should_render_different_templates_with_same_validator() -> Result<(), TemplateEngineError>
328 {
329 let mut validator = TemplateEngine::new();
330
331 let context = TestContext {
332 name: "Alice".to_string(),
333 value: 100,
334 };
335
336 let template1 = "Hello {{name}}!";
338 let result1 = validator.render("template1", template1, &context)?;
339 assert_eq!(result1, "Hello Alice!");
340
341 let template2 = "Value is: {{value}}";
343 let result2 = validator.render("template2", template2, &context)?;
344 assert_eq!(result2, "Value is: 100");
345
346 Ok(())
347 }
348
349 #[test]
350 fn it_should_handle_complex_template_with_multiple_variables() -> Result<(), TemplateEngineError>
351 {
352 let mut validator = TemplateEngine::new();
353 let template_content = r#"
354# Configuration for {{name}}
355version: "1.0"
356settings:
357 port: {{value}}
358 enabled: true
359 name: "{{name}}"
360"#;
361 let context = TestContext {
362 name: "MyApp".to_string(),
363 value: 8080,
364 };
365
366 let rendered_content = validator.render("config_template", template_content, &context)?;
367
368 assert!(rendered_content.contains("# Configuration for MyApp"));
369 assert!(rendered_content.contains("port: 8080"));
370 assert!(rendered_content.contains(r#"name: "MyApp""#));
371 Ok(())
372 }
373
374 #[test]
375 fn it_should_handle_special_characters_in_template_name() -> Result<(), TemplateEngineError> {
376 let mut validator = TemplateEngine::new();
377 let template_content = "Hello {{name}}!";
378 let context = TestContext {
379 name: "World".to_string(),
380 value: 42,
381 };
382
383 let rendered_content =
385 validator.render("template-with_special.chars", template_content, &context)?;
386
387 assert_eq!(rendered_content, "Hello World!");
388 Ok(())
389 }
390
391 #[test]
392 fn it_should_allow_extra_variables_in_context() -> Result<(), TemplateEngineError> {
393 let mut validator = TemplateEngine::new();
394 let template_content = "Hello {{name}}!"; let context = TestContext {
396 name: "World".to_string(),
397 value: 42, };
399
400 let rendered_content =
401 validator.render("extra_vars_template", template_content, &context)?;
402
403 assert_eq!(rendered_content, "Hello World!");
405 Ok(())
406 }
407
408 #[test]
409 fn it_should_allow_tera_delimiters_in_rendered_output() -> Result<(), TemplateEngineError> {
410 let mut validator = TemplateEngine::new();
411
412 let template_content =
415 "Hello {{name}}! Use {% raw %}{{ variable }}{% endraw %} in your config.";
416
417 let context = TestContext {
418 name: "World".to_string(),
419 value: 42,
420 };
421
422 let rendered_content =
423 validator.render("delimiter_template", template_content, &context)?;
424
425 assert_eq!(
427 rendered_content,
428 "Hello World! Use {{ variable }} in your config."
429 );
430 Ok(())
431 }
432
433 #[test]
434 fn it_should_allow_all_tera_delimiter_types_in_output() -> Result<(), TemplateEngineError> {
435 #[derive(Serialize)]
436 struct SimpleContext {
437 app_name: String,
438 }
439
440 let mut validator = TemplateEngine::new();
441
442 let template_content = r"App: {{app_name}}
444Expression example: {% raw %}{{ expr }}{% endraw %}
445Statement example: {% raw %}{% if condition %}{% endraw %}
446Comment example: {% raw %}{# comment #}{% endraw %}";
447
448 let context = SimpleContext {
449 app_name: "MyApp".to_string(),
450 };
451
452 let rendered_content = validator.render("all_delimiters", template_content, &context)?;
453
454 assert!(rendered_content.contains("App: MyApp"));
456 assert!(rendered_content.contains("{{ expr }}"));
457 assert!(rendered_content.contains("{% if condition %}"));
458 assert!(rendered_content.contains("{# comment #}"));
459 Ok(())
460 }
461}