mod ast;
mod fallback;
mod filter_parser;
mod grammar;
mod operators;
mod template_parser;
pub use ast::ParsedDSL;
pub use grammar::{DSLParser, Rule};
use tracing::trace;
pub fn parse_command(input: &str) -> Result<ParsedDSL, Box<dyn std::error::Error>> {
let trimmed = input.trim();
trace!("parse_command called with: '{}'", trimmed);
let in_test_mode = cfg!(test);
match DSLParser::parse_dsl(trimmed) {
Ok(mut result) => {
trace!("Main parser succeeded");
trace!(
"Parsed DSL result: filter={:?}, template={:?}, field_selector={:?}",
result.filter.is_some(),
result.template.is_some(),
result.field_selector.is_some()
);
if !in_test_mode
&& result.filter.is_some()
&& result.template.is_none()
&& result.field_selector.is_none()
{
trace!("Adding default template for filter-only expression");
match DSLParser::parse_dsl("[${0}]") {
Ok(default_template_dsl) => {
result.template = default_template_dsl.template;
trace!("Default template added successfully");
}
Err(e) => {
trace!("Failed to add default template: {:?}", e);
}
}
}
Ok(result)
}
Err(_parse_error) => {
trace!("Main parser failed, trying fallback strategies");
let mut fallback_result = fallback::try_fallback_parsing(trimmed);
if let Ok(ref mut result) = fallback_result {
trace!(
"Fallback parsing result: filter={:?}, template={:?}, field_selector={:?}",
result.filter.is_some(),
result.template.is_some(),
result.field_selector.is_some()
);
if !in_test_mode
&& result.filter.is_some()
&& result.template.is_none()
&& result.field_selector.is_none()
{
trace!("Adding default template for fallback filter-only expression");
match DSLParser::parse_dsl("[${0}]") {
Ok(default_template_dsl) => {
result.template = default_template_dsl.template;
trace!("Default template added successfully");
}
Err(e) => {
trace!("Failed to add default template: {:?}", e);
}
}
}
}
fallback_result
}
}
}
pub fn parse_separate_expressions(
filter: Option<&str>,
template: Option<&str>,
) -> Result<ParsedDSL, Box<dyn std::error::Error>> {
let mut result = ParsedDSL::new();
if let Some(filter_str) = filter {
if !filter_str.trim().is_empty() {
let filter_dsl = parse_command(filter_str)?;
result.filter = filter_dsl.filter;
}
}
if let Some(template_str) = template {
if !template_str.trim().is_empty() {
let template_dsl = parse_command(template_str)?;
result.template = template_dsl.template;
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::filter::{FilterExpr, TemplateItem};
#[test]
fn test_parse_command_field_selector() {
let result = parse_command("name").unwrap();
assert!(result.field_selector.is_some());
assert!(result.filter.is_none());
assert!(result.template.is_none());
let field = result.field_selector.unwrap();
assert_eq!(field.parts, vec!["name"]);
}
#[test]
fn test_parse_command_simple_filter() {
let result = parse_command("age > 25").unwrap();
assert!(result.filter.is_some());
assert!(result.field_selector.is_none());
assert!(result.template.is_none());
}
#[test]
fn test_parse_command_simple_template() {
let result = parse_command("{${name}}").unwrap();
assert!(result.template.is_some());
assert!(result.filter.is_none());
assert!(result.field_selector.is_none());
}
#[test]
fn test_parse_command_combined_filter_template() {
let result = parse_command("age > 25 {${name}}").unwrap();
assert!(result.filter.is_some());
assert!(result.template.is_some());
assert!(result.field_selector.is_none());
}
#[test]
fn test_new_template_syntax() {
let result = parse_command("{${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 substitution"),
}
let result = parse_command("[${name}]").unwrap();
assert!(result.template.is_some());
let result = parse_command("$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 substitution"),
}
let result = parse_command("[Hello ${name}!]").unwrap();
assert!(result.template.is_some());
let template = result.template.unwrap();
assert!(template.items.len() >= 2); }
#[test]
fn test_field_truthy_parsing() {
let result = parse_command("active?").unwrap();
assert!(result.filter.is_some());
match result.filter {
Some(FilterExpr::FieldTruthy(field)) => {
assert_eq!(field.parts, vec!["active"]);
}
_ => panic!("active? should parse as FieldTruthy"),
}
let result = parse_command("user.settings.notifications?").unwrap();
assert!(result.filter.is_some());
match result.filter {
Some(FilterExpr::FieldTruthy(field)) => {
assert_eq!(field.parts, vec!["user", "settings", "notifications"]);
}
_ => panic!("user.settings.notifications? should parse as FieldTruthy"),
}
}
#[test]
fn test_explicit_truthy_in_boolean_expressions() {
let result = parse_command("active? && verified?").unwrap();
assert!(result.filter.is_some());
match result.filter {
Some(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 two FieldTruthy in AND"),
},
_ => panic!("Expected AND expression"),
}
let result = parse_command("premium? || admin?").unwrap();
assert!(result.filter.is_some());
let result = parse_command("!suspended?").unwrap();
assert!(result.filter.is_some());
match result.filter {
Some(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"),
}
}
#[test]
fn test_not_operator_without_truthy() {
if let Ok(result) = parse_command("!active") {
assert!(result.filter.is_some());
} else {
let result = parse_command("!active?").unwrap();
assert!(result.filter.is_some());
}
if parse_command("!!verified").is_err() {
println!("Double NOT not supported in current implementation");
}
let result = parse_command("!active? && !suspended?").unwrap();
assert!(result.filter.is_some());
}
#[test]
fn test_mixed_syntax() {
let result = parse_command("active? && age > 18").unwrap();
assert!(result.filter.is_some());
let result = parse_command("name == \"Alice\" || admin?").unwrap();
assert!(result.filter.is_some());
let result = parse_command("(premium? || credits > 100) && !blacklisted?").unwrap();
assert!(result.filter.is_some());
}
#[test]
fn test_in_operator() {
let result = parse_command("status in [\"active\", \"pending\"]");
assert!(result.is_err(), "IN operator should no longer be supported");
}
#[test]
fn test_real_world_scenarios() {
let result = parse_command("authenticated? && !banned?").unwrap();
assert!(result.filter.is_some());
let result = parse_command("published? && rating >= 4.0").unwrap();
assert!(result.filter.is_some());
let result = parse_command("age >= 18 && premium_member?").unwrap();
assert!(result.filter.is_some());
}
#[test]
fn test_existing_template_preserved() {
assert!(parse_command("username").unwrap().field_selector.is_some());
assert!(
parse_command("user.profile.bio")
.unwrap()
.field_selector
.is_some()
);
assert!(
parse_command("\"field with spaces\"")
.unwrap()
.field_selector
.is_some()
);
assert!(parse_command("$name").unwrap().template.is_some());
assert!(parse_command("{${user.name}}").unwrap().template.is_some());
assert!(
parse_command("[Hello ${name}!]")
.unwrap()
.template
.is_some()
);
assert!(parse_command("age >= 21").unwrap().filter.is_some());
assert!(
parse_command("status != \"deleted\"")
.unwrap()
.filter
.is_some()
);
assert!(parse_command("score > 0.5").unwrap().filter.is_some());
let result = parse_command("score > 90 {Congrats ${name}!}").unwrap();
assert!(result.filter.is_some() && result.template.is_some());
}
#[test]
fn test_conservative_boolean_parsing() {
let test_fields = [
"a", "b", "field_1", "field_2", "name", "active", "x", "y", "z",
];
for &field1 in &test_fields {
for &field2 in &test_fields {
let command = format!("{field1}? && {field2}?");
match parse_command(&command) {
Ok(result) => {
assert!(result.filter.is_some(), "{command} should parse as filter");
}
Err(e) => panic!("{command} should work with ? syntax: {e}"),
}
let command = format!("{field1} && {field2}");
match parse_command(&command) {
Ok(result) => {
assert!(
result.filter.is_none(),
"{command} should NOT parse as filter - ambiguous"
);
}
Err(_) => {
}
}
}
}
for &field in &test_fields {
let command = format!("!{field}?"); match parse_command(&command) {
Ok(result) => {
assert!(result.filter.is_some(), "{command} should parse as filter");
}
Err(e) => panic!("{command} should work - NOT with ? is explicit: {e}"),
}
}
}
#[test]
fn test_template_variable_edge_cases() {
let result = parse_command("${0}").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!["$0"]),
_ => panic!("Expected ${{0}} to be mapped to $0 field"),
}
let result = parse_command("$0").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, "$0"),
_ => panic!("Expected $0 to be literal"),
}
let result = parse_command("$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 $20 to be literal"),
}
let result = parse_command("${1}").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!["1"]),
_ => panic!("Expected ${{1}} to be mapped to \"1\""),
}
}
#[test]
fn test_mixed_numeric_template_patterns() {
let result = parse_command("{I have $20 and ${name} has $100}").unwrap();
assert!(result.template.is_some());
let template = result.template.unwrap();
assert_eq!(template.items.len(), 5);
let result = parse_command("[Hello ${name}, you owe $25]").unwrap();
assert!(result.template.is_some());
let template = result.template.unwrap();
assert_eq!(template.items.len(), 4);
}
#[test]
fn test_quoted_string_literals() {
let result = parse_command("\"Alice\"").unwrap();
assert!(result.field_selector.is_some());
assert!(result.filter.is_none());
assert!(result.template.is_none());
let field_selector = result.field_selector.unwrap();
assert_eq!(field_selector.parts, vec!["Alice"]);
let result = parse_command("'Alice'").unwrap();
assert!(result.field_selector.is_some());
let field_selector = result.field_selector.unwrap();
assert_eq!(field_selector.parts, vec!["Alice"]);
}
#[test]
fn test_error_cases() {
let test_cases = vec!["active && verified", "field || other", "(name && active)"];
for input in test_cases {
match parse_command(input) {
Ok(result) => {
if result.filter.is_some() {
panic!("{input} should not parse as filter - ambiguous");
}
}
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("?")
|| msg.contains("truthy")
|| msg.contains("comparison")
|| msg.contains("explicit")
|| msg.contains("field name")
|| msg.contains("Template"),
"Error for '{input}' should be helpful: {msg}"
);
}
}
}
}
#[test]
fn test_complex_filters() {
let result = parse_command("name == \"Alice\" && age > 25").unwrap();
assert!(result.filter.is_some());
match result.filter {
Some(FilterExpr::And(_left, _right)) => {
println!("✓ Complex filter parsed as AND expression");
}
Some(FilterExpr::Comparison { field, .. }) => {
println!(
"Warning: Complex filter simplified to single comparison: {:?}",
field.parts
);
}
_ => {
panic!("Expected some form of filter");
}
}
}
#[test]
fn test_nested_field_access() {
let result = parse_command("user.email == \"alice@example.com\"").unwrap();
if let Some(FilterExpr::Comparison { field, .. }) = result.filter {
assert_eq!(field.parts, vec!["user", "email"]);
} else {
panic!("Expected comparison with nested field");
}
let result = parse_command("{${user.name}}").unwrap();
let template = result.template.unwrap();
assert_eq!(template.items.len(), 1);
match &template.items[0] {
TemplateItem::Field(field) => assert_eq!(field.parts, vec!["user", "name"]),
_ => panic!("Expected nested field in template"),
}
let result = parse_command("user.profile.bio").unwrap();
let field = result.field_selector.unwrap();
assert_eq!(field.parts, vec!["user", "profile", "bio"]);
}
#[test]
fn test_special_field_references() {
let result = parse_command("${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 field reference"),
}
let result = parse_command("${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 numeric field reference"),
}
}
#[test]
fn test_comprehensive_disambiguation() {
let result = parse_command("name").unwrap();
assert!(result.field_selector.is_some());
assert!(result.filter.is_none());
assert!(result.template.is_none());
let result = parse_command("name?").unwrap();
assert!(result.filter.is_some());
assert!(result.field_selector.is_none());
assert!(result.template.is_none());
let result = parse_command("$name").unwrap();
assert!(result.template.is_some());
assert!(result.filter.is_none());
assert!(result.field_selector.is_none());
let result = parse_command("{${name}}").unwrap();
assert!(result.template.is_some());
assert!(result.filter.is_none());
assert!(result.field_selector.is_none());
let result = parse_command("name == \"Alice\"").unwrap();
assert!(result.filter.is_some());
assert!(result.field_selector.is_none());
assert!(result.template.is_none());
}
#[test]
fn test_edge_cases() {
let result = parse_command("{}").unwrap();
assert!(result.template.is_some());
let result = parse_command("[]").unwrap();
assert!(result.template.is_some());
let result = parse_command("a").unwrap();
assert!(result.field_selector.is_some());
let result = parse_command("1").unwrap();
assert!(result.field_selector.is_some());
}
#[test]
fn test_bracketed_template_syntax() {
let result = parse_command("[${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 in bracketed template"),
}
let result = parse_command("[Hello ${name}!]").unwrap();
assert!(result.template.is_some());
let template = result.template.unwrap();
assert_eq!(template.items.len(), 3);
let result = parse_command("age > 25 [User: ${name}]").unwrap();
assert!(result.filter.is_some());
assert!(result.template.is_some());
}
#[test]
fn test_numeric_literal_vs_field_distinction() {
for amount in ["$0", "$1", "$5", "$10", "$20", "$100", "$999"] {
let result = parse_command(amount).unwrap();
assert!(result.template.is_some());
let template = result.template.unwrap();
match &template.items[0] {
TemplateItem::Literal(text) => assert_eq!(text, amount),
_ => panic!("Expected {amount} to be literal"),
}
}
for num in ["${1}", "${2}", "${10}", "${100}"] {
let result = parse_command(num).unwrap();
assert!(result.template.is_some());
let template = result.template.unwrap();
match &template.items[0] {
TemplateItem::Field(_) => {} _ => panic!("Expected {num} to be field reference"),
}
}
}
}