postgrest-parser 0.2.0

PostgREST URL-to-SQL parser for Rust and WASM
Documentation
use super::{common::parse_field_fallback, filter::parse_filter};
use crate::ast::{
    Field, Filter, FilterOperator, FilterValue, LogicCondition, LogicOperator, LogicTree,
};
use crate::error::ParseError;

pub fn parse_logic(key: &str, value: &str) -> Result<LogicTree, ParseError> {
    let (negated, operator) = parse_logic_key(key)?;

    let conditions_str = extract_conditions_str(value)?;
    let conditions = parse_conditions(&conditions_str)?;

    Ok(LogicTree {
        operator,
        conditions,
        negated,
    })
}

pub fn logic_key(key: &str) -> bool {
    let key_lower = key.to_lowercase();
    matches!(key_lower.as_str(), "and" | "or" | "not.and" | "not.or")
}

fn parse_logic_key(key: &str) -> Result<(bool, LogicOperator), ParseError> {
    let key_lower = key.to_lowercase();

    if let Some(rest) = key_lower.strip_prefix("not.") {
        match rest {
            "and" => Ok((true, LogicOperator::And)),
            "or" => Ok((true, LogicOperator::Or)),
            _ => Err(ParseError::InvalidLogicExpression(format!(
                "invalid key: {}",
                key
            ))),
        }
    } else {
        match key_lower.as_str() {
            "and" => Ok((false, LogicOperator::And)),
            "or" => Ok((false, LogicOperator::Or)),
            _ => Err(ParseError::InvalidLogicExpression(format!(
                "invalid key: {}",
                key
            ))),
        }
    }
}

fn extract_conditions_str(value: &str) -> Result<String, ParseError> {
    let trimmed = value.trim();

    if trimmed.starts_with('(') && trimmed.ends_with(')') {
        Ok(trimmed[1..trimmed.len() - 1].to_string())
    } else {
        Err(ParseError::LogicExpressionNotWrapped)
    }
}

fn parse_conditions(str: &str) -> Result<Vec<LogicCondition>, ParseError> {
    let parts = split_at_top_level_commas(str)?;

    parts
        .iter()
        .map(|part| parse_condition(part.trim()))
        .collect()
}

fn split_at_top_level_commas(str: &str) -> Result<Vec<String>, ParseError> {
    let mut parts = Vec::new();
    let mut current = String::new();
    let mut depth = 0;

    for c in str.chars() {
        match c {
            '(' => {
                depth += 1;
                current.push(c);
            }
            ')' => {
                depth -= 1;
                if depth < 0 {
                    return Err(ParseError::UnexpectedClosingParenthesis);
                }
                current.push(c);
            }
            ',' if depth == 0 => {
                parts.push(current.trim().to_string());
                current.clear();
            }
            _ => {
                current.push(c);
            }
        }
    }

    if !current.trim().is_empty() {
        parts.push(current.trim().to_string());
    }

    if depth > 0 {
        return Err(ParseError::UnclosedParenthesis);
    }

    Ok(parts)
}

fn parse_condition(condition_str: &str) -> Result<LogicCondition, ParseError> {
    let trimmed = condition_str.trim();

    if trimmed.is_empty() {
        return Err(ParseError::InvalidLogicExpression(
            "empty condition".to_string(),
        ));
    }

    if trimmed.starts_with("and(")
        || trimmed.starts_with("or(")
        || trimmed.starts_with("not.and(")
        || trimmed.starts_with("not.or(")
    {
        parse_nested_logic(trimmed)
    } else {
        parse_filter_condition(trimmed)
    }
}

fn parse_nested_logic(str: &str) -> Result<LogicCondition, ParseError> {
    let (negated, rest) = if let Some(stripped) = str.strip_prefix("not.") {
        (true, stripped)
    } else {
        (false, str)
    };

    let (operator, inner) = if let Some(rest) = rest.strip_prefix("and(") {
        if !rest.ends_with(')') {
            return Err(ParseError::InvalidLogicExpression(format!(
                "invalid nested logic: {}",
                str
            )));
        }
        (LogicOperator::And, &rest[..rest.len() - 1])
    } else if let Some(rest) = rest.strip_prefix("or(") {
        if !rest.ends_with(')') {
            return Err(ParseError::InvalidLogicExpression(format!(
                "invalid nested logic: {}",
                str
            )));
        }
        (LogicOperator::Or, &rest[..rest.len() - 1])
    } else {
        return Err(ParseError::InvalidLogicExpression(format!(
            "invalid nested logic: {}",
            str
        )));
    };

    let conditions = parse_conditions(inner)?;

    Ok(LogicCondition::Logic(LogicTree {
        operator,
        conditions,
        negated,
    }))
}

fn parse_filter_condition(str: &str) -> Result<LogicCondition, ParseError> {
    if str.contains('=') {
        parse_equals_notation(str)
    } else {
        parse_dot_notation(str)
    }
}

fn parse_equals_notation(str: &str) -> Result<LogicCondition, ParseError> {
    let parts: Vec<&str> = str.splitn(2, '=').collect();

    if parts.len() == 2 {
        let field_str = parts[0].trim();
        let operator_value = parts[1].trim();

        let filter = parse_filter(field_str, operator_value)?;
        Ok(LogicCondition::Filter(filter))
    } else {
        Err(ParseError::InvalidFilterFormat(format!(
            "invalid equals notation: {}",
            str
        )))
    }
}

fn parse_dot_notation(str: &str) -> Result<LogicCondition, ParseError> {
    let parts: Vec<&str> = str.split('.').collect();

    if parts.len() == 3 {
        let field_str = parts[0];
        let operator_str = parts[1];
        let value_str = parts[2];

        let operator = parse_filter_operator(operator_str)?;
        let value = FilterValue::Single(value_str.to_string());

        let field = parse_filter_field(field_str)?;

        Ok(LogicCondition::Filter(Filter {
            field,
            operator,
            value,
            quantifier: None,
            language: None,
            negated: false,
        }))
    } else if parts.len() == 4 && parts[1] == "not" {
        let field_str = parts[0];
        let operator_str = parts[2];
        let value_str = parts[3];

        let operator = parse_filter_operator(operator_str)?;
        let value = FilterValue::Single(value_str.to_string());

        let field = parse_filter_field(field_str)?;

        Ok(LogicCondition::Filter(Filter {
            field,
            operator,
            value,
            quantifier: None,
            language: None,
            negated: true,
        }))
    } else {
        Err(ParseError::InvalidFilterFormat(format!(
            "invalid dot notation: {}",
            str
        )))
    }
}

fn parse_filter_operator(op_str: &str) -> Result<FilterOperator, ParseError> {
    match op_str.to_lowercase().as_str() {
        "eq" => Ok(FilterOperator::Eq),
        "neq" => Ok(FilterOperator::Neq),
        "gt" => Ok(FilterOperator::Gt),
        "gte" => Ok(FilterOperator::Gte),
        "lt" => Ok(FilterOperator::Lt),
        "lte" => Ok(FilterOperator::Lte),
        "like" => Ok(FilterOperator::Like),
        "ilike" => Ok(FilterOperator::Ilike),
        "match" => Ok(FilterOperator::Match),
        "imatch" => Ok(FilterOperator::Imatch),
        "in" => Ok(FilterOperator::In),
        "is" => Ok(FilterOperator::Is),
        "fts" => Ok(FilterOperator::Fts),
        "plfts" => Ok(FilterOperator::Plfts),
        "phfts" => Ok(FilterOperator::Phfts),
        "wfts" => Ok(FilterOperator::Wfts),
        "cs" => Ok(FilterOperator::Cs),
        "cd" => Ok(FilterOperator::Cd),
        "ov" => Ok(FilterOperator::Ov),
        "sl" => Ok(FilterOperator::Sl),
        "sr" => Ok(FilterOperator::Sr),
        "nxl" => Ok(FilterOperator::Nxl),
        "nxr" => Ok(FilterOperator::Nxr),
        "adj" => Ok(FilterOperator::Adj),
        _ => Err(ParseError::UnknownOperator(op_str.to_string())),
    }
}

fn parse_filter_field(field_str: &str) -> Result<Field, ParseError> {
    match crate::parser::common::field(field_str) {
        Ok((_, field)) => Ok(field),
        Err(_) => parse_field_fallback(field_str),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_logic_and() {
        let result = parse_logic("and", "(id.eq.1,name.eq.john)");
        assert!(result.is_ok());
        let tree = result.unwrap();
        assert_eq!(tree.operator, LogicOperator::And);
        assert!(!tree.negated);
        assert_eq!(tree.conditions.len(), 2);
    }

    #[test]
    fn test_parse_logic_or() {
        let result = parse_logic("or", "(id.eq.1,id.eq.2)");
        assert!(result.is_ok());
        let tree = result.unwrap();
        assert_eq!(tree.operator, LogicOperator::Or);
    }

    #[test]
    fn test_parse_logic_negated() {
        let result = parse_logic("not.and", "(id.eq.1,name.eq.john)");
        assert!(result.is_ok());
        let tree = result.unwrap();
        assert!(tree.negated);
    }

    #[test]
    fn test_parse_logic_nested() {
        let result = parse_logic("and", "(id.eq.1,or(id.eq.2,id.eq.3))");
        assert!(result.is_ok());
        let tree = result.unwrap();
        assert_eq!(tree.conditions.len(), 2);

        assert!(matches!(&tree.conditions[1], LogicCondition::Logic(_)));
    }

    #[test]
    fn test_logic_key() {
        assert!(logic_key("and"));
        assert!(logic_key("or"));
        assert!(logic_key("not.and"));
        assert!(logic_key("not.or"));
        assert!(!logic_key("id"));
    }

    #[test]
    fn test_parse_condition_filter() {
        let result = parse_condition("id.eq.1");
        assert!(result.is_ok());
        assert!(matches!(result.unwrap(), LogicCondition::Filter(_)));
    }

    #[test]
    fn test_parse_condition_nested() {
        let result = parse_condition("and(id.eq.1,name.eq.john)");
        assert!(result.is_ok());
        assert!(matches!(result.unwrap(), LogicCondition::Logic(_)));
    }

    #[test]
    fn test_parse_condition_equals_notation() {
        let result = parse_condition("id=eq.1");
        assert!(result.is_ok());
        assert!(matches!(result.unwrap(), LogicCondition::Filter(_)));
    }

    #[test]
    fn test_parse_condition_invalid() {
        let result = parse_condition("invalid");
        assert!(matches!(result, Err(ParseError::InvalidFilterFormat(_))));
    }

    #[test]
    fn test_split_at_top_level_commas() {
        let result = split_at_top_level_commas("id.eq.1,name.eq.john,or(x.eq.1,y.eq.2)");
        assert!(result.is_ok());
        let parts = result.unwrap();
        assert_eq!(parts.len(), 3);
    }

    #[test]
    fn test_parse_nested_logic() {
        let result = parse_nested_logic("and(id.eq.1,name.eq.john)");
        assert!(result.is_ok());
        let condition = result.unwrap();
        assert!(matches!(condition, LogicCondition::Logic(_)));
    }

    #[test]
    fn test_parse_filter_condition_equals() {
        let result = parse_equals_notation("id=eq.1");
        assert!(result.is_ok());
        assert!(matches!(result.unwrap(), LogicCondition::Filter(_)));
    }

    #[test]
    fn test_parse_filter_condition_dot() {
        let result = parse_dot_notation("id.eq.1");
        assert!(result.is_ok());
        assert!(matches!(result.unwrap(), LogicCondition::Filter(_)));
    }
}