use crate::error::ChorusError;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Template {
pub slug: String,
pub name: String,
pub subject: String,
pub html_body: String,
pub text_body: String,
pub variables: Vec<String>,
}
impl Template {
pub fn render(
&self,
variables: &HashMap<String, String>,
) -> Result<RenderedTemplate, ChorusError> {
let subject = Self::replace_vars(&self.subject, variables);
let html_body = Self::replace_vars(&self.html_body, variables);
let text_body = Self::replace_vars(&self.text_body, variables);
Ok(RenderedTemplate {
subject,
html_body,
text_body,
})
}
fn replace_vars(text: &str, variables: &HashMap<String, String>) -> String {
let mut result = text.to_string();
for (key, value) in variables {
result = result.replace(&format!("{{{{{}}}}}", key), value);
}
result
}
}
#[derive(Debug, Clone)]
pub struct RenderedTemplate {
pub subject: String,
pub html_body: String,
pub text_body: String,
}
#[cfg(test)]
mod tests {
use super::*;
fn test_template() -> Template {
Template {
slug: "otp".to_string(),
name: "OTP Email".to_string(),
subject: "Your {{app_name}} code".to_string(),
html_body: "<p>Code: <strong>{{code}}</strong>. Expires in {{expire}} min.</p>"
.to_string(),
text_body: "Code: {{code}}. Expires in {{expire}} min.".to_string(),
variables: vec![
"code".to_string(),
"app_name".to_string(),
"expire".to_string(),
],
}
}
#[test]
fn render_replaces_all_variables() {
let tmpl = test_template();
let mut vars = HashMap::new();
vars.insert("code".to_string(), "123456".to_string());
vars.insert("app_name".to_string(), "Orbit".to_string());
vars.insert("expire".to_string(), "5".to_string());
let rendered = tmpl.render(&vars).unwrap();
assert_eq!(rendered.subject, "Your Orbit code");
assert!(rendered.html_body.contains("<strong>123456</strong>"));
assert!(rendered.text_body.contains("123456"));
assert!(rendered.text_body.contains("5 min"));
}
#[test]
fn render_leaves_unknown_vars_as_is() {
let tmpl = test_template();
let vars = HashMap::new();
let rendered = tmpl.render(&vars).unwrap();
assert!(rendered.subject.contains("{{app_name}}"));
}
#[test]
fn render_handles_repeated_variable() {
let tmpl = Template {
slug: "test".into(),
name: "Test".into(),
subject: "{{code}} is your code {{code}}".into(),
html_body: "".into(),
text_body: "".into(),
variables: vec!["code".into()],
};
let mut vars = HashMap::new();
vars.insert("code".into(), "999".into());
let rendered = tmpl.render(&vars).unwrap();
assert_eq!(rendered.subject, "999 is your code 999");
}
#[test]
fn render_empty_template() {
let tmpl = Template {
slug: "empty".into(),
name: "Empty".into(),
subject: "".into(),
html_body: "".into(),
text_body: "".into(),
variables: vec![],
};
let rendered = tmpl.render(&HashMap::new()).unwrap();
assert_eq!(rendered.subject, "");
assert_eq!(rendered.html_body, "");
assert_eq!(rendered.text_body, "");
}
#[test]
fn render_no_placeholders() {
let tmpl = Template {
slug: "plain".into(),
name: "Plain".into(),
subject: "Welcome!".into(),
html_body: "<p>Hello world</p>".into(),
text_body: "Hello world".into(),
variables: vec![],
};
let rendered = tmpl.render(&HashMap::new()).unwrap();
assert_eq!(rendered.subject, "Welcome!");
assert_eq!(rendered.text_body, "Hello world");
}
#[test]
fn render_with_special_characters_in_value() {
let tmpl = Template {
slug: "test".into(),
name: "Test".into(),
subject: "Hello {{name}}".into(),
html_body: "<p>{{name}}</p>".into(),
text_body: "{{name}}".into(),
variables: vec!["name".into()],
};
let mut vars = HashMap::new();
vars.insert("name".into(), "O'Brien <script>".into());
let rendered = tmpl.render(&vars).unwrap();
assert_eq!(rendered.subject, "Hello O'Brien <script>");
assert!(rendered.html_body.contains("O'Brien <script>"));
}
}