rustack_ses_core/
template.rs1use dashmap::{DashMap, mapref::entry::Entry};
7use rustack_ses_model::{
8 error::{SesError, SesErrorCode},
9 types::{Template, TemplateMetadata},
10};
11
12#[derive(Debug)]
17pub struct TemplateStore {
18 templates: DashMap<String, StoredTemplate>,
19}
20
21#[derive(Debug, Clone)]
23pub struct StoredTemplate {
24 pub template: Template,
26 pub created_timestamp: chrono::DateTime<chrono::Utc>,
28}
29
30impl Default for TemplateStore {
31 fn default() -> Self {
32 Self::new()
33 }
34}
35
36impl TemplateStore {
37 #[must_use]
39 pub fn new() -> Self {
40 Self {
41 templates: DashMap::new(),
42 }
43 }
44
45 pub fn create(&self, template: Template) -> Result<(), SesError> {
51 let name = template.template_name.clone();
52 match self.templates.entry(name) {
53 Entry::Occupied(_) => Err(SesError::with_message(
54 SesErrorCode::AlreadyExistsException,
55 format!("Template {} already exists.", template.template_name),
56 )),
57 Entry::Vacant(e) => {
58 e.insert(StoredTemplate {
59 template,
60 created_timestamp: chrono::Utc::now(),
61 });
62 Ok(())
63 }
64 }
65 }
66
67 pub fn get(&self, name: &str) -> Result<Template, SesError> {
73 self.templates
74 .get(name)
75 .map(|entry| entry.template.clone())
76 .ok_or_else(|| {
77 SesError::with_message(
78 SesErrorCode::TemplateDoesNotExistException,
79 format!("Template {name} does not exist."),
80 )
81 })
82 }
83
84 pub fn update(&self, template: Template) -> Result<(), SesError> {
90 let name = template.template_name.clone();
91 let mut entry = self.templates.get_mut(&name).ok_or_else(|| {
92 SesError::with_message(
93 SesErrorCode::TemplateDoesNotExistException,
94 format!("Template {name} does not exist."),
95 )
96 })?;
97 entry.template = template;
98 Ok(())
99 }
100
101 pub fn delete(&self, name: &str) {
103 self.templates.remove(name);
104 }
105
106 #[must_use]
108 pub fn list(&self) -> Vec<TemplateMetadata> {
109 self.templates
110 .iter()
111 .map(|entry| TemplateMetadata {
112 name: Some(entry.template.template_name.clone()),
113 created_timestamp: Some(entry.created_timestamp),
114 })
115 .collect()
116 }
117}
118
119pub fn render_template(template_text: &str, template_data: &str) -> Result<String, SesError> {
130 let data: serde_json::Value = serde_json::from_str(template_data).map_err(|e| {
131 SesError::with_message(
132 SesErrorCode::InvalidTemplateException,
133 format!("Invalid template data JSON: {e}"),
134 )
135 })?;
136
137 let data_map = data.as_object().ok_or_else(|| {
138 SesError::with_message(
139 SesErrorCode::InvalidTemplateException,
140 "Template data must be a JSON object",
141 )
142 })?;
143
144 let mut result = template_text.to_owned();
145 for (key, value) in data_map {
146 let placeholder = format!("{{{{{key}}}}}");
147 let replacement = match value {
148 serde_json::Value::String(s) => s.clone(),
149 serde_json::Value::Null => String::new(),
150 other => other.to_string(),
151 };
152 result = result.replace(&placeholder, &replacement);
153 }
154
155 Ok(result)
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 fn make_template(name: &str) -> Template {
163 Template {
164 template_name: name.to_owned(),
165 subject_part: Some("Hello {{name}}".to_owned()),
166 text_part: Some("Dear {{name}}, welcome!".to_owned()),
167 html_part: Some("<p>Dear {{name}}, welcome!</p>".to_owned()),
168 }
169 }
170
171 #[test]
172 fn test_should_create_and_get_template() {
173 let store = TemplateStore::new();
174 let tmpl = make_template("welcome");
175 store.create(tmpl).unwrap_or_default();
176 let retrieved = store.get("welcome");
177 assert!(retrieved.is_ok());
178 assert_eq!(retrieved.unwrap_or_default().template_name, "welcome");
179 }
180
181 #[test]
182 fn test_should_reject_duplicate_template() {
183 let store = TemplateStore::new();
184 store.create(make_template("dup")).unwrap_or_default();
185 let result = store.create(make_template("dup"));
186 assert!(result.is_err());
187 }
188
189 #[test]
190 fn test_should_update_template() {
191 let store = TemplateStore::new();
192 store.create(make_template("upd")).unwrap_or_default();
193 let mut updated = make_template("upd");
194 updated.subject_part = Some("Updated {{name}}".to_owned());
195 store.update(updated).unwrap_or_default();
196 let retrieved = store.get("upd").unwrap_or_default();
197 assert_eq!(retrieved.subject_part, Some("Updated {{name}}".to_owned()));
198 }
199
200 #[test]
201 fn test_should_return_error_on_update_nonexistent() {
202 let store = TemplateStore::new();
203 let result = store.update(make_template("nope"));
204 assert!(result.is_err());
205 }
206
207 #[test]
208 fn test_should_delete_template() {
209 let store = TemplateStore::new();
210 store.create(make_template("del")).unwrap_or_default();
211 store.delete("del");
212 assert!(store.get("del").is_err());
213 }
214
215 #[test]
216 fn test_should_list_templates() {
217 let store = TemplateStore::new();
218 store.create(make_template("a")).unwrap_or_default();
219 store.create(make_template("b")).unwrap_or_default();
220 let list = store.list();
221 assert_eq!(list.len(), 2);
222 }
223
224 #[test]
225 fn test_should_render_simple_template() {
226 let result = render_template("Hello {{name}}", r#"{"name":"World"}"#);
227 assert_eq!(result.unwrap_or_default(), "Hello World");
228 }
229
230 #[test]
231 fn test_should_render_multiple_variables() {
232 let result = render_template(
233 "{{greeting}} {{name}}!",
234 r#"{"greeting":"Hi","name":"Alice"}"#,
235 );
236 assert_eq!(result.unwrap_or_default(), "Hi Alice!");
237 }
238
239 #[test]
240 fn test_should_leave_unmatched_placeholders() {
241 let result = render_template("Hello {{name}} {{missing}}", r#"{"name":"World"}"#);
242 assert_eq!(result.unwrap_or_default(), "Hello World {{missing}}");
243 }
244
245 #[test]
246 fn test_should_handle_null_values() {
247 let result = render_template("Value: {{x}}", r#"{"x":null}"#);
248 assert_eq!(result.unwrap_or_default(), "Value: ");
249 }
250
251 #[test]
252 fn test_should_handle_numeric_values() {
253 let result = render_template("Count: {{n}}", r#"{"n":42}"#);
254 assert_eq!(result.unwrap_or_default(), "Count: 42");
255 }
256
257 #[test]
258 fn test_should_reject_invalid_json() {
259 let result = render_template("Hello", "not json");
260 assert!(result.is_err());
261 }
262
263 #[test]
264 fn test_should_reject_non_object_json() {
265 let result = render_template("Hello", "[1,2,3]");
266 assert!(result.is_err());
267 }
268
269 #[test]
270 fn test_should_handle_empty_data() {
271 let result = render_template("Hello {{name}}", "{}");
272 assert_eq!(result.unwrap_or_default(), "Hello {{name}}");
273 }
274}