use dashmap::{DashMap, mapref::entry::Entry};
use rustack_ses_model::{
error::{SesError, SesErrorCode},
types::{Template, TemplateMetadata},
};
#[derive(Debug)]
pub struct TemplateStore {
templates: DashMap<String, StoredTemplate>,
}
#[derive(Debug, Clone)]
pub struct StoredTemplate {
pub template: Template,
pub created_timestamp: chrono::DateTime<chrono::Utc>,
}
impl Default for TemplateStore {
fn default() -> Self {
Self::new()
}
}
impl TemplateStore {
#[must_use]
pub fn new() -> Self {
Self {
templates: DashMap::new(),
}
}
pub fn create(&self, template: Template) -> Result<(), SesError> {
let name = template.template_name.clone();
match self.templates.entry(name) {
Entry::Occupied(_) => Err(SesError::with_message(
SesErrorCode::AlreadyExistsException,
format!("Template {} already exists.", template.template_name),
)),
Entry::Vacant(e) => {
e.insert(StoredTemplate {
template,
created_timestamp: chrono::Utc::now(),
});
Ok(())
}
}
}
pub fn get(&self, name: &str) -> Result<Template, SesError> {
self.templates
.get(name)
.map(|entry| entry.template.clone())
.ok_or_else(|| {
SesError::with_message(
SesErrorCode::TemplateDoesNotExistException,
format!("Template {name} does not exist."),
)
})
}
pub fn update(&self, template: Template) -> Result<(), SesError> {
let name = template.template_name.clone();
let mut entry = self.templates.get_mut(&name).ok_or_else(|| {
SesError::with_message(
SesErrorCode::TemplateDoesNotExistException,
format!("Template {name} does not exist."),
)
})?;
entry.template = template;
Ok(())
}
pub fn delete(&self, name: &str) {
self.templates.remove(name);
}
#[must_use]
pub fn list(&self) -> Vec<TemplateMetadata> {
self.templates
.iter()
.map(|entry| TemplateMetadata {
name: Some(entry.template.template_name.clone()),
created_timestamp: Some(entry.created_timestamp),
})
.collect()
}
}
pub fn render_template(template_text: &str, template_data: &str) -> Result<String, SesError> {
let data: serde_json::Value = serde_json::from_str(template_data).map_err(|e| {
SesError::with_message(
SesErrorCode::InvalidTemplateException,
format!("Invalid template data JSON: {e}"),
)
})?;
let data_map = data.as_object().ok_or_else(|| {
SesError::with_message(
SesErrorCode::InvalidTemplateException,
"Template data must be a JSON object",
)
})?;
let mut result = template_text.to_owned();
for (key, value) in data_map {
let placeholder = format!("{{{{{key}}}}}");
let replacement = match value {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Null => String::new(),
other => other.to_string(),
};
result = result.replace(&placeholder, &replacement);
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_template(name: &str) -> Template {
Template {
template_name: name.to_owned(),
subject_part: Some("Hello {{name}}".to_owned()),
text_part: Some("Dear {{name}}, welcome!".to_owned()),
html_part: Some("<p>Dear {{name}}, welcome!</p>".to_owned()),
}
}
#[test]
fn test_should_create_and_get_template() {
let store = TemplateStore::new();
let tmpl = make_template("welcome");
store.create(tmpl).unwrap_or_default();
let retrieved = store.get("welcome");
assert!(retrieved.is_ok());
assert_eq!(retrieved.unwrap_or_default().template_name, "welcome");
}
#[test]
fn test_should_reject_duplicate_template() {
let store = TemplateStore::new();
store.create(make_template("dup")).unwrap_or_default();
let result = store.create(make_template("dup"));
assert!(result.is_err());
}
#[test]
fn test_should_update_template() {
let store = TemplateStore::new();
store.create(make_template("upd")).unwrap_or_default();
let mut updated = make_template("upd");
updated.subject_part = Some("Updated {{name}}".to_owned());
store.update(updated).unwrap_or_default();
let retrieved = store.get("upd").unwrap_or_default();
assert_eq!(retrieved.subject_part, Some("Updated {{name}}".to_owned()));
}
#[test]
fn test_should_return_error_on_update_nonexistent() {
let store = TemplateStore::new();
let result = store.update(make_template("nope"));
assert!(result.is_err());
}
#[test]
fn test_should_delete_template() {
let store = TemplateStore::new();
store.create(make_template("del")).unwrap_or_default();
store.delete("del");
assert!(store.get("del").is_err());
}
#[test]
fn test_should_list_templates() {
let store = TemplateStore::new();
store.create(make_template("a")).unwrap_or_default();
store.create(make_template("b")).unwrap_or_default();
let list = store.list();
assert_eq!(list.len(), 2);
}
#[test]
fn test_should_render_simple_template() {
let result = render_template("Hello {{name}}", r#"{"name":"World"}"#);
assert_eq!(result.unwrap_or_default(), "Hello World");
}
#[test]
fn test_should_render_multiple_variables() {
let result = render_template(
"{{greeting}} {{name}}!",
r#"{"greeting":"Hi","name":"Alice"}"#,
);
assert_eq!(result.unwrap_or_default(), "Hi Alice!");
}
#[test]
fn test_should_leave_unmatched_placeholders() {
let result = render_template("Hello {{name}} {{missing}}", r#"{"name":"World"}"#);
assert_eq!(result.unwrap_or_default(), "Hello World {{missing}}");
}
#[test]
fn test_should_handle_null_values() {
let result = render_template("Value: {{x}}", r#"{"x":null}"#);
assert_eq!(result.unwrap_or_default(), "Value: ");
}
#[test]
fn test_should_handle_numeric_values() {
let result = render_template("Count: {{n}}", r#"{"n":42}"#);
assert_eq!(result.unwrap_or_default(), "Count: 42");
}
#[test]
fn test_should_reject_invalid_json() {
let result = render_template("Hello", "not json");
assert!(result.is_err());
}
#[test]
fn test_should_reject_non_object_json() {
let result = render_template("Hello", "[1,2,3]");
assert!(result.is_err());
}
#[test]
fn test_should_handle_empty_data() {
let result = render_template("Hello {{name}}", "{}");
assert_eq!(result.unwrap_or_default(), "Hello {{name}}");
}
}