1use anyhow::{Context as _, Result};
2use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase, ToTitleCase};
3use romance_templates::Templates;
4use std::collections::HashMap;
5use tera::{Context, Tera, Value};
6
7pub struct TemplateEngine {
8 tera: Tera,
9}
10
11impl TemplateEngine {
12 pub fn new() -> Result<Self> {
13 let mut tera = Tera::default();
14
15 for file in Templates::iter() {
17 let path = file.as_ref();
18 if !path.ends_with(".tera") {
19 continue;
20 }
21 if let Some(content) = Templates::get(path) {
22 let content_str = std::str::from_utf8(content.data.as_ref())?;
23 tera.add_raw_template(path, content_str)?;
24 }
25 }
26
27 tera.register_filter("snake_case", snake_case_filter);
29 tera.register_filter("pascal_case", pascal_case_filter);
30 tera.register_filter("camel_case", camel_case_filter);
31 tera.register_filter("plural", plural_filter);
32 tera.register_filter("title_case", title_case_filter);
33 tera.register_filter("rust_ident", rust_ident_filter);
34
35 Ok(TemplateEngine { tera })
36 }
37
38 pub fn render(&self, template_name: &str, context: &Context) -> Result<String> {
39 let result = self
40 .tera
41 .render(template_name, context)
42 .with_context(|| format!("Failed to render template '{}'", template_name))?;
43 Ok(result)
44 }
45
46 pub fn get_raw(&self, path: &str) -> Result<String> {
48 let content = Templates::get(path)
49 .with_context(|| format!("Embedded file '{}' not found", path))?;
50 let s = std::str::from_utf8(content.data.as_ref())
51 .with_context(|| format!("Invalid UTF-8 in '{}'", path))?;
52 Ok(s.to_string())
53 }
54}
55
56fn snake_case_filter(
57 value: &Value,
58 _args: &HashMap<String, Value>,
59) -> tera::Result<Value> {
60 match value.as_str() {
61 Some(s) => Ok(Value::String(s.to_snake_case())),
62 None => Err(tera::Error::msg("snake_case filter expects a string")),
63 }
64}
65
66fn pascal_case_filter(
67 value: &Value,
68 _args: &HashMap<String, Value>,
69) -> tera::Result<Value> {
70 match value.as_str() {
71 Some(s) => Ok(Value::String(s.to_pascal_case())),
72 None => Err(tera::Error::msg("pascal_case filter expects a string")),
73 }
74}
75
76fn camel_case_filter(
77 value: &Value,
78 _args: &HashMap<String, Value>,
79) -> tera::Result<Value> {
80 match value.as_str() {
81 Some(s) => Ok(Value::String(s.to_lower_camel_case())),
82 None => Err(tera::Error::msg("camel_case filter expects a string")),
83 }
84}
85
86fn plural_filter(
87 value: &Value,
88 _args: &HashMap<String, Value>,
89) -> tera::Result<Value> {
90 match value.as_str() {
91 Some(s) => Ok(Value::String(crate::utils::pluralize(s))),
92 None => Err(tera::Error::msg("plural filter expects a string")),
93 }
94}
95
96fn title_case_filter(
97 value: &Value,
98 _args: &HashMap<String, Value>,
99) -> tera::Result<Value> {
100 match value.as_str() {
101 Some(s) => Ok(Value::String(s.to_title_case())),
102 None => Err(tera::Error::msg("title_case filter expects a string")),
103 }
104}
105
106fn rust_ident_filter(
107 value: &Value,
108 _args: &HashMap<String, Value>,
109) -> tera::Result<Value> {
110 match value.as_str() {
111 Some(s) => Ok(Value::String(crate::utils::rust_ident(s))),
112 None => Err(tera::Error::msg("rust_ident filter expects a string")),
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 fn val(s: &str) -> Value {
123 Value::String(s.to_string())
124 }
125 fn empty_args() -> HashMap<String, Value> {
126 HashMap::new()
127 }
128
129 #[test]
130 fn snake_case_filter_works() {
131 let result = snake_case_filter(&val("ProductCategory"), &empty_args()).unwrap();
132 assert_eq!(result.as_str().unwrap(), "product_category");
133 }
134
135 #[test]
136 fn snake_case_filter_single_word() {
137 let result = snake_case_filter(&val("Post"), &empty_args()).unwrap();
138 assert_eq!(result.as_str().unwrap(), "post");
139 }
140
141 #[test]
142 fn pascal_case_filter_works() {
143 let result = pascal_case_filter(&val("product_category"), &empty_args()).unwrap();
144 assert_eq!(result.as_str().unwrap(), "ProductCategory");
145 }
146
147 #[test]
148 fn pascal_case_filter_from_snake() {
149 let result = pascal_case_filter(&val("blog_post"), &empty_args()).unwrap();
150 assert_eq!(result.as_str().unwrap(), "BlogPost");
151 }
152
153 #[test]
154 fn camel_case_filter_works() {
155 let result = camel_case_filter(&val("ProductCategory"), &empty_args()).unwrap();
156 assert_eq!(result.as_str().unwrap(), "productCategory");
157 }
158
159 #[test]
160 fn camel_case_filter_from_snake() {
161 let result = camel_case_filter(&val("blog_post"), &empty_args()).unwrap();
162 assert_eq!(result.as_str().unwrap(), "blogPost");
163 }
164
165 #[test]
166 fn plural_filter_regular() {
167 let result = plural_filter(&val("post"), &empty_args()).unwrap();
168 assert_eq!(result.as_str().unwrap(), "posts");
169 }
170
171 #[test]
172 fn plural_filter_category() {
173 let result = plural_filter(&val("Category"), &empty_args()).unwrap();
174 assert_eq!(result.as_str().unwrap(), "Categories");
175 }
176
177 #[test]
178 fn plural_filter_box() {
179 let result = plural_filter(&val("box"), &empty_args()).unwrap();
180 assert_eq!(result.as_str().unwrap(), "boxes");
181 }
182
183 #[test]
184 fn title_case_filter_works() {
185 let result = title_case_filter(&val("product_category"), &empty_args()).unwrap();
186 assert_eq!(result.as_str().unwrap(), "Product Category");
187 }
188
189 #[test]
190 fn rust_ident_filter_works() {
191 let result = rust_ident_filter(&val("type"), &empty_args()).unwrap();
192 assert_eq!(result.as_str().unwrap(), "r#type");
193 }
194
195 #[test]
196 fn rust_ident_filter_non_reserved() {
197 let result = rust_ident_filter(&val("title"), &empty_args()).unwrap();
198 assert_eq!(result.as_str().unwrap(), "title");
199 }
200
201 #[test]
204 fn snake_case_filter_rejects_non_string() {
205 let num = Value::Number(serde_json::Number::from(42));
206 assert!(snake_case_filter(&num, &empty_args()).is_err());
207 }
208
209 #[test]
210 fn pascal_case_filter_rejects_non_string() {
211 let num = Value::Number(serde_json::Number::from(42));
212 assert!(pascal_case_filter(&num, &empty_args()).is_err());
213 }
214
215 #[test]
216 fn camel_case_filter_rejects_non_string() {
217 let num = Value::Number(serde_json::Number::from(42));
218 assert!(camel_case_filter(&num, &empty_args()).is_err());
219 }
220
221 #[test]
222 fn plural_filter_rejects_non_string() {
223 let num = Value::Number(serde_json::Number::from(42));
224 assert!(plural_filter(&num, &empty_args()).is_err());
225 }
226
227 #[test]
230 fn template_engine_creates_successfully() {
231 let _engine = TemplateEngine::new().unwrap();
232 }
233
234 #[test]
237 fn render_inline_template_with_snake_case() {
238 let mut tera = Tera::default();
239 tera.register_filter("snake_case", snake_case_filter);
240 tera.add_raw_template("test", "{{ name | snake_case }}").unwrap();
241 let mut ctx = Context::new();
242 ctx.insert("name", "ProductCategory");
243 let result = tera.render("test", &ctx).unwrap();
244 assert_eq!(result, "product_category");
245 }
246
247 #[test]
248 fn render_inline_template_with_plural() {
249 let mut tera = Tera::default();
250 tera.register_filter("plural", plural_filter);
251 tera.add_raw_template("test", "{{ name | plural }}").unwrap();
252 let mut ctx = Context::new();
253 ctx.insert("name", "Category");
254 let result = tera.render("test", &ctx).unwrap();
255 assert_eq!(result, "Categories");
256 }
257
258 #[test]
259 fn render_inline_template_with_multiple_filters() {
260 let mut tera = Tera::default();
261 tera.register_filter("snake_case", snake_case_filter);
262 tera.register_filter("pascal_case", pascal_case_filter);
263 tera.register_filter("camel_case", camel_case_filter);
264 tera.add_raw_template(
265 "test",
266 "snake={{ name | snake_case }} pascal={{ name | pascal_case }} camel={{ name | camel_case }}",
267 )
268 .unwrap();
269 let mut ctx = Context::new();
270 ctx.insert("name", "blog_post");
271 let result = tera.render("test", &ctx).unwrap();
272 assert_eq!(result, "snake=blog_post pascal=BlogPost camel=blogPost");
273 }
274
275 #[test]
276 fn render_inline_template_with_context_variables() {
277 let mut tera = Tera::default();
278 tera.add_raw_template("test", "Hello {{ name }}, port {{ port }}").unwrap();
279 let mut ctx = Context::new();
280 ctx.insert("name", "Romance");
281 ctx.insert("port", &3000);
282 let result = tera.render("test", &ctx).unwrap();
283 assert_eq!(result, "Hello Romance, port 3000");
284 }
285}