elif_email/templates/
mod.rs

1pub mod engine;
2pub mod registry;
3
4pub use engine::*;
5pub use registry::*;
6
7use crate::{Email, EmailError};
8use serde::Serialize;
9use std::collections::HashMap;
10
11/// Template context for rendering emails
12pub type TemplateContext = HashMap<String, serde_json::Value>;
13
14/// Email template definition
15#[derive(Debug, Clone)]
16pub struct EmailTemplate {
17    /// Template name/identifier
18    pub name: String,
19    /// HTML template content or path
20    pub html_template: Option<String>,
21    /// Text template content or path
22    pub text_template: Option<String>,
23    /// Layout to use (optional)
24    pub layout: Option<String>,
25    /// Default subject template
26    pub subject_template: Option<String>,
27    /// Template metadata
28    pub metadata: HashMap<String, String>,
29}
30
31/// Rendered email content
32#[derive(Debug, Clone)]
33pub struct RenderedEmail {
34    /// Rendered HTML content
35    pub html_content: Option<String>,
36    /// Rendered text content
37    pub text_content: Option<String>,
38    /// Rendered subject
39    pub subject: String,
40}
41
42/// Email template builder for fluent API
43#[derive(Debug, Clone)]
44pub struct EmailTemplateBuilder {
45    template: EmailTemplate,
46}
47
48impl EmailTemplate {
49    /// Create new email template
50    pub fn new(name: impl Into<String>) -> Self {
51        Self {
52            name: name.into(),
53            html_template: None,
54            text_template: None,
55            layout: None,
56            subject_template: None,
57            metadata: HashMap::new(),
58        }
59    }
60
61    /// Create template builder
62    pub fn builder(name: impl Into<String>) -> EmailTemplateBuilder {
63        EmailTemplateBuilder {
64            template: Self::new(name),
65        }
66    }
67
68    /// Render template with context  
69    pub fn render(&self, engine: &TemplateEngine, context: &TemplateContext) -> Result<RenderedEmail, EmailError> {
70        engine.render_template(self, context)
71    }
72}
73
74impl EmailTemplateBuilder {
75    /// Set HTML template
76    pub fn html_template(mut self, template: impl Into<String>) -> Self {
77        self.template.html_template = Some(template.into());
78        self
79    }
80
81    /// Set text template
82    pub fn text_template(mut self, template: impl Into<String>) -> Self {
83        self.template.text_template = Some(template.into());
84        self
85    }
86
87    /// Set layout
88    pub fn layout(mut self, layout: impl Into<String>) -> Self {
89        self.template.layout = Some(layout.into());
90        self
91    }
92
93    /// Set subject template
94    pub fn subject_template(mut self, template: impl Into<String>) -> Self {
95        self.template.subject_template = Some(template.into());
96        self
97    }
98
99    /// Add metadata
100    pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
101        self.template.metadata.insert(key.into(), value.into());
102        self
103    }
104
105    /// Build the template
106    pub fn build(self) -> EmailTemplate {
107        self.template
108    }
109}
110
111/// Helper trait for serializable contexts
112pub trait IntoTemplateContext {
113    fn into_context(self) -> Result<TemplateContext, EmailError>;
114}
115
116impl<T: Serialize> IntoTemplateContext for T {
117    fn into_context(self) -> Result<TemplateContext, EmailError> {
118        let value = serde_json::to_value(self)?;
119        match value {
120            serde_json::Value::Object(map) => {
121                Ok(map.into_iter().collect())
122            }
123            _ => {
124                let mut context = TemplateContext::new();
125                context.insert("data".to_string(), value);
126                Ok(context)
127            }
128        }
129    }
130}
131
132/// Helper for TemplateContext to avoid blanket implementation conflict
133pub fn template_context_into_context(context: TemplateContext) -> Result<TemplateContext, EmailError> {
134    Ok(context)
135}
136
137/// Template debug information for troubleshooting
138#[derive(Debug, Clone)]
139pub struct TemplateDebugInfo {
140    /// Template name
141    pub name: String,
142    /// Whether template has HTML content
143    pub has_html: bool,
144    /// Whether template has text content
145    pub has_text: bool,
146    /// Whether template has subject content
147    pub has_subject: bool,
148    /// Layout name if any
149    pub layout: Option<String>,
150    /// Template metadata
151    pub metadata: HashMap<String, String>,
152    /// HTML template content (for debugging)
153    pub html_content: Option<String>,
154    /// Text template content (for debugging)
155    pub text_content: Option<String>,
156    /// Subject template content (for debugging)
157    pub subject_content: Option<String>,
158}
159
160/// Extension methods for Email to work with templates
161impl Email {
162    /// Create email from template
163    pub fn from_template(
164        engine: &TemplateEngine,
165        template: &EmailTemplate,
166        context: impl IntoTemplateContext,
167    ) -> Result<Self, EmailError> {
168        let context = context.into_context()?;
169        let rendered = template.render(engine, &context)?;
170
171        let mut email = Self::new();
172
173        if let Some(html) = rendered.html_content {
174            email = email.html_body(html);
175        }
176
177        if let Some(text) = rendered.text_content {
178            email = email.text_body(text);
179        }
180
181        email = email.subject(rendered.subject);
182
183        Ok(email)
184    }
185
186    /// Update email with rendered template content by name
187    pub fn with_template(
188        mut self,
189        engine: &TemplateEngine,
190        template_name: &str,
191        context: impl IntoTemplateContext,
192    ) -> Result<Self, EmailError> {
193        let context = context.into_context()?;
194        let rendered = engine.render_template_by_name(template_name, &context)?;
195
196        if let Some(html) = rendered.html_content {
197            self.html_body = Some(html);
198        }
199
200        if let Some(text) = rendered.text_content {
201            self.text_body = Some(text);
202        }
203
204        // Only update subject if it's currently empty
205        if self.subject.is_empty() {
206            self.subject = rendered.subject;
207        }
208
209        Ok(self)
210    }
211
212    /// Update email with rendered template content using EmailTemplate struct
213    pub fn with_template_struct(
214        mut self,
215        engine: &TemplateEngine,
216        template: &EmailTemplate,
217        context: impl IntoTemplateContext,
218    ) -> Result<Self, EmailError> {
219        let context = context.into_context()?;
220        let rendered = template.render(engine, &context)?;
221
222        if let Some(html) = rendered.html_content {
223            self.html_body = Some(html);
224        }
225
226        if let Some(text) = rendered.text_content {
227            self.text_body = Some(text);
228        }
229
230        // Only update subject if it's currently empty
231        if self.subject.is_empty() {
232            self.subject = rendered.subject;
233        }
234
235        Ok(self)
236    }
237}
238
239/// # Tera Template Engine Documentation
240/// 
241/// This module provides email templating using the Tera template engine.
242/// Tera uses Jinja2-like syntax and provides powerful features for email templates.
243/// 
244/// ## Quick Start
245/// 
246/// ```rust,ignore
247/// use elif_email::templates::{TemplateEngine, EmailTemplate, TemplateContext};
248/// use elif_email::config::TemplateConfig;
249/// 
250/// let config = TemplateConfig {
251///     templates_dir: "templates".to_string(),
252///     layouts_dir: "layouts".to_string(),
253///     partials_dir: "partials".to_string(),
254///     enable_cache: true,
255///     template_extension: ".html".to_string(),
256///     cache_size: Some(100),
257///     watch_files: false,
258/// };
259/// 
260/// let engine = TemplateEngine::new(config)?;
261/// let template = EmailTemplate::builder("welcome")
262///     .html_template("<h1>Welcome {{ user.name }}!</h1>")
263///     .subject_template("Welcome to {{ app_name }}")
264///     .build();
265/// ```
266/// 
267/// ## Available Filters
268/// 
269/// ### Date/Time Filters
270/// - `{{ date_value | format_date(format="%Y-%m-%d") }}`
271/// - `{{ datetime_value | format_datetime(format="%Y-%m-%d %H:%M:%S") }}`
272/// - `{{ "" | now(format="%Y-%m-%d %H:%M:%S") }}`
273/// 
274/// ### Tracking Filters
275/// - `{{ email_id | tracking_pixel(base_url="https://example.com") }}`
276/// - `{{ email_id | tracking_link(url="https://target.com", base_url="https://example.com") }}`
277/// 
278/// ### Formatting Filters
279/// - `{{ amount | currency(currency="USD") }}`
280/// - `{{ phone_number | phone(country="US") }}`
281/// - `{{ address_obj | address }}`
282/// 
283/// ### String Filters
284/// - `{{ text | url_encode }}`
285/// - `{{ text | truncate(length=100) }}` (use built-in)
286/// - `{{ text | title }}` (use built-in instead of capitalize)
287/// 
288/// ## Template Syntax
289/// 
290/// ### Conditionals
291/// Use Tera's native syntax instead of helpers:
292/// - `{% if value1 == value2 %}...{% endif %}` (instead of if_eq)
293/// - `{% if not condition %}...{% endif %}` (instead of unless)  
294/// - `{% if value1 > value2 %}...{% endif %}` (instead of if_gt)
295/// - `{% if value1 < value2 %}...{% endif %}` (instead of if_lt)
296/// 
297/// ### Loops
298/// Use Tera's native loop syntax:
299/// - `{% for item in items %}{{ loop.index0 }}: {{ item }}{% endfor %}` (instead of each_with_index)
300/// - `{% for i in range(start=0, end=10) %}{{ i }}{% endfor %}` (instead of range helper)
301/// 
302/// ### Migration from Handlebars
303/// 
304/// | Handlebars | Tera Equivalent |
305/// |------------|-----------------|
306/// | `{{#if condition}}` | `{% if condition %}` |
307/// | `{{#unless condition}}` | `{% if not condition %}` |
308/// | `{{#each items}}` | `{% for item in items %}` |
309/// | `{{@index}}` | `{{ loop.index0 }}` |
310/// | `{{#if_eq a b}}` | `{% if a == b %}` |
311/// | `{{capitalize text}}` | `{{ text \| title }}` |
312/// 
313/// All filters are registered in the TemplateEngine and available in templates.
314/// For full documentation, see [Tera docs](https://tera.netlify.app/docs/).
315pub struct TemplateDocumentation;