use serde::{Deserialize, Serialize};
pub use nexo_tool_meta::template::{render_template, MISSING_PLACEHOLDER};
#[cfg(feature = "templating-handlebars")]
pub mod handlebars;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Template {
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub body: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Snippet {
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shortcut: Option<String>,
pub body: String,
}
pub fn render(template: &Template, context: &serde_json::Value) -> String {
render_template(&template.body, context)
}
pub fn render_snippet(snippet: &Snippet, context: &serde_json::Value) -> String {
render_template(&snippet.body, context)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn template(id: &str, body: &str) -> Template {
Template {
id: id.into(),
name: id.into(),
description: None,
body: body.into(),
}
}
fn snippet(id: &str, body: &str) -> Snippet {
Snippet {
id: id.into(),
name: id.into(),
shortcut: None,
body: body.into(),
}
}
#[test]
fn template_renders_dotted_paths() {
let t = template("welcome", "Hola {{person.name}}, soy {{seller.name}}.");
let ctx = json!({
"person": { "name": "Juan" },
"seller": { "name": "Pedro" }
});
assert_eq!(render(&t, &ctx), "Hola Juan, soy Pedro.");
}
#[test]
fn template_missing_path_renders_placeholder() {
let t = template("x", "{{person.email}}");
let ctx = json!({ "person": {} });
assert_eq!(render(&t, &ctx), MISSING_PLACEHOLDER);
}
#[test]
fn snippet_renders_same_as_template() {
let s = snippet("saludo", "Hola {{person.name}}");
let ctx = json!({ "person": { "name": "Ana" } });
assert_eq!(render_snippet(&s, &ctx), "Hola Ana");
}
#[test]
fn template_serde_roundtrips() {
let t = Template {
id: "welcome".into(),
name: "Welcome".into(),
description: Some("First-touch greeting".into()),
body: "Hola {{person.name}}".into(),
};
let json = serde_json::to_string(&t).unwrap();
let back: Template = serde_json::from_str(&json).unwrap();
assert_eq!(t, back);
}
#[test]
fn snippet_skips_none_shortcut_in_serialised_form() {
let s = snippet("x", "y");
let json = serde_json::to_string(&s).unwrap();
assert!(!json.contains("shortcut"));
}
#[test]
fn snippet_shortcut_round_trips() {
let s = Snippet {
id: "saludo".into(),
name: "Saludo".into(),
shortcut: Some("/saludo".into()),
body: "Hola".into(),
};
let json = serde_json::to_string(&s).unwrap();
assert!(json.contains("\"shortcut\":\"/saludo\""));
let back: Snippet = serde_json::from_str(&json).unwrap();
assert_eq!(s, back);
}
#[test]
fn template_description_optional() {
let t = template("x", "y");
let json = serde_json::to_string(&t).unwrap();
assert!(!json.contains("description"));
}
#[test]
fn empty_body_renders_empty() {
let t = template("x", "");
assert_eq!(render(&t, &json!({})), "");
}
#[test]
fn sandboxed_no_helpers() {
let t = template("x", "{{#if person.name}}HI{{/if}}");
let ctx = json!({ "person": { "name": "Ana" } });
let out = render(&t, &ctx);
assert_eq!(out, format!("{m}HI{m}", m = MISSING_PLACEHOLDER),);
assert!(out.starts_with(MISSING_PLACEHOLDER));
assert!(out.ends_with(MISSING_PLACEHOLDER));
}
#[test]
fn array_index_in_template() {
let t = template("x", "First tag: {{tags.0}}");
let ctx = json!({ "tags": ["alpha", "beta"] });
assert_eq!(render(&t, &ctx), "First tag: alpha");
}
}