use tracing::trace;
use super::ast::ParsedDSL;
use super::grammar::DSLParser;
use super::template_parser::TemplateParser;
use crate::filter::{ComparisonOp, FieldPath, FilterExpr, FilterValue, Template, TemplateItem};
pub fn try_fallback_parsing(input: &str) -> Result<ParsedDSL, Box<dyn std::error::Error>> {
if let Ok(result) = try_manual_parsing(input) {
trace!("Manual parsing strategy succeeded");
return Ok(result);
}
if let Ok(result) = try_boolean_with_truthy_fields(input) {
trace!("Boolean with truthy fields strategy succeeded");
return Ok(result);
}
if let Ok(result) = try_simple_template_patterns(input) {
trace!("Simple template patterns strategy succeeded");
return Ok(result);
}
if let Ok(result) = try_as_field_selector(input) {
trace!("Field selector strategy succeeded");
return Ok(result);
}
trace!("All parsing strategies failed");
Err(format!(
"Could not parse '{input}'. Try:\n - Templates: [{{${{name}}}}], $name, or [Hello ${{name}}]\n - Filters: name == \"value\" or age > 25\n - Field selectors: name or \"field name\""
).into())
}
fn try_simple_template_patterns(input: &str) -> Result<ParsedDSL, Box<dyn std::error::Error>> {
trace!("try_simple_template_patterns called with: '{}'", input);
let mut result = ParsedDSL::new();
if input.starts_with('{') && input.ends_with('}') {
trace!("Found braced template pattern");
let content = &input[1..input.len() - 1];
if !content.trim().is_empty() {
if content.contains('$') {
trace!("Braced content contains variables, parsing manually");
if let Ok(template) = TemplateParser::parse_template_content_manually(content) {
result.template = Some(template);
return Ok(result);
}
}
trace!("Braced content has no variables, treating as literal");
result.template = Some(Template {
items: vec![TemplateItem::Literal(content.to_string())],
});
return Ok(result);
}
}
if input.starts_with('$') && !input.contains(' ') {
trace!("Found $variable pattern");
if input.starts_with("${") && input.ends_with("}") {
let field_name = &input[2..input.len() - 1]; if !field_name.is_empty() {
trace!(
"Parsing as braced variable (field substitution): '{}'",
field_name
);
let field_path = parse_field_name_simple(field_name);
result.template = Some(Template {
items: vec![TemplateItem::Field(field_path)],
});
return Ok(result);
} else {
trace!("Empty braced variable, treating as literal");
}
} else {
let field_name = &input[1..];
if !field_name.is_empty() && !field_name.chars().all(|c| c.is_ascii_digit()) {
trace!("Parsing as simple variable: '{}'", field_name);
let field_path = parse_field_name_simple(field_name);
result.template = Some(Template {
items: vec![TemplateItem::Field(field_path)],
});
return Ok(result);
} else if field_name.chars().all(|c| c.is_ascii_digit()) {
trace!(
"Dollar sign followed by digits only ($0, $1, $20, etc.), treating as numeric literal"
);
result.template = Some(Template {
items: vec![TemplateItem::Literal(input.to_string())],
});
return Ok(result);
} else {
trace!("Empty dollar sign, treating as literal");
result.template = Some(Template {
items: vec![TemplateItem::Literal(input.to_string())],
});
return Ok(result);
}
}
}
if input.contains('$') && !input.starts_with('{') && !input.starts_with('[') {
trace!(
"Found potential interpolated text pattern, but bare interpolated text is not allowed"
);
return Err(
"Bare interpolated text not allowed. Use [Hello ${name}] or {Hello ${name}} instead"
.into(),
);
}
Err("Not a simple template pattern".into())
}
fn try_as_field_selector(input: &str) -> Result<ParsedDSL, Box<dyn std::error::Error>> {
let mut result = ParsedDSL::new();
if (input.starts_with('"') && input.ends_with('"'))
|| (input.starts_with('\'') && input.ends_with('\''))
{
let content = &input[1..input.len() - 1];
let parts: Vec<String> = content.split('.').map(|s| s.to_string()).collect();
result.field_selector = Some(FieldPath::new(parts));
return Ok(result);
}
if input
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '.')
&& !input.is_empty()
&& !input.contains(' ')
&& !input.contains('$')
&& !input.contains('{')
&& !input.contains('}')
&& !input.contains('!')
&& !input.contains('=')
&& !input.contains('>')
&& !input.contains('<')
&& !input.contains('&')
&& !input.contains('|')
{
let parts: Vec<String> = input.split('.').map(|s| s.to_string()).collect();
result.field_selector = Some(FieldPath::new(parts));
return Ok(result);
}
Err("Not a field selector".into())
}
fn try_manual_parsing(input: &str) -> Result<ParsedDSL, Box<dyn std::error::Error>> {
if let Some((filter_part, template_part)) = split_filter_template_manually(input) {
let mut result = ParsedDSL::new();
let filter_parsed = if let Ok(boolean_result) = try_boolean_with_truthy_fields(filter_part)
{
result.filter = boolean_result.filter;
result.filter.is_some()
} else if let Ok(filter) = parse_simple_filter(filter_part) {
result.filter = Some(filter);
true
} else {
if let Ok(filter_result) = DSLParser::parse_filter_only(filter_part) {
result.filter = Some(filter_result);
true
} else {
false
}
};
let template_parsed = if let Ok(template) = DSLParser::parse_template_only(template_part) {
result.template = Some(template);
true
} else if let Ok(template_result) = try_simple_template_patterns(template_part) {
result.template = template_result.template;
result.template.is_some()
} else if template_part.starts_with('{') && template_part.ends_with('}')
|| template_part.starts_with('[') && template_part.ends_with(']')
{
let content = &template_part[1..template_part.len() - 1];
if let Ok(template) = TemplateParser::parse_template_content_manually(content) {
result.template = Some(template);
true
} else {
result.template = Some(Template {
items: vec![TemplateItem::Literal(content.to_string())],
});
true
}
} else {
false
};
if filter_parsed && template_parsed {
return Ok(result);
} else if filter_parsed {
return Ok(result);
}
}
if let Ok(filter) = parse_simple_filter(input) {
let mut result = ParsedDSL::new();
result.filter = Some(filter);
return Ok(result);
}
Err("Could not manually parse".into())
}
fn try_boolean_with_truthy_fields(input: &str) -> Result<ParsedDSL, Box<dyn std::error::Error>> {
trace!("try_boolean_with_truthy_fields: parsing '{}'", input);
if input.contains("&&") {
trace!("try_boolean_with_truthy_fields: found &&, trying AND expression");
return try_parse_and_expression(input);
}
if input.contains("||") {
trace!("try_boolean_with_truthy_fields: found ||, trying OR expression");
return try_parse_or_expression(input);
}
trace!("try_boolean_with_truthy_fields: no explicit boolean operators found");
Err("Not a boolean expression with explicit operators".into())
}
fn try_parse_and_expression(input: &str) -> Result<ParsedDSL, Box<dyn std::error::Error>> {
trace!("try_parse_and_expression: parsing '{}'", input);
let parts: Vec<&str> = input.split("&&").map(|s| s.trim()).collect();
if parts.len() != 2 {
trace!(
"try_parse_and_expression: found {} parts, only 2 supported",
parts.len()
);
return Err("Complex AND expressions not supported".into());
}
trace!(
"try_parse_and_expression: left='{}', right='{}'",
parts[0], parts[1]
);
let left = try_parse_single_boolean_term(parts[0])?;
let right = try_parse_single_boolean_term(parts[1])?;
trace!("try_parse_and_expression: successfully parsed both terms, creating AND");
let mut result = ParsedDSL::new();
result.filter = Some(FilterExpr::And(Box::new(left), Box::new(right)));
Ok(result)
}
fn try_parse_or_expression(input: &str) -> Result<ParsedDSL, Box<dyn std::error::Error>> {
let parts: Vec<&str> = input.split("||").map(|s| s.trim()).collect();
if parts.len() != 2 {
return Err("Complex OR expressions not supported".into());
}
let left = try_parse_single_boolean_term(parts[0])?;
let right = try_parse_single_boolean_term(parts[1])?;
let mut result = ParsedDSL::new();
result.filter = Some(FilterExpr::Or(Box::new(left), Box::new(right)));
Ok(result)
}
fn try_parse_single_boolean_term(term: &str) -> Result<FilterExpr, Box<dyn std::error::Error>> {
let trimmed = term.trim();
trace!("try_parse_single_boolean_term: parsing '{}'", trimmed);
if let Some(stripped) = trimmed.strip_prefix('!') {
let field_part = stripped.trim();
trace!(
"try_parse_single_boolean_term: found NOT operation on '{}'",
field_part
);
let field_path = parse_field_name_for_truthy(field_part);
trace!(
"try_parse_single_boolean_term: creating NOT(FieldTruthy) for '{}'",
field_part
);
return Ok(FilterExpr::Not(Box::new(FilterExpr::FieldTruthy(
field_path,
))));
}
if super::operators::contains_filter_operators(trimmed) {
trace!(
"try_parse_single_boolean_term: '{}' contains filter operators, trying normal parse",
trimmed
);
if let Ok(result) = DSLParser::parse_dsl(trimmed) {
if let Some(filter) = result.filter {
trace!(
"try_parse_single_boolean_term: successfully parsed '{}' as filter",
trimmed
);
return Ok(filter);
}
}
trace!(
"try_parse_single_boolean_term: failed to parse '{}' as filter despite operators",
trimmed
);
}
if let Some(field_name) = trimmed.strip_suffix('?') {
let field_path = parse_field_name_for_truthy(field_name);
trace!(
"try_parse_single_boolean_term: creating FieldTruthy for '{}'",
trimmed
);
return Ok(FilterExpr::FieldTruthy(field_path));
}
trace!(
"try_parse_single_boolean_term: '{}' not recognized as valid boolean term, rejecting",
trimmed
);
Err(format!(
"Cannot parse '{trimmed}' as boolean term - use 'field?' for truthy check or add comparison operator"
)
.into())
}
fn parse_field_name_simple(field_name: &str) -> FieldPath {
trace!("parse_field_name_simple called with: '{}'", field_name);
if field_name == "0" {
return FieldPath::new(vec!["$0".to_string()]);
}
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 simple field path: {:?}", parts);
FieldPath::new(parts)
}
fn split_filter_template_manually(input: &str) -> Option<(&str, &str)> {
let mut in_quotes = false;
let mut quote_char = ' ';
let mut brace_count = 0;
let mut bracket_count = 0;
let mut pos = 0;
let chars: Vec<char> = input.chars().collect();
for (i, &c) in chars.iter().enumerate() {
match c {
'"' | '\'' => {
if !in_quotes {
in_quotes = true;
quote_char = c;
} else if c == quote_char {
in_quotes = false;
}
}
'{' => {
if !in_quotes && brace_count == 0 && bracket_count == 0 && pos == 0 {
pos = i;
}
if !in_quotes {
brace_count += 1;
}
}
'}' => {
if !in_quotes {
brace_count -= 1;
}
}
'[' => {
if !in_quotes && brace_count == 0 && bracket_count == 0 && pos == 0 {
pos = i;
}
if !in_quotes {
bracket_count += 1;
}
}
']' => {
if !in_quotes {
bracket_count -= 1;
}
}
_ => {}
}
}
if pos > 0 {
let filter_part = input[..pos].trim();
let template_part = input[pos..].trim();
if !filter_part.is_empty() && !template_part.is_empty() {
return Some((filter_part, template_part));
}
}
None
}
fn parse_simple_filter(input: &str) -> Result<FilterExpr, Box<dyn std::error::Error>> {
let parts: Vec<&str> = input.split_whitespace().collect();
if parts.len() >= 3 {
let field_name = parts[0];
let operator = parts[1];
let value_str = parts[2..].join(" ");
let field_parts: Vec<String> = field_name.split('.').map(|s| s.to_string()).collect();
let field = FieldPath::new(field_parts);
let op = match operator {
"==" => ComparisonOp::Equal,
"!=" => ComparisonOp::NotEqual,
">" => ComparisonOp::GreaterThan,
">=" => ComparisonOp::GreaterThanOrEqual,
"<" => ComparisonOp::LessThan,
"<=" => ComparisonOp::LessThanOrEqual,
"~" => ComparisonOp::Contains,
"^=" => ComparisonOp::StartsWith,
"$=" => ComparisonOp::EndsWith,
"*=" => ComparisonOp::Contains,
_ => return Err("Unknown operator".into()),
};
let value = if value_str.starts_with('"') && value_str.ends_with('"') {
FilterValue::String(value_str[1..value_str.len() - 1].to_string())
} else if value_str == "true" {
FilterValue::Boolean(true)
} else if value_str == "false" {
FilterValue::Boolean(false)
} else if value_str == "null" {
FilterValue::Null
} else if let Ok(num) = value_str.parse::<f64>() {
FilterValue::Number(num)
} else {
FilterValue::String(value_str)
};
return Ok(FilterExpr::Comparison { field, op, value });
}
Err("Could not parse as simple filter".into())
}
fn parse_field_name_for_truthy(name: &str) -> FieldPath {
let base_name = name.strip_suffix('?').unwrap_or(name);
trace!(
"parse_field_name_for_truthy: '{}' -> base: '{}'",
name, base_name
);
let parts: Vec<String> = base_name.split('.').map(|s| s.to_string()).collect();
let field_path = FieldPath::new(parts);
trace!(
"parse_field_name_for_truthy: created FieldPath {:?}",
field_path.parts
);
field_path
}
#[cfg(test)]
mod tests {
use super::*;
use crate::filter::{ComparisonOp, FilterExpr, FilterValue, TemplateItem};
#[test]
fn test_try_simple_template_patterns() {
let result = try_simple_template_patterns("{Hello ${name}}").unwrap();
assert!(result.template.is_some());
let template = result.template.unwrap();
assert_eq!(template.items.len(), 2);
let result = try_simple_template_patterns("$name").unwrap();
assert!(result.template.is_some());
let template = result.template.unwrap();
assert_eq!(template.items.len(), 1);
match &template.items[0] {
TemplateItem::Field(field) => assert_eq!(field.parts, vec!["name"]),
_ => panic!("Expected field"),
}
let result = try_simple_template_patterns("$20").unwrap();
assert!(result.template.is_some());
let template = result.template.unwrap();
assert_eq!(template.items.len(), 1);
match &template.items[0] {
TemplateItem::Literal(text) => assert_eq!(text, "$20"),
_ => panic!("Expected literal"),
}
}
#[test]
fn test_try_as_field_selector() {
let result = try_as_field_selector("name").unwrap();
assert!(result.field_selector.is_some());
let field = result.field_selector.unwrap();
assert_eq!(field.parts, vec!["name"]);
let result = try_as_field_selector("user.email").unwrap();
assert!(result.field_selector.is_some());
let field = result.field_selector.unwrap();
assert_eq!(field.parts, vec!["user", "email"]);
let result = try_as_field_selector("\"field with spaces\"").unwrap();
assert!(result.field_selector.is_some());
let field = result.field_selector.unwrap();
assert_eq!(field.parts, vec!["field with spaces"]);
let result = try_as_field_selector("'another field'").unwrap();
assert!(result.field_selector.is_some());
let field = result.field_selector.unwrap();
assert_eq!(field.parts, vec!["another field"]);
}
#[test]
fn test_try_boolean_with_truthy_fields() {
let result = try_boolean_with_truthy_fields("active? && verified?").unwrap();
assert!(result.filter.is_some());
match result.filter.unwrap() {
FilterExpr::And(left, right) => match (left.as_ref(), right.as_ref()) {
(FilterExpr::FieldTruthy(l), FilterExpr::FieldTruthy(r)) => {
assert_eq!(l.parts, vec!["active"]);
assert_eq!(r.parts, vec!["verified"]);
}
_ => panic!("Expected FieldTruthy expressions"),
},
_ => panic!("Expected AND expression"),
}
let result = try_boolean_with_truthy_fields("premium? || admin?").unwrap();
assert!(result.filter.is_some());
match result.filter.unwrap() {
FilterExpr::Or(left, right) => match (left.as_ref(), right.as_ref()) {
(FilterExpr::FieldTruthy(l), FilterExpr::FieldTruthy(r)) => {
assert_eq!(l.parts, vec!["premium"]);
assert_eq!(r.parts, vec!["admin"]);
}
_ => panic!("Expected FieldTruthy expressions"),
},
_ => panic!("Expected OR expression"),
}
}
#[test]
fn test_try_parse_single_boolean_term() {
let result = try_parse_single_boolean_term("active?").unwrap();
match result {
FilterExpr::FieldTruthy(field) => {
assert_eq!(field.parts, vec!["active"]);
}
_ => panic!("Expected FieldTruthy"),
}
let result = try_parse_single_boolean_term("!suspended").unwrap();
match result {
FilterExpr::Not(inner) => match inner.as_ref() {
FilterExpr::FieldTruthy(field) => {
assert_eq!(field.parts, vec!["suspended"]);
}
_ => panic!("Expected FieldTruthy inside NOT"),
},
_ => panic!("Expected NOT expression"),
}
let result = try_parse_single_boolean_term("age > 25").unwrap();
match result {
FilterExpr::Comparison { field, op, value } => {
assert_eq!(field.parts, vec!["age"]);
assert!(matches!(op, ComparisonOp::GreaterThan));
assert!(matches!(value, FilterValue::Number(25.0)));
}
_ => panic!("Expected comparison"),
}
}
#[test]
fn test_try_parse_single_boolean_term_rejects_bare_fields() {
assert!(try_parse_single_boolean_term("active").is_err());
assert!(try_parse_single_boolean_term("user.verified").is_err());
assert!(try_parse_single_boolean_term("field_name").is_err());
}
#[test]
fn test_parse_field_name_for_truthy() {
let field = parse_field_name_for_truthy("active");
assert_eq!(field.parts, vec!["active"]);
let field = parse_field_name_for_truthy("active?");
assert_eq!(field.parts, vec!["active"]);
let field = parse_field_name_for_truthy("user.verified");
assert_eq!(field.parts, vec!["user", "verified"]);
let field = parse_field_name_for_truthy("user.verified?");
assert_eq!(field.parts, vec!["user", "verified"]);
}
#[test]
fn test_parse_simple_filter() {
let result = parse_simple_filter("age > 25").unwrap();
match result {
FilterExpr::Comparison { field, op, value } => {
assert_eq!(field.parts, vec!["age"]);
assert!(matches!(op, ComparisonOp::GreaterThan));
assert!(matches!(value, FilterValue::Number(25.0)));
}
_ => panic!("Expected comparison"),
}
let result = parse_simple_filter("name == \"Alice\"").unwrap();
match result {
FilterExpr::Comparison { field, op, value } => {
assert_eq!(field.parts, vec!["name"]);
assert!(matches!(op, ComparisonOp::Equal));
assert!(matches!(value, FilterValue::String(s) if s == "Alice"));
}
_ => panic!("Expected comparison"),
}
let result = parse_simple_filter("active == true").unwrap();
match result {
FilterExpr::Comparison { field, op, value } => {
assert_eq!(field.parts, vec!["active"]);
assert!(matches!(op, ComparisonOp::Equal));
assert!(matches!(value, FilterValue::Boolean(true)));
}
_ => panic!("Expected comparison"),
}
}
#[test]
fn test_split_filter_template_manually() {
let result = split_filter_template_manually("age > 25 {Hello ${name}}");
assert!(result.is_some());
let (filter_part, template_part) = result.unwrap();
assert_eq!(filter_part, "age > 25");
assert_eq!(template_part, "{Hello ${name}}");
let result = split_filter_template_manually("status == \"active\" [User: ${name}]");
assert!(result.is_some());
let (filter_part, template_part) = result.unwrap();
assert_eq!(filter_part, "status == \"active\"");
assert_eq!(template_part, "[User: ${name}]");
let result = split_filter_template_manually("message == \"Hello {world}\"");
assert!(result.is_none());
}
#[test]
fn test_try_fallback_parsing() {
let result = try_fallback_parsing("$name").unwrap();
assert!(result.template.is_some());
let result = try_fallback_parsing("user.email").unwrap();
assert!(result.field_selector.is_some());
let result = try_fallback_parsing("active? && verified?").unwrap();
assert!(result.filter.is_some());
let result = try_fallback_parsing("age > 25 {Hello ${name}}").unwrap();
assert!(result.filter.is_some());
assert!(result.template.is_some());
}
#[test]
fn test_edge_cases_and_error_handling() {
assert!(try_as_field_selector("").is_err());
assert!(try_as_field_selector("age > 25").is_err());
assert!(try_as_field_selector("name == value").is_err());
assert!(try_boolean_with_truthy_fields("just_a_field").is_err());
assert!(parse_simple_filter("age >").is_err());
assert!(parse_simple_filter("name").is_err());
}
#[test]
fn test_numeric_vs_variable_distinction() {
let result = try_simple_template_patterns("${0}").unwrap();
assert!(result.template.is_some());
let template = result.template.unwrap();
match &template.items[0] {
TemplateItem::Field(field) => assert_eq!(field.parts, vec!["$0"]),
_ => panic!("Expected ${{0}} to map to $0 field"),
}
let result = try_simple_template_patterns("${1}").unwrap();
assert!(result.template.is_some());
let template = result.template.unwrap();
match &template.items[0] {
TemplateItem::Field(field) => assert_eq!(field.parts, vec!["1"]),
_ => panic!("Expected ${{1}} to map to '1' field"),
}
let result = try_simple_template_patterns("$1").unwrap();
assert!(result.template.is_some());
let template = result.template.unwrap();
match &template.items[0] {
TemplateItem::Literal(text) => assert_eq!(text, "$1"),
_ => panic!("Expected $1 to be literal"),
}
}
}