use pest::iterators::Pair;
use tracing::trace;
use super::grammar::{DSLParser, Rule};
use crate::filter::{FieldPath, Template, TemplateItem};
pub struct TemplateParser;
impl TemplateParser {
pub fn parse_template_expr(
pair: Pair<Rule>,
) -> Result<Template, Box<pest::error::Error<Rule>>> {
let inner = match pair.into_inner().next() {
Some(inner) => inner,
None => {
trace!("parse_template_expr: no inner content found");
return Ok(Template { items: Vec::new() });
}
};
match inner.as_rule() {
Rule::braced_template => Self::parse_braced_template(inner),
Rule::bracketed_template => Self::parse_bracketed_template(inner),
Rule::simple_variable => {
let var_str = inner.as_str();
trace!("Parsing simple_variable: '{}'", var_str);
if let Some(field_name) = var_str.strip_prefix('$') {
if field_name.chars().all(|c| c.is_ascii_digit()) && !field_name.is_empty() {
trace!(
"Simple variable '{}' is numeric dollar amount, treating as literal",
var_str
);
Ok(Template {
items: vec![TemplateItem::Literal(var_str.to_string())],
})
} else {
trace!(
"Simple variable '{}' is field reference, treating as field",
var_str
);
let field_path = Self::parse_field_path_from_simple_var(inner);
Ok(Template {
items: vec![TemplateItem::Field(field_path)],
})
}
} else {
let field_path = Self::parse_field_path_from_simple_var(inner);
Ok(Template {
items: vec![TemplateItem::Field(field_path)],
})
}
}
Rule::interpolated_text => Self::parse_interpolated_text(inner),
_ => unreachable!("Unexpected template expression type"),
}
}
fn parse_braced_template(pair: Pair<Rule>) -> Result<Template, Box<pest::error::Error<Rule>>> {
let template_content = match pair.into_inner().next() {
Some(content) => content,
None => {
trace!("parse_braced_template: no content found, returning empty template");
return Ok(Template { items: Vec::new() });
}
};
match template_content.as_rule() {
Rule::braced_template_content => {
Self::parse_template_content_from_pairs(template_content)
}
_ => {
Self::parse_template_content_manually(template_content.as_str())
}
}
}
fn parse_bracketed_template(
pair: Pair<Rule>,
) -> Result<Template, Box<pest::error::Error<Rule>>> {
let template_content = match pair.into_inner().next() {
Some(content) => content,
None => {
trace!("parse_bracketed_template: no content found, returning empty template");
return Ok(Template { items: Vec::new() });
}
};
match template_content.as_rule() {
Rule::bracketed_template_content => {
Self::parse_template_content_from_pairs(template_content)
}
_ => {
Self::parse_template_content_manually(template_content.as_str())
}
}
}
fn parse_template_content_from_pairs(
pair: Pair<Rule>,
) -> Result<Template, Box<pest::error::Error<Rule>>> {
trace!("parse_template_content_from_pairs called");
let mut items = Vec::new();
for item in pair.into_inner() {
trace!(
"Processing template content item: {:?} with text: '{}'",
item.as_rule(),
item.as_str()
);
match item.as_rule() {
Rule::braced_template_item | Rule::bracketed_template_item => {
for inner_item in item.into_inner() {
trace!(
"Processing inner template item: {:?} with text: '{}'",
inner_item.as_rule(),
inner_item.as_str()
);
match inner_item.as_rule() {
Rule::template_variable => {
trace!("Found template_variable: '{}'", inner_item.as_str());
let field_path = Self::parse_template_variable(inner_item);
trace!(
"Parsed template variable to field path: {:?}",
field_path.parts
);
items.push(TemplateItem::Field(field_path));
}
Rule::braced_template_literal => {
let text = inner_item.as_str().to_string();
trace!("Found braced_template_literal: '{}'", text);
if !text.is_empty() {
items.push(TemplateItem::Literal(text));
}
}
Rule::bracketed_template_literal => {
let text = inner_item.as_str().to_string();
trace!("Found bracketed_template_literal: '{}'", text);
if !text.is_empty() {
items.push(TemplateItem::Literal(text));
}
}
_ => {
trace!(
"Unexpected inner rule in template item: {:?}",
inner_item.as_rule()
);
}
}
}
}
Rule::template_variable => {
trace!("Found direct template_variable: '{}'", item.as_str());
let field_path = Self::parse_template_variable(item);
trace!(
"Parsed direct template variable to field path: {:?}",
field_path.parts
);
items.push(TemplateItem::Field(field_path));
}
Rule::braced_template_literal => {
let text = item.as_str().to_string();
trace!("Found direct braced_template_literal: '{}'", text);
if !text.is_empty() {
items.push(TemplateItem::Literal(text));
}
}
Rule::bracketed_template_literal => {
let text = item.as_str().to_string();
trace!("Found direct bracketed_template_literal: '{}'", text);
if !text.is_empty() {
items.push(TemplateItem::Literal(text));
}
}
Rule::interpolated_content => {
trace!("Found interpolated_content: '{}'", item.as_str());
let parsed_template = Self::parse_template_content_manually(item.as_str())?;
items.extend(parsed_template.items);
}
_ => {
trace!("Unexpected rule in template content: {:?}", item.as_rule());
}
}
}
Ok(Template { items })
}
pub fn parse_template_content_manually(
content: &str,
) -> Result<Template, Box<pest::error::Error<Rule>>> {
trace!("parse_template_content_manually called with: '{}'", content);
let mut items = Vec::new();
let mut chars = content.chars().peekable();
let mut current_text = String::new();
while let Some(ch) = chars.next() {
if ch == '$' {
if chars.peek() == Some(&'{') {
chars.next(); trace!("Found ${{variable}} pattern");
if !current_text.is_empty() {
items.push(TemplateItem::Literal(current_text.clone()));
current_text.clear();
}
let mut var_name = String::new();
let mut brace_depth = 1;
while chars.peek().is_some() {
let ch = chars.next().unwrap();
if ch == '{' {
brace_depth += 1;
var_name.push(ch);
} else if ch == '}' {
brace_depth -= 1;
if brace_depth == 0 {
break;
} else {
var_name.push(ch);
}
} else {
var_name.push(ch);
}
}
if !var_name.is_empty() {
trace!("Parsed braced variable: '{}'", var_name);
let field_path = if var_name == "0" {
FieldPath::new(vec!["$0".to_string()])
} else {
Self::parse_field_name(&var_name)
};
items.push(TemplateItem::Field(field_path));
}
} else {
trace!("Found simple $variable pattern");
if !current_text.is_empty() {
items.push(TemplateItem::Literal(current_text.clone()));
current_text.clear();
}
let mut var_name = String::new();
if let Some(&first_ch) = chars.peek() {
if first_ch.is_alphabetic() || first_ch == '_' {
var_name.push(chars.next().unwrap());
while let Some(&next_ch) = chars.peek() {
if next_ch.is_alphanumeric() || next_ch == '_' || next_ch == '.' {
var_name.push(chars.next().unwrap());
} else {
break;
}
}
}
}
if !var_name.is_empty() {
trace!("Parsed simple variable: '{}'", var_name);
let field_path = Self::parse_field_name(&var_name);
items.push(TemplateItem::Field(field_path));
} else {
trace!(
"Dollar sign followed by non-alphabetic character, treating as literal"
);
current_text.push(ch);
while let Some(&next_ch) = chars.peek() {
if next_ch.is_ascii_digit() {
current_text.push(chars.next().unwrap());
} else {
break;
}
}
}
}
} else {
current_text.push(ch);
}
}
if !current_text.is_empty() {
items.push(TemplateItem::Literal(current_text));
}
if items.is_empty() {
}
Ok(Template { items })
}
fn parse_interpolated_text(
pair: Pair<Rule>,
) -> Result<Template, Box<pest::error::Error<Rule>>> {
let mut items = Vec::new();
for part in pair.into_inner() {
match part.as_rule() {
Rule::template_variable => {
let field_path = Self::parse_template_variable(part);
items.push(TemplateItem::Field(field_path));
}
Rule::interpolated_literal => {
let text = part.as_str().to_string();
if !text.is_empty() {
items.push(TemplateItem::Literal(text));
}
}
_ => {}
}
}
Ok(Template { items })
}
fn parse_template_variable(pair: Pair<Rule>) -> FieldPath {
trace!("parse_template_variable called with: '{}'", pair.as_str());
let inner = pair.into_inner().next().unwrap();
trace!(
"parse_template_variable inner rule: {:?} with text: '{}'",
inner.as_rule(),
inner.as_str()
);
match inner.as_rule() {
Rule::braced_variable => {
trace!("Processing braced_variable: '{}'", inner.as_str());
let field_path_pair = inner.into_inner().next().unwrap();
let field_path = DSLParser::parse_field_path(field_path_pair);
trace!("Braced variable field path: {:?}", field_path.parts);
if field_path.parts.len() == 1 && field_path.parts[0] == "0" {
trace!("Converting ${{0}} to $0 field");
return FieldPath::new(vec!["$0".to_string()]);
}
field_path
}
Rule::plain_variable => {
trace!("Processing plain_variable: '{}'", inner.as_str());
let field_path_pair = inner.into_inner().next().unwrap();
let field_path = DSLParser::parse_field_path(field_path_pair);
trace!("Plain variable field path: {:?}", field_path.parts);
field_path
}
Rule::field_path => {
trace!("Processing direct field_path: '{}'", inner.as_str());
let field_path = DSLParser::parse_field_path(inner);
trace!("Direct field path: {:?}", field_path.parts);
field_path
}
_ => unreachable!("Unexpected template variable type"),
}
}
fn parse_field_path_from_simple_var(pair: Pair<Rule>) -> FieldPath {
let var_str = pair.as_str();
trace!(
"parse_field_path_from_simple_var called with atomic rule: '{}'",
var_str
);
if let Some(field_name) = var_str.strip_prefix('$') {
let parts: Vec<String> = field_name.split('.').map(|s| s.to_string()).collect();
FieldPath::new(parts)
} else {
let parts: Vec<String> = var_str.split('.').map(|s| s.to_string()).collect();
FieldPath::new(parts)
}
}
fn parse_field_name(field_name: &str) -> FieldPath {
trace!("parse_field_name called with: '{}'", field_name);
if let Ok(index) = field_name.parse::<usize>() {
if index > 0 {
trace!("Numeric field {} stays as is", index);
return FieldPath::new(vec![field_name.to_string()]);
}
}
let parts: Vec<String> = field_name
.split('.')
.map(|s| s.trim().to_string())
.collect();
trace!("Parsed field path: {:?}", parts);
FieldPath::new(parts)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::filter::TemplateItem;
#[test]
fn test_parse_template_content_manually() {
let result =
TemplateParser::parse_template_content_manually("Hello ${name}, you have $5").unwrap();
assert!(result.items.len() >= 3);
match &result.items[0] {
TemplateItem::Literal(text) => assert_eq!(text, "Hello "),
_ => panic!("Expected literal"),
}
match &result.items[1] {
TemplateItem::Field(field) => assert_eq!(field.parts, vec!["name"]),
_ => panic!("Expected field"),
}
let remaining_content = result.items[2..]
.iter()
.map(|item| match item {
TemplateItem::Literal(text) => text.as_str(),
TemplateItem::Field(_) => "",
TemplateItem::Conditional { .. } => "",
})
.collect::<String>();
assert!(remaining_content.contains(", you have $5"));
}
#[test]
fn test_braced_variable_special_cases() {
let result = TemplateParser::parse_template_content_manually("${0}").unwrap();
assert_eq!(result.items.len(), 1);
match &result.items[0] {
TemplateItem::Field(field) => assert_eq!(field.parts, vec!["$0"]),
_ => panic!("Expected ${{0}} to map to $0 field"),
}
let result = TemplateParser::parse_template_content_manually("${1}").unwrap();
assert_eq!(result.items.len(), 1);
match &result.items[0] {
TemplateItem::Field(field) => assert_eq!(field.parts, vec!["1"]),
_ => panic!("Expected ${{1}} to map to '1' field"),
}
}
#[test]
fn test_mixed_template_content() {
let result = TemplateParser::parse_template_content_manually(
"ID: ${user_id}, Amount: $20, Name: ${name}",
)
.unwrap();
assert!(result.items.len() >= 3);
match &result.items[0] {
TemplateItem::Literal(text) => assert_eq!(text, "ID: "),
_ => panic!("Expected literal"),
}
match &result.items[1] {
TemplateItem::Field(field) => assert_eq!(field.parts, vec!["user_id"]),
_ => panic!("Expected field"),
}
let all_content = result
.items
.iter()
.map(|item| match item {
TemplateItem::Literal(text) => text.as_str(),
TemplateItem::Field(_) => "", TemplateItem::Conditional { .. } => "", })
.collect::<String>();
assert!(all_content.contains(", Amount: $20, Name: "));
}
#[test]
fn test_nested_field_paths() {
let result =
TemplateParser::parse_template_content_manually("${user.profile.name}").unwrap();
assert_eq!(result.items.len(), 1);
match &result.items[0] {
TemplateItem::Field(field) => assert_eq!(field.parts, vec!["user", "profile", "name"]),
_ => panic!("Expected nested field path"),
}
}
#[test]
fn test_template_edge_cases() {
let result = TemplateParser::parse_template_content_manually("").unwrap();
assert_eq!(result.items.len(), 0);
let result = TemplateParser::parse_template_content_manually("Hello World").unwrap();
assert_eq!(result.items.len(), 1);
match &result.items[0] {
TemplateItem::Literal(text) => assert_eq!(text, "Hello World"),
_ => panic!("Expected literal"),
}
let result = TemplateParser::parse_template_content_manually("${name}").unwrap();
assert_eq!(result.items.len(), 1);
match &result.items[0] {
TemplateItem::Field(field) => assert_eq!(field.parts, vec!["name"]),
_ => panic!("Expected field"),
}
}
#[test]
fn test_dollar_amounts_vs_variables() {
let result = TemplateParser::parse_template_content_manually("$20").unwrap();
assert_eq!(result.items.len(), 1);
match &result.items[0] {
TemplateItem::Literal(text) => assert_eq!(text, "$20"),
_ => panic!("Expected $20 to be literal"),
}
let result = TemplateParser::parse_template_content_manually("$name").unwrap();
assert_eq!(result.items.len(), 1);
match &result.items[0] {
TemplateItem::Field(field) => assert_eq!(field.parts, vec!["name"]),
_ => panic!("Expected $name to be field"),
}
let result = TemplateParser::parse_template_content_manually("$name has $50").unwrap();
assert_eq!(result.items.len(), 3);
match &result.items[0] {
TemplateItem::Field(field) => assert_eq!(field.parts, vec!["name"]),
_ => panic!("Expected field"),
}
match &result.items[1] {
TemplateItem::Literal(text) => assert_eq!(text, " has "),
_ => panic!("Expected literal"),
}
match &result.items[2] {
TemplateItem::Literal(text) => assert_eq!(text, "$50"),
_ => panic!("Expected literal"),
}
}
#[test]
fn test_parse_field_name() {
let field = TemplateParser::parse_field_name("name");
assert_eq!(field.parts, vec!["name"]);
let field = TemplateParser::parse_field_name("user.email");
assert_eq!(field.parts, vec!["user", "email"]);
let field = TemplateParser::parse_field_name("1");
assert_eq!(field.parts, vec!["1"]);
let field = TemplateParser::parse_field_name("5");
assert_eq!(field.parts, vec!["5"]);
}
}