use handlebars::Handlebars;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use super::{Snippet, Template};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Error)]
pub enum HandlebarsRenderError {
#[error("handlebars template parse error: {0}")]
Parse(String),
#[error("handlebars render error: {0}")]
Render(String),
}
pub fn render_handlebars(
template: &str,
context: &serde_json::Value,
) -> Result<String, HandlebarsRenderError> {
let mut hb = Handlebars::new();
hb.set_strict_mode(false);
hb.set_dev_mode(false);
hb.render_template(template, context).map_err(|e| {
let message = e.to_string();
if message.contains("Template") || message.contains("parse") || message.contains("syntax") {
HandlebarsRenderError::Parse(message)
} else {
HandlebarsRenderError::Render(message)
}
})
}
pub fn render_template(
template: &Template,
context: &serde_json::Value,
) -> Result<String, HandlebarsRenderError> {
render_handlebars(&template.body, context)
}
pub fn render_snippet(
snippet: &Snippet,
context: &serde_json::Value,
) -> Result<String, HandlebarsRenderError> {
render_handlebars(&snippet.body, context)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn simple_substitution_works() {
let out = render_handlebars(
"Hola {{person.name}}, soy {{seller.name}}.",
&json!({
"person": { "name": "Juan" },
"seller": { "name": "Pedro" }
}),
)
.unwrap();
assert_eq!(out, "Hola Juan, soy Pedro.");
}
#[test]
fn if_block_branches_on_truthy_field() {
let tmpl = "{{#if vip}}VIP{{else}}standard{{/if}}";
assert_eq!(
render_handlebars(tmpl, &json!({ "vip": true })).unwrap(),
"VIP",
);
assert_eq!(
render_handlebars(tmpl, &json!({ "vip": false })).unwrap(),
"standard",
);
assert_eq!(render_handlebars(tmpl, &json!({})).unwrap(), "standard",);
}
#[test]
fn unless_block_inverts_if() {
let out = render_handlebars(
"{{#unless paid}}reminder{{/unless}}",
&json!({ "paid": false }),
)
.unwrap();
assert_eq!(out, "reminder");
}
#[test]
fn each_loops_over_array() {
let out = render_handlebars(
"{{#each tags}}#{{this}} {{/each}}",
&json!({ "tags": ["alpha", "beta", "gamma"] }),
)
.unwrap();
assert_eq!(out, "#alpha #beta #gamma ");
}
#[test]
fn each_with_index_metadata() {
let out = render_handlebars(
"{{#each items}}{{@index}}={{this}};{{/each}}",
&json!({ "items": ["a", "b", "c"] }),
)
.unwrap();
assert_eq!(out, "0=a;1=b;2=c;");
}
#[test]
fn with_block_scopes_lookups() {
let out = render_handlebars(
"{{#with person}}{{name}} <{{email}}>{{/with}}",
&json!({ "person": { "name": "Ana", "email": "ana@x.io" } }),
)
.unwrap();
assert_eq!(out, "Ana <ana@x.io>");
}
#[test]
fn html_escapes_by_default() {
let out =
render_handlebars("{{body}}", &json!({ "body": "<script>alert(1)</script>" })).unwrap();
assert!(out.contains("<script>"));
assert!(!out.contains("<script>"));
}
#[test]
fn triple_braces_render_raw() {
let out = render_handlebars("{{{body}}}", &json!({ "body": "<b>bold</b>" })).unwrap();
assert_eq!(out, "<b>bold</b>");
}
#[test]
fn missing_key_renders_empty_in_lenient_mode() {
let out = render_handlebars("v={{not.present}}", &json!({})).unwrap();
assert_eq!(out, "v=");
}
#[test]
fn unregistered_partial_is_render_error() {
let r = render_handlebars("{{> some_partial}}", &json!({}));
assert!(matches!(r, Err(HandlebarsRenderError::Render(_))));
}
#[test]
fn malformed_template_returns_parse_error() {
let r = render_handlebars("{{#if x}}only", &json!({}));
assert!(matches!(r, Err(HandlebarsRenderError::Parse(_))));
}
#[test]
fn dotted_path_resolution() {
let out = render_handlebars(
"company={{lead.company.name}}",
&json!({
"lead": { "company": { "name": "Acme" } }
}),
)
.unwrap();
assert_eq!(out, "company=Acme");
}
#[test]
fn nested_if_each_compose() {
let tmpl = "{{#each leads}}{{#if vip}}⭐{{/if}}{{name}}\n{{/each}}";
let out = render_handlebars(
tmpl,
&json!({
"leads": [
{ "name": "Juan", "vip": true },
{ "name": "Ana", "vip": false }
]
}),
)
.unwrap();
assert_eq!(out, "⭐Juan\nAna\n");
}
#[test]
fn render_template_typed_helper() {
let t = Template {
id: "x".into(),
name: "x".into(),
description: None,
body: "Hi {{name}}".into(),
};
assert_eq!(
render_template(&t, &json!({ "name": "Pedro" })).unwrap(),
"Hi Pedro",
);
}
#[test]
fn render_snippet_typed_helper() {
let s = Snippet {
id: "saludo".into(),
name: "Saludo".into(),
shortcut: None,
body: "{{#if formal}}Estimado{{else}}Hola{{/if}} {{name}}".into(),
};
assert_eq!(
render_snippet(&s, &json!({ "formal": false, "name": "Ana" })).unwrap(),
"Hola Ana",
);
}
#[test]
fn empty_body_renders_empty() {
assert_eq!(render_handlebars("", &json!({})).unwrap(), "");
}
#[test]
fn array_index_via_lookup() {
let out = render_handlebars(
"first={{lookup tags 0}}",
&json!({ "tags": ["alpha", "beta"] }),
)
.unwrap();
assert_eq!(out, "first=alpha");
}
}