standout_render/template/
simple.rs1use std::collections::HashMap;
43
44use crate::error::RenderError;
45
46use super::TemplateEngine;
47
48pub struct SimpleEngine {
79 templates: HashMap<String, String>,
80}
81
82impl SimpleEngine {
83 pub fn new() -> Self {
85 Self {
86 templates: HashMap::new(),
87 }
88 }
89
90 fn resolve_path<'a>(value: &'a serde_json::Value, path: &str) -> Option<&'a serde_json::Value> {
97 let mut current = value;
98
99 for part in path.split('.') {
100 current = match current {
101 serde_json::Value::Object(map) => map.get(part)?,
102 serde_json::Value::Array(arr) => {
103 let index: usize = part.parse().ok()?;
104 arr.get(index)?
105 }
106 _ => return None,
107 };
108 }
109
110 Some(current)
111 }
112
113 fn format_value(value: &serde_json::Value) -> String {
115 match value {
116 serde_json::Value::String(s) => s.clone(),
117 serde_json::Value::Number(n) => n.to_string(),
118 serde_json::Value::Bool(b) => b.to_string(),
119 serde_json::Value::Null => String::new(),
120 serde_json::Value::Array(_) | serde_json::Value::Object(_) => value.to_string(),
122 }
123 }
124
125 fn render_impl(
127 &self,
128 template: &str,
129 data: &serde_json::Value,
130 context: Option<&HashMap<String, serde_json::Value>>,
131 ) -> Result<String, RenderError> {
132 let mut result = String::with_capacity(template.len());
133 let mut chars = template.chars().peekable();
134
135 while let Some(ch) = chars.next() {
136 if ch == '{' {
137 if chars.peek() == Some(&'{') {
138 chars.next();
140 result.push('{');
141 } else {
142 let mut var_name = String::new();
144 let mut found_close = false;
145
146 for inner_ch in chars.by_ref() {
147 if inner_ch == '}' {
148 found_close = true;
149 break;
150 }
151 var_name.push(inner_ch);
152 }
153
154 if !found_close {
155 return Err(RenderError::TemplateError(format!(
156 "Unclosed variable substitution: {{{}",
157 var_name
158 )));
159 }
160
161 let var_name = var_name.trim();
162
163 if var_name.is_empty() {
164 return Err(RenderError::TemplateError(
165 "Empty variable name in template".to_string(),
166 ));
167 }
168
169 let value = if let Some(ctx) = context {
171 if !var_name.contains('.') {
173 if let Some(ctx_val) = ctx.get(var_name) {
174 Some(ctx_val)
175 } else {
176 Self::resolve_path(data, var_name)
177 }
178 } else {
179 let first_segment = var_name.split('.').next().unwrap_or(var_name);
181 if let Some(ctx_val) = ctx.get(first_segment) {
182 let rest = &var_name[first_segment.len()..];
184 if rest.is_empty() {
185 Some(ctx_val)
186 } else {
187 Self::resolve_path(ctx_val, &rest[1..]) }
189 } else {
190 Self::resolve_path(data, var_name)
191 }
192 }
193 } else {
194 Self::resolve_path(data, var_name)
195 };
196
197 match value {
198 Some(v) => result.push_str(&Self::format_value(v)),
199 None => {
200 result.push_str(&format!("{{{}}}", var_name));
202 }
203 }
204 }
205 } else if ch == '}' {
206 if chars.peek() == Some(&'}') {
207 chars.next();
209 result.push('}');
210 } else {
211 result.push(ch);
213 }
214 } else {
215 result.push(ch);
216 }
217 }
218
219 Ok(result)
220 }
221}
222
223impl Default for SimpleEngine {
224 fn default() -> Self {
225 Self::new()
226 }
227}
228
229impl TemplateEngine for SimpleEngine {
230 fn render_template(
231 &self,
232 template: &str,
233 data: &serde_json::Value,
234 ) -> Result<String, RenderError> {
235 self.render_impl(template, data, None)
236 }
237
238 fn add_template(&mut self, name: &str, source: &str) -> Result<(), RenderError> {
239 self.templates.insert(name.to_string(), source.to_string());
240 Ok(())
241 }
242
243 fn render_named(&self, name: &str, data: &serde_json::Value) -> Result<String, RenderError> {
244 let template = self
245 .templates
246 .get(name)
247 .ok_or_else(|| RenderError::TemplateNotFound(name.to_string()))?;
248 self.render_impl(template, data, None)
249 }
250
251 fn has_template(&self, name: &str) -> bool {
252 self.templates.contains_key(name)
253 }
254
255 fn render_with_context(
256 &self,
257 template: &str,
258 data: &serde_json::Value,
259 context: HashMap<String, serde_json::Value>,
260 ) -> Result<String, RenderError> {
261 self.render_impl(template, data, Some(&context))
262 }
263
264 fn supports_includes(&self) -> bool {
265 false
266 }
267
268 fn supports_filters(&self) -> bool {
269 false
270 }
271
272 fn supports_control_flow(&self) -> bool {
273 false
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use serde_json::json;
281
282 #[test]
283 fn test_simple_substitution() {
284 let engine = SimpleEngine::new();
285 let data = json!({"name": "World"});
286
287 let output = engine.render_template("Hello, {name}!", &data).unwrap();
288 assert_eq!(output, "Hello, World!");
289 }
290
291 #[test]
292 fn test_multiple_variables() {
293 let engine = SimpleEngine::new();
294 let data = json!({"first": "John", "last": "Doe"});
295
296 let output = engine.render_template("{first} {last}", &data).unwrap();
297 assert_eq!(output, "John Doe");
298 }
299
300 #[test]
301 fn test_nested_access() {
302 let engine = SimpleEngine::new();
303 let data = json!({
304 "user": {
305 "name": "Alice",
306 "profile": {
307 "email": "alice@example.com"
308 }
309 }
310 });
311
312 let output = engine
313 .render_template("Name: {user.name}, Email: {user.profile.email}", &data)
314 .unwrap();
315 assert_eq!(output, "Name: Alice, Email: alice@example.com");
316 }
317
318 #[test]
319 fn test_array_index() {
320 let engine = SimpleEngine::new();
321 let data = json!({
322 "items": ["first", "second", "third"]
323 });
324
325 let output = engine
326 .render_template("First: {items.0}, Third: {items.2}", &data)
327 .unwrap();
328 assert_eq!(output, "First: first, Third: third");
329 }
330
331 #[test]
332 fn test_array_object_access() {
333 let engine = SimpleEngine::new();
334 let data = json!({
335 "users": [
336 {"name": "Alice"},
337 {"name": "Bob"}
338 ]
339 });
340
341 let output = engine
342 .render_template("{users.0.name} and {users.1.name}", &data)
343 .unwrap();
344 assert_eq!(output, "Alice and Bob");
345 }
346
347 #[test]
348 fn test_number_values() {
349 let engine = SimpleEngine::new();
350 let data = json!({"count": 42, "price": 19.99});
351
352 let output = engine
353 .render_template("Count: {count}, Price: {price}", &data)
354 .unwrap();
355 assert_eq!(output, "Count: 42, Price: 19.99");
356 }
357
358 #[test]
359 fn test_boolean_values() {
360 let engine = SimpleEngine::new();
361 let data = json!({"active": true, "deleted": false});
362
363 let output = engine
364 .render_template("Active: {active}, Deleted: {deleted}", &data)
365 .unwrap();
366 assert_eq!(output, "Active: true, Deleted: false");
367 }
368
369 #[test]
370 fn test_null_value() {
371 let engine = SimpleEngine::new();
372 let data = json!({"value": null});
373
374 let output = engine.render_template("Value: {value}", &data).unwrap();
375 assert_eq!(output, "Value: ");
376 }
377
378 #[test]
379 fn test_escaped_braces() {
380 let engine = SimpleEngine::new();
381 let data = json!({"name": "test"});
382
383 let output = engine
384 .render_template("Use {{name}} for {name}", &data)
385 .unwrap();
386 assert_eq!(output, "Use {name} for test");
387 }
388
389 #[test]
390 fn test_escaped_closing_brace() {
391 let engine = SimpleEngine::new();
392 let data = json!({});
393
394 let output = engine
395 .render_template("JSON: {{\"key\": \"value\"}}", &data)
396 .unwrap();
397 assert_eq!(output, "JSON: {\"key\": \"value\"}");
398 }
399
400 #[test]
401 fn test_missing_variable() {
402 let engine = SimpleEngine::new();
403 let data = json!({"name": "test"});
404
405 let output = engine.render_template("Hello {missing}!", &data).unwrap();
406 assert_eq!(output, "Hello {missing}!");
408 }
409
410 #[test]
411 fn test_unclosed_variable() {
412 let engine = SimpleEngine::new();
413 let data = json!({});
414
415 let result = engine.render_template("Hello {name", &data);
416 assert!(result.is_err());
417 assert!(result.unwrap_err().to_string().contains("Unclosed"));
418 }
419
420 #[test]
421 fn test_empty_variable_name() {
422 let engine = SimpleEngine::new();
423 let data = json!({});
424
425 let result = engine.render_template("Hello {}!", &data);
426 assert!(result.is_err());
427 assert!(result.unwrap_err().to_string().contains("Empty variable"));
428 }
429
430 #[test]
431 fn test_whitespace_in_variable() {
432 let engine = SimpleEngine::new();
433 let data = json!({"name": "World"});
434
435 let output = engine.render_template("Hello { name }!", &data).unwrap();
437 assert_eq!(output, "Hello World!");
438 }
439
440 #[test]
441 fn test_named_template() {
442 let mut engine = SimpleEngine::new();
443 engine.add_template("greeting", "Hello, {name}!").unwrap();
444
445 let data = json!({"name": "World"});
446 let output = engine.render_named("greeting", &data).unwrap();
447 assert_eq!(output, "Hello, World!");
448 }
449
450 #[test]
451 fn test_named_template_not_found() {
452 let engine = SimpleEngine::new();
453 let data = json!({});
454
455 let result = engine.render_named("missing", &data);
456 assert!(result.is_err());
457 assert!(matches!(
458 result.unwrap_err(),
459 RenderError::TemplateNotFound(_)
460 ));
461 }
462
463 #[test]
464 fn test_has_template() {
465 let mut engine = SimpleEngine::new();
466 assert!(!engine.has_template("test"));
467
468 engine.add_template("test", "content").unwrap();
469 assert!(engine.has_template("test"));
470 }
471
472 #[test]
473 fn test_with_context() {
474 let engine = SimpleEngine::new();
475 let data = json!({"name": "Alice"});
476 let mut context = HashMap::new();
477 context.insert("version".to_string(), json!("1.0.0"));
478
479 let output = engine
480 .render_with_context("{name} v{version}", &data, context)
481 .unwrap();
482 assert_eq!(output, "Alice v1.0.0");
483 }
484
485 #[test]
486 fn test_context_data_precedence() {
487 let engine = SimpleEngine::new();
488 let data = json!({"value": "from_data"});
489 let mut context = HashMap::new();
490 context.insert("value".to_string(), json!("from_context"));
491
492 let output = engine
494 .render_with_context("{value}", &data, context)
495 .unwrap();
496 assert_eq!(output, "from_context");
497 }
498
499 #[test]
500 fn test_supports_flags() {
501 let engine = SimpleEngine::new();
502 assert!(!engine.supports_includes());
503 assert!(!engine.supports_filters());
504 assert!(!engine.supports_control_flow());
505 }
506
507 #[test]
508 fn test_no_template_logic() {
509 let engine = SimpleEngine::new();
510 let data = json!({"items": [1, 2, 3]});
511
512 let output = engine
515 .render_template("{% for i in items %}{{i}}{% endfor %}", &data)
516 .unwrap();
517 assert_eq!(output, "{% for i in items %}{i}{% endfor %}");
519 }
520
521 #[test]
522 fn test_plain_text() {
523 let engine = SimpleEngine::new();
524 let data = json!({});
525
526 let output = engine
527 .render_template("Just plain text, no variables", &data)
528 .unwrap();
529 assert_eq!(output, "Just plain text, no variables");
530 }
531
532 #[test]
533 fn test_complex_json_value() {
534 let engine = SimpleEngine::new();
535 let data = json!({
536 "obj": {"a": 1, "b": 2},
537 "arr": [1, 2, 3]
538 });
539
540 let output = engine.render_template("Obj: {obj}", &data).unwrap();
542 assert!(output.contains("\"a\":1") || output.contains("\"a\": 1"));
543 }
544}