use camel_language_api::LanguageError;
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
Header(String),
Body,
BodyField(Vec<PathSegment>),
ExchangeProperty(String),
StringLit(String),
NumberLit(f64),
Null,
BinOp {
left: Box<Expr>,
op: Op,
right: Box<Expr>,
},
Interpolated(Vec<InterpolatedPart>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum InterpolatedPart {
Literal(String),
Expr(Box<Expr>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathSegment {
Key(String),
Index(usize),
}
#[derive(Debug, Clone, PartialEq)]
pub enum Op {
Eq,
Ne,
Gt,
Lt,
Gte,
Lte,
Contains,
}
pub fn parse(input: &str) -> Result<Expr, LanguageError> {
let input = input.trim();
let ops = [
(">=", Op::Gte),
("<=", Op::Lte),
("!=", Op::Ne),
("==", Op::Eq),
(">", Op::Gt),
("<", Op::Lt),
(" contains ", Op::Contains),
];
for (op_str, op) in &ops {
if let Some(pos) = find_op_outside_quotes(input, op_str) {
let left = parse_atom(input[..pos].trim())?;
let right = parse_atom(input[pos + op_str.len()..].trim())?;
return Ok(Expr::BinOp {
left: Box::new(left),
op: op.clone(),
right: Box::new(right),
});
}
}
if input.contains("${") {
if is_pure_interpolation(input) {
return parse_atom(input);
} else {
return parse_interpolated(input);
}
}
if let Ok(expr) = parse_atom(input) {
return Ok(expr);
}
Ok(Expr::StringLit(input.to_string()))
}
fn is_pure_interpolation(input: &str) -> bool {
if !input.starts_with("${") {
return false;
}
if let Some(end) = input.find('}') {
end == input.len() - 1
} else {
false
}
}
fn parse_interpolated(input: &str) -> Result<Expr, LanguageError> {
let mut parts: Vec<InterpolatedPart> = Vec::new();
let mut remaining = input;
while !remaining.is_empty() {
if let Some(start) = remaining.find("${") {
if start > 0 {
parts.push(InterpolatedPart::Literal(remaining[..start].to_string()));
}
let after_dollar = &remaining[start..]; if let Some(end) = after_dollar.find('}') {
let token = &after_dollar[..=end]; let expr = parse_atom(token)?;
parts.push(InterpolatedPart::Expr(Box::new(expr)));
remaining = &after_dollar[end + 1..];
} else {
parts.push(InterpolatedPart::Literal(after_dollar.to_string()));
remaining = "";
}
} else {
parts.push(InterpolatedPart::Literal(remaining.to_string()));
remaining = "";
}
}
Ok(Expr::Interpolated(parts))
}
fn find_op_outside_quotes(input: &str, op: &str) -> Option<usize> {
for (pos, _) in input.match_indices(op) {
let quote_count = input[..pos].chars().filter(|&c| c == '\'').count();
if quote_count % 2 == 0 {
return Some(pos);
}
}
None
}
fn parse_atom(s: &str) -> Result<Expr, LanguageError> {
let s = s.trim();
if s == "null" {
return Ok(Expr::Null);
}
if s.starts_with("${header.") && s.ends_with('}') {
let key = &s[9..s.len() - 1];
if key.is_empty() {
return Err(LanguageError::ParseError {
expr: s.to_string(),
reason: "header key must not be empty".to_string(),
});
}
return Ok(Expr::Header(key.to_string()));
}
if s == "${body}" {
return Ok(Expr::Body);
}
if s.starts_with("${body.") && s.ends_with('}') {
let path_str = &s[7..s.len() - 1]; let segments = parse_body_path(path_str)?;
return Ok(Expr::BodyField(segments));
}
if s.starts_with("${exchangeProperty.") && s.ends_with('}') {
let key = &s[19..s.len() - 1];
if key.is_empty() {
return Err(LanguageError::ParseError {
expr: s.to_string(),
reason: "exchange property key must not be empty".to_string(),
});
}
return Ok(Expr::ExchangeProperty(key.to_string()));
}
if s.starts_with('\'') && s.ends_with('\'') && s.len() >= 2 {
return Ok(Expr::StringLit(s[1..s.len() - 1].to_string()));
}
if let Ok(n) = s.parse::<f64>() {
return Ok(Expr::NumberLit(n));
}
Err(LanguageError::ParseError {
expr: s.to_string(),
reason: "unrecognized token".to_string(),
})
}
fn parse_body_path(path: &str) -> Result<Vec<PathSegment>, LanguageError> {
let mut segments = Vec::new();
for seg in path.split('.') {
if seg.is_empty() {
return Err(LanguageError::ParseError {
expr: format!("${{body.{path}}}"),
reason: "body path segment must not be empty".to_string(),
});
}
let is_index = seg.parse::<usize>().is_ok() && (seg == "0" || !seg.starts_with('0'));
if is_index {
segments.push(PathSegment::Index(seg.parse::<usize>().unwrap()));
} else {
segments.push(PathSegment::Key(seg.to_string()));
}
}
Ok(segments)
}