use crate::{Error, Result};
#[derive(Debug, Clone, PartialEq)]
pub struct FunctionCall {
pub name: String,
pub args: Vec<FunctionArg>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum FunctionArg {
Literal(String),
Number(i64),
PathExpr(PathExpr),
ContextRef(String),
Identifier(String),
Function(Box<FunctionCall>),
}
#[derive(Debug, Clone, PartialEq)]
pub struct PathExpr {
pub level: usize,
pub path: String,
}
impl FunctionCall {
pub fn parse(input: &str) -> Result<Self> {
let input = input.trim();
if !input.starts_with('=') {
return Err(Error::ParseError(format!(
"Function call must start with '=', got: {}",
input
)));
}
let input = &input[1..];
let open_paren_pos = input.find('(');
let (name, args_str) = if let Some(pos) = open_paren_pos {
let name = input[..pos].trim().to_string();
let rest = &input[pos..];
if !rest.starts_with('(') || !rest.ends_with(')') {
return Err(Error::ParseError(format!(
"Malformed function call: {}",
input
)));
}
let args_str = &rest[1..rest.len() - 1];
(name, Some(args_str))
} else {
(input.trim().to_string(), None)
};
let args = if let Some(args_str) = args_str {
parse_function_args(args_str)?
} else {
vec![]
};
Ok(FunctionCall { name, args })
}
}
fn parse_function_args(input: &str) -> Result<Vec<FunctionArg>> {
let mut args = Vec::new();
let mut current = String::new();
let mut in_single_quote = false;
let mut in_paren = 0;
let mut escape_next = false;
for ch in input.chars() {
if escape_next {
current.push(ch);
escape_next = false;
continue;
}
match ch {
'\\' => {
escape_next = true;
}
'\'' if !in_single_quote => {
in_single_quote = true;
current.push(ch);
}
'\'' if in_single_quote => {
in_single_quote = false;
current.push(ch);
}
'(' if !in_single_quote => {
in_paren += 1;
current.push(ch);
}
')' if !in_single_quote => {
in_paren -= 1;
current.push(ch);
}
',' if !in_single_quote && in_paren == 0 => {
let arg = current.trim();
if !arg.is_empty() {
args.push(parse_single_arg(arg)?);
}
current.clear();
}
_ => {
current.push(ch);
}
}
}
let arg = current.trim();
if !arg.is_empty() {
args.push(parse_single_arg(arg)?);
}
Ok(args)
}
fn parse_single_arg(input: &str) -> Result<FunctionArg> {
let input = input.trim();
if input.starts_with('=') {
let nested_call = FunctionCall::parse(input)?;
return Ok(FunctionArg::Function(Box::new(nested_call)));
}
if input.starts_with('\'') && input.ends_with('\'') {
let literal = &input[1..input.len() - 1];
return Ok(FunctionArg::Literal(literal.to_string()));
}
if input.starts_with('@') {
return parse_path_expr(input);
}
if let Some(stripped) = input.strip_prefix('^') {
let ident = stripped.trim().to_string();
return Ok(FunctionArg::ContextRef(ident));
}
if let Ok(num) = input.parse::<i64>() {
return Ok(FunctionArg::Number(num));
}
Ok(FunctionArg::Identifier(input.to_string()))
}
fn parse_path_expr(input: &str) -> Result<FunctionArg> {
let input = input.trim();
let rest = input.strip_prefix('@').ok_or_else(|| {
Error::ParseError(format!(
"Path expression must start with '@', got: {}",
input
))
})?;
if !rest.starts_with('(') {
if let Ok(level) = rest.parse::<usize>() {
return Ok(FunctionArg::PathExpr(PathExpr {
level,
path: String::new(),
}));
} else {
return Ok(FunctionArg::PathExpr(PathExpr {
level: 0,
path: rest.to_string(),
}));
}
}
if !rest.starts_with('(') || !rest.ends_with(')') {
return Err(Error::ParseError(format!(
"Malformed path expression: {}",
input
)));
}
let inner = &rest[1..rest.len() - 1];
let comma_pos = inner.find(',');
let (level, path) = if let Some(pos) = comma_pos {
let level_str = inner[..pos].trim();
let path_str = inner[pos + 1..].trim();
let level = level_str.parse::<usize>().map_err(|_| {
Error::ParseError(format!("Invalid level in path expression: {}", level_str))
})?;
(level, path_str.to_string())
} else {
(0, inner.trim().to_string())
};
Ok(FunctionArg::PathExpr(PathExpr { level, path }))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_concat() {
let call = FunctionCall::parse("=concat(@(1,name[0]),' ',@(1,name[1]))").unwrap();
assert_eq!(call.name, "concat");
assert_eq!(call.args.len(), 3);
match &call.args[0] {
FunctionArg::PathExpr(p) => {
assert_eq!(p.level, 1);
assert_eq!(p.path, "name[0]");
}
_ => panic!("Expected PathExpr"),
}
match &call.args[1] {
FunctionArg::Literal(s) => assert_eq!(s, " "),
_ => panic!("Expected Literal"),
}
}
#[test]
fn test_parse_to_lower() {
let call = FunctionCall::parse("=toLower(@(2,string))").unwrap();
assert_eq!(call.name, "toLower");
assert_eq!(call.args.len(), 1);
}
#[test]
fn test_parse_shorthand() {
let call = FunctionCall::parse("=trim").unwrap();
assert_eq!(call.name, "trim");
assert_eq!(call.args.len(), 0);
}
#[test]
fn test_parse_nested_function() {
let call = FunctionCall::parse("=toUpper(=toLower(@(0)))").unwrap();
assert_eq!(call.name, "toUpper");
assert_eq!(call.args.len(), 1);
match &call.args[0] {
FunctionArg::Function(nested) => {
assert_eq!(nested.name, "toLower");
assert_eq!(nested.args.len(), 1);
}
_ => panic!("Expected Function variant"),
}
}
#[test]
fn test_parse_concat_with_nested_substring() {
let call =
FunctionCall::parse("=concat(=substring(@(0),0,4),\"\\-\",=substring(@(0),4,6))").unwrap();
assert_eq!(call.name, "concat");
assert_eq!(call.args.len(), 3);
match &call.args[0] {
FunctionArg::Function(nested) => {
assert_eq!(nested.name, "substring");
assert_eq!(nested.args.len(), 3);
}
_ => panic!("Expected Function variant for first arg"),
}
match &call.args[2] {
FunctionArg::Function(nested) => {
assert_eq!(nested.name, "substring");
assert_eq!(nested.args.len(), 3);
}
_ => panic!("Expected Function variant for third arg"),
}
}
#[test]
fn test_parse_substring_with_numbers() {
let call = FunctionCall::parse("=substring(@(0),0,4)").unwrap();
assert_eq!(call.name, "substring");
assert_eq!(call.args.len(), 3);
match &call.args[1] {
FunctionArg::Number(n) => assert_eq!(*n, 0),
_ => panic!("Expected Number variant for start index"),
}
match &call.args[2] {
FunctionArg::Number(n) => assert_eq!(*n, 4),
_ => panic!("Expected Number variant for end index"),
}
}
}