armature_mail/
template_handlebars.rs1use handlebars::Handlebars;
4use std::path::Path;
5use tracing::debug;
6
7use crate::{MailError, RenderedTemplate, Result, TemplateEngine};
8
9pub struct HandlebarsEngine {
11 handlebars: Handlebars<'static>,
12}
13
14impl HandlebarsEngine {
15 pub fn new() -> Self {
17 let mut handlebars = Handlebars::new();
18 handlebars.set_strict_mode(true);
19 Self { handlebars }
20 }
21
22 pub fn from_directory(path: impl AsRef<Path>) -> Result<Self> {
37 let mut engine = Self::new();
38 let path = path.as_ref();
39
40 if !path.exists() {
41 return Err(MailError::Config(format!(
42 "Template directory not found: {}",
43 path.display()
44 )));
45 }
46
47 for entry in std::fs::read_dir(path)? {
48 let entry = entry?;
49 let entry_path = entry.path();
50
51 if entry_path.is_dir() {
52 let template_name =
53 entry_path
54 .file_name()
55 .and_then(|n| n.to_str())
56 .ok_or_else(|| {
57 MailError::Config("Invalid template directory name".to_string())
58 })?;
59
60 let html_path = entry_path.join("html.hbs");
62 if html_path.exists() {
63 let content = std::fs::read_to_string(&html_path)?;
64 engine
65 .handlebars
66 .register_template_string(&format!("{}/html", template_name), content)?;
67 }
68
69 let text_path = entry_path.join("text.hbs");
71 if text_path.exists() {
72 let content = std::fs::read_to_string(&text_path)?;
73 engine
74 .handlebars
75 .register_template_string(&format!("{}/text", template_name), content)?;
76 }
77
78 let subject_path = entry_path.join("subject.hbs");
80 if subject_path.exists() {
81 let content = std::fs::read_to_string(&subject_path)?;
82 engine
83 .handlebars
84 .register_template_string(&format!("{}/subject", template_name), content)?;
85 }
86
87 debug!(template = template_name, "Loaded email template");
88 }
89 }
90
91 Ok(engine)
92 }
93
94 pub fn register_helper<H: handlebars::HelperDef + Send + Sync + 'static>(
96 mut self,
97 name: &str,
98 helper: H,
99 ) -> Self {
100 self.handlebars.register_helper(name, Box::new(helper));
101 self
102 }
103
104 pub fn register_partial(mut self, name: &str, content: &str) -> Result<Self> {
106 self.handlebars.register_partial(name, content)?;
107 Ok(self)
108 }
109}
110
111impl Default for HandlebarsEngine {
112 fn default() -> Self {
113 Self::new()
114 }
115}
116
117impl TemplateEngine for HandlebarsEngine {
118 fn render(&self, name: &str, context: &serde_json::Value) -> Result<RenderedTemplate> {
119 let html = if self.handlebars.has_template(&format!("{}/html", name)) {
120 Some(self.handlebars.render(&format!("{}/html", name), context)?)
121 } else {
122 None
123 };
124
125 let text = if self.handlebars.has_template(&format!("{}/text", name)) {
126 Some(self.handlebars.render(&format!("{}/text", name), context)?)
127 } else {
128 None
129 };
130
131 let subject = if self.handlebars.has_template(&format!("{}/subject", name)) {
132 Some(
133 self.handlebars
134 .render(&format!("{}/subject", name), context)?
135 .trim()
136 .to_string(),
137 )
138 } else {
139 None
140 };
141
142 if html.is_none() && text.is_none() {
143 return Err(MailError::TemplateNotFound(name.to_string()));
144 }
145
146 Ok(RenderedTemplate {
147 html,
148 text,
149 subject,
150 })
151 }
152
153 fn has_template(&self, name: &str) -> bool {
154 self.handlebars.has_template(&format!("{}/html", name))
155 || self.handlebars.has_template(&format!("{}/text", name))
156 }
157
158 fn register_template(&mut self, name: &str, content: &str) -> Result<()> {
159 self.handlebars
160 .register_template_string(&format!("{}/html", name), content)?;
161 Ok(())
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use serde_json::json;
169
170 #[test]
171 fn test_handlebars_render() {
172 let mut engine = HandlebarsEngine::new();
173 engine
174 .handlebars
175 .register_template_string("test/html", "<h1>Hello, {{name}}!</h1>")
176 .unwrap();
177 engine
178 .handlebars
179 .register_template_string("test/text", "Hello, {{name}}!")
180 .unwrap();
181 engine
182 .handlebars
183 .register_template_string("test/subject", "Welcome {{name}}")
184 .unwrap();
185
186 let result = engine.render("test", &json!({"name": "World"})).unwrap();
187
188 assert_eq!(result.html.as_deref(), Some("<h1>Hello, World!</h1>"));
189 assert_eq!(result.text.as_deref(), Some("Hello, World!"));
190 assert_eq!(result.subject.as_deref(), Some("Welcome World"));
191 }
192}