gatekpr_email/templates/
loader.rs1use crate::error::{EmailError, Result};
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9use tokio::sync::RwLock;
10use tracing::{debug, info};
11
12pub struct TemplateLoader {
20 template_dir: PathBuf,
22 cache: Arc<RwLock<HashMap<String, String>>>,
24 use_embedded: bool,
26}
27
28impl TemplateLoader {
29 pub fn new(template_dir: impl AsRef<Path>) -> Self {
35 Self {
36 template_dir: template_dir.as_ref().to_path_buf(),
37 cache: Arc::new(RwLock::new(HashMap::new())),
38 use_embedded: false,
39 }
40 }
41
42 pub fn embedded() -> Self {
44 Self {
45 template_dir: PathBuf::new(),
46 cache: Arc::new(RwLock::new(HashMap::new())),
47 use_embedded: true,
48 }
49 }
50
51 pub async fn load(&self, name: &str) -> Result<String> {
61 {
63 let cache = self.cache.read().await;
64 if let Some(content) = cache.get(name) {
65 debug!(template = %name, "Using cached template");
66 return Ok(content.clone());
67 }
68 }
69
70 let content = if self.use_embedded {
72 self.load_embedded(name)?
73 } else {
74 self.load_from_file(name).await?
75 };
76
77 {
79 let mut cache = self.cache.write().await;
80 cache.insert(name.to_string(), content.clone());
81 }
82
83 info!(template = %name, "Template loaded and cached");
84 Ok(content)
85 }
86
87 async fn load_from_file(&self, name: &str) -> Result<String> {
89 let path = self.template_dir.join(format!("{}.mjml", name));
90
91 tokio::fs::read_to_string(&path)
92 .await
93 .map_err(|e| EmailError::TemplateNotFound(format!("{}: {}", path.display(), e)))
94 }
95
96 fn load_embedded(&self, name: &str) -> Result<String> {
98 match name {
100 "welcome" => Ok(include_str!("../../templates/welcome.mjml").to_string()),
101 "verify-email" => Ok(include_str!("../../templates/verify-email.mjml").to_string()),
102 "password-reset" => Ok(include_str!("../../templates/password-reset.mjml").to_string()),
103 "password-changed" => {
104 Ok(include_str!("../../templates/password-changed.mjml").to_string())
105 }
106 "api-key-generated" => {
107 Ok(include_str!("../../templates/api-key-generated.mjml").to_string())
108 }
109 "new-login" => Ok(include_str!("../../templates/new-login.mjml").to_string()),
110 "device-auth" => Ok(include_str!("../../templates/device-auth.mjml").to_string()),
111 "validation-complete" => {
112 Ok(include_str!("../../templates/validation-complete.mjml").to_string())
113 }
114 _ => Err(EmailError::TemplateNotFound(format!(
115 "Unknown embedded template: {}",
116 name
117 ))),
118 }
119 }
120
121 pub async fn clear_cache(&self) {
123 let mut cache = self.cache.write().await;
124 cache.clear();
125 info!("Template cache cleared");
126 }
127
128 pub async fn preload_all(&self) -> Result<()> {
130 let templates = [
131 "welcome",
132 "verify-email",
133 "password-reset",
134 "password-changed",
135 "api-key-generated",
136 "new-login",
137 "device-auth",
138 "validation-complete",
139 ];
140
141 for name in templates {
142 self.load(name).await?;
143 }
144
145 info!(count = templates.len(), "All templates preloaded");
146 Ok(())
147 }
148}
149
150impl Default for TemplateLoader {
151 fn default() -> Self {
152 Self::embedded()
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[tokio::test]
161 async fn test_cache_operations() {
162 let loader = TemplateLoader::new("/tmp/nonexistent");
163
164 {
166 let cache = loader.cache.read().await;
167 assert!(cache.is_empty());
168 }
169
170 loader.clear_cache().await;
172 }
173}