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}