use crate::{ParserError, Result};
use webui_protocol::{ComparisonOperator, ConditionExpr, LogicalOperator};
pub struct ConditionParser;
impl ConditionParser {
pub fn new() -> Self {
Self
}
pub fn parse(&self, input: &str) -> Result<ConditionExpr> {
let input = input.trim();
if input.is_empty() {
return Err(ParserError::Parse("Empty condition string".to_string()));
}
if let Ok(expr) = self.parse_compound_expr(input) {
return Ok(expr);
}
if let Ok(expr) = self.parse_not_expr(input) {
return Ok(expr);
}
if let Ok(expr) = self.parse_predicate(input) {
return Ok(expr);
}
if let Ok(expr) = self.parse_identifier(input) {
return Ok(expr);
}
Err(ParserError::Parse(format!(
"Failed to parse condition: '{}'",
input
)))
}
fn parse_identifier(&self, input: &str) -> Result<ConditionExpr> {
if self.is_valid_identifier(input) {
return Ok(ConditionExpr::identifier(input));
}
Err(ParserError::Parse(format!(
"Invalid identifier value: '{}'",
input
)))
}
fn is_valid_identifier(&self, input: &str) -> bool {
if input.is_empty() {
return false;
}
let first_char = match input.chars().next() {
Some(c) => c,
None => return false,
};
if !first_char.is_alphabetic() && first_char != '_' {
return false;
}
input
.chars()
.skip(1)
.all(|c| c.is_alphanumeric() || c == '_' || c == '.')
}
fn parse_not_expr(&self, input: &str) -> Result<ConditionExpr> {
let input = input.trim();
if let Some(expr_str) = input.strip_prefix('!') {
let expr_str = expr_str.trim();
let expr = self.parse(expr_str)?;
return Ok(ConditionExpr::negated(expr));
}
Err(ParserError::Parse("Not a negation expression".to_string()))
}
fn parse_predicate(&self, input: &str) -> Result<ConditionExpr> {
let input = input.trim();
for op_str in &[">=", "<=", "==", "!=", ">", "<"] {
if let Some(pos) = input.find(op_str) {
let is_standalone = (pos == 0 || !input[..pos].ends_with(&op_str[0..1]))
&& (pos + op_str.len() == input.len()
|| !input[pos + op_str.len()..].starts_with(&op_str[0..1]));
if !is_standalone {
continue;
}
let left = input[..pos].trim();
let right = input[pos + op_str.len()..].trim();
if left.is_empty() || right.is_empty() {
continue;
}
let operator = match *op_str {
">=" => ComparisonOperator::GreaterThanOrEqual,
"<=" => ComparisonOperator::LessThanOrEqual,
"==" => ComparisonOperator::Equal,
"!=" => ComparisonOperator::NotEqual,
">" => ComparisonOperator::GreaterThan,
"<" => ComparisonOperator::LessThan,
_ => unreachable!(),
};
let predicate = ConditionExpr::predicate(left, operator, right);
return Ok(predicate);
}
}
Err(ParserError::Parse("Not a predicate expression".to_string()))
}
fn parse_compound_expr(&self, input: &str) -> Result<ConditionExpr> {
let input = input.trim();
if let Some(parts) = self.split_by_logical_op(input, &["&&", "and"], LogicalOperator::And) {
return Ok(parts);
}
if let Some(parts) = self.split_by_logical_op(input, &["||", "or"], LogicalOperator::Or) {
return Ok(parts);
}
Err(ParserError::Parse("Not a compound expression".to_string()))
}
fn split_by_logical_op(
&self,
input: &str,
operators: &[&str],
op: LogicalOperator,
) -> Option<ConditionExpr> {
let mut quote_char: Option<char> = None;
let chars: Vec<char> = input.chars().collect();
let mut operator_indices = Vec::new();
let mut i = 0;
while i < chars.len() {
match chars[i] {
'\'' | '"' => {
if quote_char.is_none() {
quote_char = Some(chars[i]);
} else if quote_char == Some(chars[i]) {
quote_char = None;
}
}
_ => {
if quote_char.is_none() {
for op_str in operators {
if i + op_str.len() <= chars.len() {
let slice = chars[i..i + op_str.len()].iter().collect::<String>();
if slice == *op_str {
let is_standalone = (i == 0 || !chars[i - 1].is_alphanumeric())
&& (i + op_str.len() == chars.len()
|| !chars[i + op_str.len()].is_alphanumeric());
if is_standalone {
operator_indices.push((i, op_str.len()));
}
}
}
}
}
}
}
i += 1;
}
if let Some((op_start, op_len)) = operator_indices.first() {
let left_str = input[..(*op_start)].trim();
let right_str = input[(*op_start + *op_len)..].trim();
if !left_str.is_empty() && !right_str.is_empty() {
if let Ok(left) = self.parse(left_str) {
if let Ok(right) = self.parse(right_str) {
return Some(ConditionExpr::compound(left, op, right));
}
}
}
}
None
}
}
impl Default for ConditionParser {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use webui_protocol::condition_expr::Expr;
#[test]
fn test_parse_identifier() {
let parser = ConditionParser::new();
let result = parser
.parse("isLoggedIn")
.expect("Failed to parse identifier");
assert!(matches!(&result.expr, Some(Expr::Identifier(id)) if id.value == "isLoggedIn"));
let result = parser
.parse("user.isAdmin")
.expect("Failed to parse dotted identifier");
assert!(matches!(&result.expr, Some(Expr::Identifier(id)) if id.value == "user.isAdmin"));
}
#[test]
fn test_parse_not() {
let parser = ConditionParser::new();
let result = parser
.parse("!isLoggedIn")
.expect("Failed to parse NOT expression");
assert!(matches!(&result.expr, Some(Expr::Not(not)) if
matches!(not.condition.as_ref().and_then(|c| c.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "isLoggedIn")
));
let result = parser
.parse("! isLoggedIn")
.expect("Failed to parse NOT expression with space");
assert!(matches!(&result.expr, Some(Expr::Not(_))));
}
#[test]
fn test_parse_predicate() {
let parser = ConditionParser::new();
let test_cases = vec![
("age > 18", ComparisonOperator::GreaterThan),
("age < 65", ComparisonOperator::LessThan),
("name == 'John'", ComparisonOperator::Equal),
("status != 'inactive'", ComparisonOperator::NotEqual),
("score >= 90", ComparisonOperator::GreaterThanOrEqual),
("score <= 100", ComparisonOperator::LessThanOrEqual),
];
for (input, expected_op) in test_cases {
let result = parser
.parse(input)
.expect("Failed to parse predicate expression");
assert!(
matches!(&result.expr, Some(Expr::Predicate(pred)) if ComparisonOperator::try_from(pred.operator) == Ok(expected_op))
);
}
let result = parser
.parse("name == \"John\"")
.expect("Failed to parse string comparison");
assert!(matches!(&result.expr, Some(Expr::Predicate(pred)) if
pred.left == "name" &&
ComparisonOperator::try_from(pred.operator) == Ok(ComparisonOperator::Equal) &&
pred.right == "\"John\""
));
}
#[test]
fn test_parse_compound() {
let parser = ConditionParser::new();
let result = parser
.parse("isLoggedIn && age > 18")
.expect("Failed to parse compound AND expression");
assert!(matches!(&result.expr, Some(Expr::Compound(compound)) if
matches!(compound.left.as_ref().and_then(|l| l.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "isLoggedIn") &&
LogicalOperator::try_from(compound.op) == Ok(LogicalOperator::And) &&
matches!(compound.right.as_ref().and_then(|r| r.expr.as_ref()), Some(Expr::Predicate(_)))
));
let result = parser
.parse("status == 'premium' or isAdmin")
.expect("Failed to parse compound OR expression");
assert!(matches!(&result.expr, Some(Expr::Compound(compound)) if
matches!(compound.left.as_ref().and_then(|l| l.expr.as_ref()), Some(Expr::Predicate(_))) &&
LogicalOperator::try_from(compound.op) == Ok(LogicalOperator::Or) &&
matches!(compound.right.as_ref().and_then(|r| r.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "isAdmin")
));
}
#[test]
fn test_complex_expressions() {
let parser = ConditionParser::new();
let result = parser
.parse("age > 18 && isVerified || isAdmin")
.expect("Failed to parse complex expression");
assert!(matches!(&result.expr, Some(Expr::Compound(_))));
let result = parser
.parse("!isVerified")
.expect("Failed to parse NOT expression");
assert!(matches!(&result.expr, Some(Expr::Not(_))));
}
#[test]
fn test_invalid_expressions() {
let parser = ConditionParser::new();
assert!(parser.parse("").is_err());
assert!(parser.parse("age << 18").is_err());
assert!(parser.parse("age >").is_err());
assert!(parser.parse("&& isAdmin").is_err());
}
#[test]
fn test_tokenize_simple_identifier() {
let parser = ConditionParser::new();
let result = parser
.parse("isVisible")
.expect("Failed to parse simple identifier");
assert!(matches!(&result.expr, Some(Expr::Identifier(id)) if id.value == "isVisible"));
}
#[test]
fn test_tokenize_dotted_path() {
let parser = ConditionParser::new();
let result = parser
.parse("appearance")
.expect("Failed to parse identifier");
assert!(matches!(&result.expr, Some(Expr::Identifier(id)) if id.value == "appearance"));
}
#[test]
fn test_tokenize_binary_expression() {
let parser = ConditionParser::new();
let result = parser
.parse("x > 5")
.expect("Failed to parse binary expression");
assert!(matches!(&result.expr, Some(Expr::Predicate(pred)) if
pred.left == "x" &&
ComparisonOperator::try_from(pred.operator) == Ok(ComparisonOperator::GreaterThan) &&
pred.right == "5"
));
}
#[test]
fn test_tokenize_complex_and_dotted() {
let parser = ConditionParser::new();
let result = parser
.parse("appearance == \"hub\" && actions.trailing")
.expect("Failed to parse complex and-dotted expression");
assert!(matches!(&result.expr, Some(Expr::Compound(compound)) if
matches!(compound.left.as_ref().and_then(|l| l.expr.as_ref()), Some(Expr::Predicate(pred)) if
pred.left == "appearance" &&
ComparisonOperator::try_from(pred.operator) == Ok(ComparisonOperator::Equal) &&
pred.right == "\"hub\""
) &&
LogicalOperator::try_from(compound.op) == Ok(LogicalOperator::And) &&
matches!(compound.right.as_ref().and_then(|r| r.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "actions.trailing")
));
}
#[test]
fn test_tokenize_unary_not() {
let parser = ConditionParser::new();
let result = parser
.parse("!disabled")
.expect("Failed to parse unary not expression");
assert!(matches!(&result.expr, Some(Expr::Not(not)) if
matches!(not.condition.as_ref().and_then(|c| c.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "disabled")
));
}
#[test]
fn test_tokenize_undefined_comparison() {
let parser = ConditionParser::new();
let result = parser
.parse("foo == undefined")
.expect("Failed to parse undefined comparison");
assert!(matches!(&result.expr, Some(Expr::Predicate(pred)) if
pred.left == "foo" &&
ComparisonOperator::try_from(pred.operator) == Ok(ComparisonOperator::Equal) &&
pred.right == "undefined"
));
}
#[test]
fn test_reject_mixed_and_or() {
let parser = ConditionParser::new();
let result = parser
.parse("a && b || c")
.expect("Rust parser handles mixed operators via nesting");
assert!(matches!(&result.expr, Some(Expr::Compound(compound)) if
matches!(compound.left.as_ref().and_then(|l| l.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "a") &&
LogicalOperator::try_from(compound.op) == Ok(LogicalOperator::And) &&
matches!(compound.right.as_ref().and_then(|r| r.expr.as_ref()), Some(Expr::Compound(inner)) if
matches!(inner.left.as_ref().and_then(|l| l.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "b") &&
LogicalOperator::try_from(inner.op) == Ok(LogicalOperator::Or) &&
matches!(inner.right.as_ref().and_then(|r| r.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "c")
)
));
}
#[test]
fn test_reject_too_many_tokens() {
let parser = ConditionParser::new();
let result = parser
.parse("a && b && c && d && e && f && g && h && i && j")
.expect("Parser handles long chained expressions");
assert!(matches!(&result.expr, Some(Expr::Compound(compound)) if
matches!(compound.left.as_ref().and_then(|l| l.expr.as_ref()), Some(Expr::Identifier(id)) if id.value == "a") &&
LogicalOperator::try_from(compound.op) == Ok(LogicalOperator::And) &&
matches!(compound.right.as_ref().and_then(|r| r.expr.as_ref()), Some(Expr::Compound(_)))
));
}
}