use crate::common::config::env_loader;
use anyhow::{Context, Result, anyhow};
#[derive(Debug, Clone, PartialEq)]
pub enum TemplateNode {
Text(String),
Variable {
parts: Vec<Self>,
},
}
pub fn parse_template(input: &str) -> Result<Vec<TemplateNode>> {
let max_depth = env_loader::get_env("MAX_TEMPLATE_PARSE_DEPTH", "5".to_owned())
.parse()
.unwrap_or(5);
let max_nodes = env_loader::get_env("MAX_TEMPLATE_PARSE_NODES", "50".to_owned())
.parse()
.unwrap_or(50);
let mut node_count = 0;
parse_recursive(input, 0, max_depth, &mut node_count, max_nodes)
}
fn parse_recursive(
input: &str,
depth: usize,
max_depth: usize,
node_count: &mut usize,
max_nodes: usize,
) -> Result<Vec<TemplateNode>> {
if depth > max_depth {
return Err(anyhow!(
"Template parsing depth limit ({max_depth}) exceeded"
));
}
let mut nodes = Vec::new();
let mut chars = input.chars().peekable();
let mut current_text = String::new();
while let Some(ch) = chars.next() {
if ch == '{' {
if chars.peek() == Some(&'{') {
chars.next();
if !current_text.is_empty() {
*node_count += 1;
if *node_count > max_nodes {
return Err(anyhow!(
"Template parsing node limit ({max_nodes}) exceeded"
));
}
nodes.push(TemplateNode::Text(current_text.clone()));
current_text.clear();
}
let var_content =
parse_variable_content(&mut chars).context("Failed to parse variable content")?;
let parts = parse_recursive(&var_content, depth + 1, max_depth, node_count, max_nodes)?;
*node_count += 1;
if *node_count > max_nodes {
return Err(anyhow!(
"Template parsing node limit ({max_nodes}) exceeded"
));
}
nodes.push(TemplateNode::Variable { parts });
} else {
current_text.push(ch);
}
} else {
current_text.push(ch);
}
}
if !current_text.is_empty() {
*node_count += 1;
if *node_count > max_nodes {
return Err(anyhow!(
"Template parsing node limit ({max_nodes}) exceeded"
));
}
nodes.push(TemplateNode::Text(current_text));
}
Ok(nodes)
}
fn parse_variable_content(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Result<String> {
let mut content = String::new();
let mut depth = 0;
while let Some(ch) = chars.next() {
if ch == '{' {
if chars.peek() == Some(&'{') {
chars.next(); depth += 1;
content.push_str("{{");
} else {
content.push(ch);
}
} else if ch == '}' {
if chars.peek() == Some(&'}') {
chars.next();
if depth == 0 {
return Ok(content);
} else {
depth -= 1;
content.push_str("}}");
}
} else {
content.push(ch);
}
} else {
content.push(ch);
}
}
anyhow::bail!("Unclosed variable: missing closing }}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_plain_text() {
let result = parse_template("plain text").unwrap();
assert_eq!(result, vec![TemplateNode::Text("plain text".to_string())]);
}
#[test]
fn test_parse_simple_variable() {
let result = parse_template("{{key}}").unwrap();
assert_eq!(
result,
vec![TemplateNode::Variable {
parts: vec![TemplateNode::Text("key".to_string())]
}]
);
}
#[test]
fn test_parse_mixed() {
let result = parse_template("before {{key}} after").unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result[0], TemplateNode::Text("before ".to_string()));
assert!(matches!(result[1], TemplateNode::Variable { .. }));
assert_eq!(result[2], TemplateNode::Text(" after".to_string()));
}
#[test]
fn test_parse_concatenation() {
let result = parse_template("{{a}}:{{b}}").unwrap();
assert_eq!(result.len(), 3);
assert!(matches!(result[0], TemplateNode::Variable { .. }));
assert_eq!(result[1], TemplateNode::Text(":".to_string()));
assert!(matches!(result[2], TemplateNode::Variable { .. }));
}
#[test]
fn test_parse_nested() {
let result = parse_template("{{kv.{{proto}}_backend}}").unwrap();
assert_eq!(result.len(), 1);
if let TemplateNode::Variable { parts } = &result[0] {
assert_eq!(parts.len(), 3);
assert_eq!(parts[0], TemplateNode::Text("kv.".to_string()));
assert!(matches!(parts[1], TemplateNode::Variable { .. }));
assert_eq!(parts[2], TemplateNode::Text("_backend".to_string()));
} else {
panic!("Expected Variable node");
}
}
#[test]
fn test_parse_empty() {
let result = parse_template("").unwrap();
assert_eq!(result, vec![]);
}
#[test]
fn test_parse_unclosed_variable() {
let result = parse_template("{{key");
assert!(result.is_err());
}
#[test]
fn test_parse_single_brace() {
let result = parse_template("single { brace").unwrap();
assert_eq!(
result,
vec![TemplateNode::Text("single { brace".to_string())]
);
}
#[test]
fn test_parse_recursion_limit() {
let deep = "{{{{{{{{{{{{key}}}}}}}}}}}}";
let result = parse_template(deep);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("depth limit"));
}
#[test]
fn test_parse_node_limit() {
let mut long = String::new();
for i in 0..26 {
long.push_str(&format!("{{{{v{}}}}}", i));
}
let result = parse_template(&long);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("node limit"));
}
}