1use crate::error::ChorusError;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct Template {
7 pub slug: String,
8 pub name: String,
9 pub subject: String,
10 pub html_body: String,
11 pub text_body: String,
12 pub variables: Vec<String>,
13}
14
15impl Template {
16 pub fn render(
18 &self,
19 variables: &HashMap<String, String>,
20 ) -> Result<RenderedTemplate, ChorusError> {
21 let subject = Self::replace_vars(&self.subject, variables);
22 let html_body = Self::replace_vars(&self.html_body, variables);
23 let text_body = Self::replace_vars(&self.text_body, variables);
24
25 Ok(RenderedTemplate {
26 subject,
27 html_body,
28 text_body,
29 })
30 }
31
32 fn replace_vars(text: &str, variables: &HashMap<String, String>) -> String {
33 let mut result = text.to_string();
34 for (key, value) in variables {
35 result = result.replace(&format!("{{{{{}}}}}", key), value);
36 }
37 result
38 }
39}
40
41#[derive(Debug, Clone)]
42pub struct RenderedTemplate {
43 pub subject: String,
44 pub html_body: String,
45 pub text_body: String,
46}
47
48#[cfg(test)]
49mod tests {
50 use super::*;
51
52 fn test_template() -> Template {
53 Template {
54 slug: "otp".to_string(),
55 name: "OTP Email".to_string(),
56 subject: "Your {{app_name}} code".to_string(),
57 html_body: "<p>Code: <strong>{{code}}</strong>. Expires in {{expire}} min.</p>"
58 .to_string(),
59 text_body: "Code: {{code}}. Expires in {{expire}} min.".to_string(),
60 variables: vec![
61 "code".to_string(),
62 "app_name".to_string(),
63 "expire".to_string(),
64 ],
65 }
66 }
67
68 #[test]
69 fn render_replaces_all_variables() {
70 let tmpl = test_template();
71 let mut vars = HashMap::new();
72 vars.insert("code".to_string(), "123456".to_string());
73 vars.insert("app_name".to_string(), "Orbit".to_string());
74 vars.insert("expire".to_string(), "5".to_string());
75
76 let rendered = tmpl.render(&vars).unwrap();
77 assert_eq!(rendered.subject, "Your Orbit code");
78 assert!(rendered.html_body.contains("<strong>123456</strong>"));
79 assert!(rendered.text_body.contains("123456"));
80 assert!(rendered.text_body.contains("5 min"));
81 }
82
83 #[test]
84 fn render_leaves_unknown_vars_as_is() {
85 let tmpl = test_template();
86 let vars = HashMap::new();
87 let rendered = tmpl.render(&vars).unwrap();
88 assert!(rendered.subject.contains("{{app_name}}"));
89 }
90
91 #[test]
92 fn render_handles_repeated_variable() {
93 let tmpl = Template {
94 slug: "test".into(),
95 name: "Test".into(),
96 subject: "{{code}} is your code {{code}}".into(),
97 html_body: "".into(),
98 text_body: "".into(),
99 variables: vec!["code".into()],
100 };
101 let mut vars = HashMap::new();
102 vars.insert("code".into(), "999".into());
103 let rendered = tmpl.render(&vars).unwrap();
104 assert_eq!(rendered.subject, "999 is your code 999");
105 }
106
107 #[test]
108 fn render_empty_template() {
109 let tmpl = Template {
110 slug: "empty".into(),
111 name: "Empty".into(),
112 subject: "".into(),
113 html_body: "".into(),
114 text_body: "".into(),
115 variables: vec![],
116 };
117 let rendered = tmpl.render(&HashMap::new()).unwrap();
118 assert_eq!(rendered.subject, "");
119 assert_eq!(rendered.html_body, "");
120 assert_eq!(rendered.text_body, "");
121 }
122
123 #[test]
124 fn render_no_placeholders() {
125 let tmpl = Template {
126 slug: "plain".into(),
127 name: "Plain".into(),
128 subject: "Welcome!".into(),
129 html_body: "<p>Hello world</p>".into(),
130 text_body: "Hello world".into(),
131 variables: vec![],
132 };
133 let rendered = tmpl.render(&HashMap::new()).unwrap();
134 assert_eq!(rendered.subject, "Welcome!");
135 assert_eq!(rendered.text_body, "Hello world");
136 }
137
138 #[test]
139 fn render_with_special_characters_in_value() {
140 let tmpl = Template {
141 slug: "test".into(),
142 name: "Test".into(),
143 subject: "Hello {{name}}".into(),
144 html_body: "<p>{{name}}</p>".into(),
145 text_body: "{{name}}".into(),
146 variables: vec!["name".into()],
147 };
148 let mut vars = HashMap::new();
149 vars.insert("name".into(), "O'Brien <script>".into());
150 let rendered = tmpl.render(&vars).unwrap();
151 assert_eq!(rendered.subject, "Hello O'Brien <script>");
152 assert!(rendered.html_body.contains("O'Brien <script>"));
153 }
154}