use std::fmt;
use serde_json::Value;
#[derive(Debug, Clone, PartialEq)]
pub enum ASTExpr {
And(Vec<ASTExpr>),
Or(Vec<ASTExpr>),
Not(Box<ASTExpr>),
Compare { field: String, op: CompareOp },
}
#[derive(Debug, Clone, PartialEq)]
pub enum CompareOp {
Eq(Value), Ne(Value), Lt(f64), Lte(f64), Gt(f64), Gte(f64), }
impl fmt::Display for CompareOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CompareOp::Eq(_) => write!(f, "=="),
CompareOp::Ne(_) => write!(f, "!="),
CompareOp::Lt(_) => write!(f, "<"),
CompareOp::Lte(_) => write!(f, "<="),
CompareOp::Gt(_) => write!(f, ">"),
CompareOp::Gte(_) => write!(f, ">="),
}
}
}
pub trait ASTVisitor {
type Output;
fn visit(&mut self, expr: &ASTExpr) -> Self::Output {
match expr {
ASTExpr::And(exprs) => self.visit_and(exprs),
ASTExpr::Or(exprs) => self.visit_or(exprs),
ASTExpr::Not(expr) => self.visit_not(expr),
ASTExpr::Compare { field, op } => self.visit_compare(field, op),
}
}
fn visit_and(&mut self, exprs: &[ASTExpr]) -> Self::Output;
fn visit_or(&mut self, exprs: &[ASTExpr]) -> Self::Output;
fn visit_not(&mut self, expr: &ASTExpr) -> Self::Output;
fn visit_compare(&mut self, field: &str, op: &CompareOp) -> Self::Output;
}
impl ASTExpr {
pub fn accept<V: ASTVisitor>(&self, visitor: &mut V) -> V::Output {
visitor.visit(self)
}
}
pub struct PrintVisitor {
indent_level: usize,
indent_str: String,
}
impl Default for PrintVisitor {
fn default() -> Self {
Self::new()
}
}
impl PrintVisitor {
pub fn new() -> Self {
Self {
indent_level: 0,
indent_str: " ".to_string(),
}
}
pub fn with_indent(indent_str: &str) -> Self {
Self {
indent_level: 0,
indent_str: indent_str.to_string(),
}
}
fn indent(&self) -> String {
self.indent_str.repeat(self.indent_level)
}
fn value_to_string(value: &Value) -> String {
match value {
Value::String(s) => format!("\"{}\"", s.replace('\"', "\\\"")),
Value::Array(arr) => {
let items: Vec<String> = arr.iter().map(Self::value_to_string).collect();
format!("[{}]", items.join(", "))
}
_ => value.to_string(),
}
}
}
impl ASTVisitor for PrintVisitor {
type Output = String;
fn visit_and(&mut self, exprs: &[ASTExpr]) -> Self::Output {
if exprs.is_empty() {
return "true".to_string();
}
if exprs.len() == 1 {
return self.visit(&exprs[0]);
}
let current_indent = self.indent();
self.indent_level += 1;
let inner: Vec<String> = exprs
.iter()
.map(|expr| format!("\n{}{}", self.indent(), self.visit(expr)))
.collect();
self.indent_level -= 1;
format!("AND({}\n{})", inner.join(","), current_indent)
}
fn visit_or(&mut self, exprs: &[ASTExpr]) -> Self::Output {
if exprs.is_empty() {
return "false".to_string();
}
if exprs.len() == 1 {
return self.visit(&exprs[0]);
}
let current_indent = self.indent();
self.indent_level += 1;
let inner: Vec<String> = exprs
.iter()
.map(|expr| format!("\n{}{}", self.indent(), self.visit(expr)))
.collect();
self.indent_level -= 1;
format!("OR({}\n{})", inner.join(","), current_indent)
}
fn visit_not(&mut self, expr: &ASTExpr) -> Self::Output {
format!("NOT({})", self.visit(expr))
}
fn visit_compare(&mut self, field: &str, op: &CompareOp) -> Self::Output {
let value_str = match op {
CompareOp::Eq(value) => Self::value_to_string(value),
CompareOp::Ne(value) => Self::value_to_string(value),
CompareOp::Lt(num) => num.to_string(),
CompareOp::Lte(num) => num.to_string(),
CompareOp::Gt(num) => num.to_string(),
CompareOp::Gte(num) => num.to_string(),
};
format!("{}{}{}", field, op, value_str)
}
}
impl fmt::Display for ASTExpr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut visitor = PrintVisitor::new();
write!(f, "{}", self.accept(&mut visitor))
}
}
impl ASTExpr {
pub fn to_string_with_indent(&self, indent: &str) -> String {
let mut visitor = PrintVisitor::with_indent(indent);
self.accept(&mut visitor)
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn test_ast_visitor() {
let expr = ASTExpr::Compare {
field: "age".to_string(),
op: CompareOp::Gt(30.0),
};
assert_eq!(expr.to_string(), "age>30");
let and_expr = ASTExpr::And(vec![
ASTExpr::Compare {
field: "age".to_string(),
op: CompareOp::Gt(30.0),
},
ASTExpr::Compare {
field: "name".to_string(),
op: CompareOp::Eq(json!("John")),
},
]);
let expected_and = "AND(\n age>30,\n name==\"John\"\n)";
assert_eq!(and_expr.to_string(), expected_and);
let or_expr = ASTExpr::Or(vec![
ASTExpr::Compare {
field: "age".to_string(),
op: CompareOp::Gt(30.0),
},
ASTExpr::Compare {
field: "name".to_string(),
op: CompareOp::Eq(json!("John")),
},
]);
let expected_or = "OR(\n age>30,\n name==\"John\"\n)";
assert_eq!(or_expr.to_string(), expected_or);
let not_expr = ASTExpr::Not(Box::new(ASTExpr::Compare {
field: "age".to_string(),
op: CompareOp::Lt(18.0),
}));
assert_eq!(not_expr.to_string(), "NOT(age<18)");
let nested_expr = ASTExpr::And(vec![
ASTExpr::Or(vec![
ASTExpr::Compare {
field: "age".to_string(),
op: CompareOp::Gt(30.0),
},
ASTExpr::Compare {
field: "age".to_string(),
op: CompareOp::Lt(20.0),
},
]),
ASTExpr::Not(Box::new(ASTExpr::Compare {
field: "name".to_string(),
op: CompareOp::Eq(json!("Admin")),
})),
]);
let expected_nested = "AND(\n OR(\n age>30,\n age<20\n ),\n NOT(name==\"Admin\")\n)";
assert_eq!(nested_expr.to_string(), expected_nested);
}
}