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;