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