use handlebars::Handlebars;
use std::path::Path;
use tracing::debug;
use crate::{MailError, RenderedTemplate, Result, TemplateEngine};
pub struct HandlebarsEngine {
handlebars: Handlebars<'static>,
}
impl HandlebarsEngine {
pub fn new() -> Self {
let mut handlebars = Handlebars::new();
handlebars.set_strict_mode(true);
Self { handlebars }
}
pub fn from_directory(path: impl AsRef<Path>) -> Result<Self> {
let mut engine = Self::new();
let path = path.as_ref();
if !path.exists() {
return Err(MailError::Config(format!(
"Template directory not found: {}",
path.display()
)));
}
for entry in std::fs::read_dir(path)? {
let entry = entry?;
let entry_path = entry.path();
if entry_path.is_dir() {
let template_name =
entry_path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| {
MailError::Config("Invalid template directory name".to_string())
})?;
let html_path = entry_path.join("html.hbs");
if html_path.exists() {
let content = std::fs::read_to_string(&html_path)?;
engine
.handlebars
.register_template_string(&format!("{}/html", template_name), content)?;
}
let text_path = entry_path.join("text.hbs");
if text_path.exists() {
let content = std::fs::read_to_string(&text_path)?;
engine
.handlebars
.register_template_string(&format!("{}/text", template_name), content)?;
}
let subject_path = entry_path.join("subject.hbs");
if subject_path.exists() {
let content = std::fs::read_to_string(&subject_path)?;
engine
.handlebars
.register_template_string(&format!("{}/subject", template_name), content)?;
}
debug!(template = template_name, "Loaded email template");
}
}
Ok(engine)
}
pub fn register_helper<H: handlebars::HelperDef + Send + Sync + 'static>(
mut self,
name: &str,
helper: H,
) -> Self {
self.handlebars.register_helper(name, Box::new(helper));
self
}
pub fn register_partial(mut self, name: &str, content: &str) -> Result<Self> {
self.handlebars.register_partial(name, content)?;
Ok(self)
}
}
impl Default for HandlebarsEngine {
fn default() -> Self {
Self::new()
}
}
impl TemplateEngine for HandlebarsEngine {
fn render(&self, name: &str, context: &serde_json::Value) -> Result<RenderedTemplate> {
let html = if self.handlebars.has_template(&format!("{}/html", name)) {
Some(self.handlebars.render(&format!("{}/html", name), context)?)
} else {
None
};
let text = if self.handlebars.has_template(&format!("{}/text", name)) {
Some(self.handlebars.render(&format!("{}/text", name), context)?)
} else {
None
};
let subject = if self.handlebars.has_template(&format!("{}/subject", name)) {
Some(
self.handlebars
.render(&format!("{}/subject", name), context)?
.trim()
.to_string(),
)
} else {
None
};
if html.is_none() && text.is_none() {
return Err(MailError::TemplateNotFound(name.to_string()));
}
Ok(RenderedTemplate {
html,
text,
subject,
})
}
fn has_template(&self, name: &str) -> bool {
self.handlebars.has_template(&format!("{}/html", name))
|| self.handlebars.has_template(&format!("{}/text", name))
}
fn register_template(&mut self, name: &str, content: &str) -> Result<()> {
self.handlebars
.register_template_string(&format!("{}/html", name), content)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_handlebars_render() {
let mut engine = HandlebarsEngine::new();
engine
.handlebars
.register_template_string("test/html", "<h1>Hello, {{name}}!</h1>")
.unwrap();
engine
.handlebars
.register_template_string("test/text", "Hello, {{name}}!")
.unwrap();
engine
.handlebars
.register_template_string("test/subject", "Welcome {{name}}")
.unwrap();
let result = engine.render("test", &json!({"name": "World"})).unwrap();
assert_eq!(result.html.as_deref(), Some("<h1>Hello, World!</h1>"));
assert_eq!(result.text.as_deref(), Some("Hello, World!"));
assert_eq!(result.subject.as_deref(), Some("Welcome World"));
}
}