acton_htmx/email/template.rs
1//! Email template trait for Askama integration
2//!
3//! Provides a trait for rendering email templates with both HTML and plain text versions.
4
5use super::EmailError;
6
7/// Trait for email templates
8///
9/// Implement this trait on your Askama templates to render emails with both
10/// HTML and plain text versions.
11///
12/// # Examples
13///
14/// ```rust,no_run
15/// use acton_htmx::email::{EmailTemplate, EmailError};
16/// use askama::Template;
17///
18/// #[derive(Template)]
19/// #[template(path = "emails/welcome.html")]
20/// struct WelcomeEmail {
21/// name: String,
22/// verification_url: String,
23/// }
24///
25/// #[derive(Template)]
26/// #[template(path = "emails/welcome.txt")]
27/// struct WelcomeEmailText {
28/// name: String,
29/// verification_url: String,
30/// }
31///
32/// impl EmailTemplate for WelcomeEmail {
33/// fn render_email(&self) -> Result<(Option<String>, Option<String>), EmailError> {
34/// let html = self.render()?;
35/// let text_template = WelcomeEmailText {
36/// name: self.name.clone(),
37/// verification_url: self.verification_url.clone(),
38/// };
39/// let text = text_template.render()?;
40/// Ok((Some(html), Some(text)))
41/// }
42/// }
43/// ```
44pub trait EmailTemplate {
45 /// Render the email template
46 ///
47 /// Returns a tuple of `(html, text)` where either can be `None`.
48 /// Most emails should provide both HTML and plain text versions.
49 ///
50 /// # Errors
51 ///
52 /// Returns `EmailError::TemplateError` if the template fails to render
53 fn render_email(&self) -> Result<(Option<String>, Option<String>), EmailError>;
54}
55
56/// Helper trait for rendering both HTML and text versions from a single template
57///
58/// This trait provides a default implementation that uses the same template
59/// for both HTML and text. For production use, you should create separate
60/// templates for HTML and text versions.
61pub trait SimpleEmailTemplate: askama::Template {
62 /// Render the template as HTML
63 ///
64 /// # Errors
65 ///
66 /// Returns `EmailError::TemplateError` if the template fails to render
67 fn render_html(&self) -> Result<String, EmailError> {
68 Ok(self.render()?)
69 }
70
71 /// Render a plain text version
72 ///
73 /// Default implementation returns `None`. Override this to provide
74 /// a plain text version.
75 ///
76 /// # Errors
77 ///
78 /// Returns `EmailError::TemplateError` if the template fails to render
79 fn render_text(&self) -> Result<Option<String>, EmailError> {
80 Ok(None)
81 }
82}
83
84impl<T: SimpleEmailTemplate> EmailTemplate for T {
85 fn render_email(&self) -> Result<(Option<String>, Option<String>), EmailError> {
86 let html = Some(self.render_html()?);
87 let text = self.render_text()?;
88 Ok((html, text))
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use askama::Template;
96
97 #[derive(Template)]
98 #[template(source = "<h1>Hello, {{ name }}!</h1>", ext = "html")]
99 struct TestTemplate {
100 name: String,
101 }
102
103 impl SimpleEmailTemplate for TestTemplate {}
104
105 #[test]
106 fn test_simple_email_template() {
107 let template = TestTemplate {
108 name: "Alice".to_string(),
109 };
110
111 let (html, text) = template.render_email().unwrap();
112
113 assert!(html.is_some());
114 assert_eq!(html.unwrap(), "<h1>Hello, Alice!</h1>");
115 assert!(text.is_none());
116 }
117
118 #[derive(Template)]
119 #[template(source = "<h1>Welcome, {{ name }}!</h1>", ext = "html")]
120 struct TestTemplateWithText {
121 name: String,
122 }
123
124 impl SimpleEmailTemplate for TestTemplateWithText {
125 fn render_text(&self) -> Result<Option<String>, EmailError> {
126 Ok(Some(format!("Welcome, {}!", self.name)))
127 }
128 }
129
130 #[test]
131 fn test_email_template_with_text() {
132 let template = TestTemplateWithText {
133 name: "Bob".to_string(),
134 };
135
136 let (html, text) = template.render_email().unwrap();
137
138 assert!(html.is_some());
139 assert_eq!(html.unwrap(), "<h1>Welcome, Bob!</h1>");
140 assert!(text.is_some());
141 assert_eq!(text.unwrap(), "Welcome, Bob!");
142 }
143}