use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum FilterExpr {
LiteralString(String),
LiteralInt(i64),
LiteralFloat(f64),
LiteralBool(bool),
LiteralArray(Vec<FilterExpr>),
Field(String),
Eq(Box<FilterExpr>, Box<FilterExpr>),
Ne(Box<FilterExpr>, Box<FilterExpr>),
Lt(Box<FilterExpr>, Box<FilterExpr>),
Le(Box<FilterExpr>, Box<FilterExpr>),
Gt(Box<FilterExpr>, Box<FilterExpr>),
Ge(Box<FilterExpr>, Box<FilterExpr>),
Contains(Box<FilterExpr>, Box<FilterExpr>),
StartsWith(Box<FilterExpr>, Box<FilterExpr>),
EndsWith(Box<FilterExpr>, Box<FilterExpr>),
Like(Box<FilterExpr>, Box<FilterExpr>),
In(Box<FilterExpr>, Box<FilterExpr>),
NotIn(Box<FilterExpr>, Box<FilterExpr>),
Any(Box<FilterExpr>, Box<FilterExpr>),
All(Box<FilterExpr>, Box<FilterExpr>),
None(Box<FilterExpr>, Box<FilterExpr>),
Between(Box<FilterExpr>, Box<FilterExpr>, Box<FilterExpr>),
And(Box<FilterExpr>, Box<FilterExpr>),
Or(Box<FilterExpr>, Box<FilterExpr>),
Not(Box<FilterExpr>),
IsNull(Box<FilterExpr>),
IsNotNull(Box<FilterExpr>),
}
impl FilterExpr {
#[must_use]
pub fn is_literal(&self) -> bool {
matches!(
self,
FilterExpr::LiteralString(_)
| FilterExpr::LiteralInt(_)
| FilterExpr::LiteralFloat(_)
| FilterExpr::LiteralBool(_)
| FilterExpr::LiteralArray(_)
)
}
#[must_use]
pub fn is_field(&self) -> bool {
matches!(self, FilterExpr::Field(_))
}
#[must_use]
pub fn is_comparison(&self) -> bool {
matches!(
self,
FilterExpr::Eq(_, _)
| FilterExpr::Ne(_, _)
| FilterExpr::Lt(_, _)
| FilterExpr::Le(_, _)
| FilterExpr::Gt(_, _)
| FilterExpr::Ge(_, _)
)
}
#[must_use]
pub fn is_string_op(&self) -> bool {
matches!(
self,
FilterExpr::Contains(_, _)
| FilterExpr::StartsWith(_, _)
| FilterExpr::EndsWith(_, _)
| FilterExpr::Like(_, _)
)
}
#[must_use]
pub fn is_array_op(&self) -> bool {
matches!(
self,
FilterExpr::In(_, _)
| FilterExpr::NotIn(_, _)
| FilterExpr::Any(_, _)
| FilterExpr::All(_, _)
| FilterExpr::None(_, _)
)
}
#[must_use]
pub fn is_logical(&self) -> bool {
matches!(
self,
FilterExpr::And(_, _) | FilterExpr::Or(_, _) | FilterExpr::Not(_)
)
}
#[must_use]
pub fn is_null_check(&self) -> bool {
matches!(self, FilterExpr::IsNull(_) | FilterExpr::IsNotNull(_))
}
#[must_use]
pub fn as_string(&self) -> Option<&str> {
match self {
FilterExpr::LiteralString(s) => Some(s),
_ => Option::None,
}
}
#[must_use]
pub fn as_int(&self) -> Option<i64> {
match self {
FilterExpr::LiteralInt(i) => Some(*i),
_ => Option::None,
}
}
#[must_use]
pub fn as_float(&self) -> Option<f64> {
match self {
FilterExpr::LiteralFloat(f) => Some(*f),
_ => Option::None,
}
}
#[must_use]
pub fn as_bool(&self) -> Option<bool> {
match self {
FilterExpr::LiteralBool(b) => Some(*b),
_ => Option::None,
}
}
#[must_use]
pub fn as_field(&self) -> Option<&str> {
match self {
FilterExpr::Field(name) => Some(name),
_ => Option::None,
}
}
#[must_use]
pub fn as_array(&self) -> Option<&[FilterExpr]> {
match self {
FilterExpr::LiteralArray(arr) => Some(arr),
_ => Option::None,
}
}
#[must_use]
pub fn operator_name(&self) -> &'static str {
match self {
FilterExpr::LiteralString(_) => "LiteralString",
FilterExpr::LiteralInt(_) => "LiteralInt",
FilterExpr::LiteralFloat(_) => "LiteralFloat",
FilterExpr::LiteralBool(_) => "LiteralBool",
FilterExpr::LiteralArray(_) => "LiteralArray",
FilterExpr::Field(_) => "Field",
FilterExpr::Eq(_, _) => "Eq",
FilterExpr::Ne(_, _) => "Ne",
FilterExpr::Lt(_, _) => "Lt",
FilterExpr::Le(_, _) => "Le",
FilterExpr::Gt(_, _) => "Gt",
FilterExpr::Ge(_, _) => "Ge",
FilterExpr::Contains(_, _) => "Contains",
FilterExpr::StartsWith(_, _) => "StartsWith",
FilterExpr::EndsWith(_, _) => "EndsWith",
FilterExpr::Like(_, _) => "Like",
FilterExpr::In(_, _) => "In",
FilterExpr::NotIn(_, _) => "NotIn",
FilterExpr::Any(_, _) => "Any",
FilterExpr::All(_, _) => "All",
FilterExpr::None(_, _) => "None",
FilterExpr::Between(_, _, _) => "Between",
FilterExpr::And(_, _) => "And",
FilterExpr::Or(_, _) => "Or",
FilterExpr::Not(_) => "Not",
FilterExpr::IsNull(_) => "IsNull",
FilterExpr::IsNotNull(_) => "IsNotNull",
}
}
#[must_use]
pub fn depth(&self) -> usize {
match self {
FilterExpr::LiteralString(_)
| FilterExpr::LiteralInt(_)
| FilterExpr::LiteralFloat(_)
| FilterExpr::LiteralBool(_)
| FilterExpr::Field(_) => 1,
FilterExpr::LiteralArray(arr) => {
1 + arr.iter().map(FilterExpr::depth).max().unwrap_or(0)
}
FilterExpr::Not(expr) | FilterExpr::IsNull(expr) | FilterExpr::IsNotNull(expr) => {
1 + expr.depth()
}
FilterExpr::Eq(l, r)
| FilterExpr::Ne(l, r)
| FilterExpr::Lt(l, r)
| FilterExpr::Le(l, r)
| FilterExpr::Gt(l, r)
| FilterExpr::Ge(l, r)
| FilterExpr::Contains(l, r)
| FilterExpr::StartsWith(l, r)
| FilterExpr::EndsWith(l, r)
| FilterExpr::Like(l, r)
| FilterExpr::In(l, r)
| FilterExpr::NotIn(l, r)
| FilterExpr::Any(l, r)
| FilterExpr::All(l, r)
| FilterExpr::None(l, r)
| FilterExpr::And(l, r)
| FilterExpr::Or(l, r) => 1 + l.depth().max(r.depth()),
FilterExpr::Between(field, low, high) => {
1 + field.depth().max(low.depth()).max(high.depth())
}
}
}
#[must_use]
pub fn referenced_fields(&self) -> Vec<&str> {
let mut fields = Vec::new();
self.collect_fields(&mut fields);
fields
}
fn collect_fields<'a>(&'a self, fields: &mut Vec<&'a str>) {
match self {
FilterExpr::Field(name) => fields.push(name),
FilterExpr::LiteralArray(arr) => {
for elem in arr {
elem.collect_fields(fields);
}
}
FilterExpr::Not(expr) | FilterExpr::IsNull(expr) | FilterExpr::IsNotNull(expr) => {
expr.collect_fields(fields);
}
FilterExpr::Eq(l, r)
| FilterExpr::Ne(l, r)
| FilterExpr::Lt(l, r)
| FilterExpr::Le(l, r)
| FilterExpr::Gt(l, r)
| FilterExpr::Ge(l, r)
| FilterExpr::Contains(l, r)
| FilterExpr::StartsWith(l, r)
| FilterExpr::EndsWith(l, r)
| FilterExpr::Like(l, r)
| FilterExpr::In(l, r)
| FilterExpr::NotIn(l, r)
| FilterExpr::Any(l, r)
| FilterExpr::All(l, r)
| FilterExpr::None(l, r)
| FilterExpr::And(l, r)
| FilterExpr::Or(l, r) => {
l.collect_fields(fields);
r.collect_fields(fields);
}
FilterExpr::Between(field, low, high) => {
field.collect_fields(fields);
low.collect_fields(fields);
high.collect_fields(fields);
}
FilterExpr::LiteralString(_)
| FilterExpr::LiteralInt(_)
| FilterExpr::LiteralFloat(_)
| FilterExpr::LiteralBool(_) => {}
}
}
}
#[cfg(test)]
#[allow(clippy::approx_constant)] #[allow(clippy::manual_string_new)] mod tests {
use super::*;
#[test]
fn test_filter_expr_has_27_variants() {
let variants: Vec<FilterExpr> = vec![
FilterExpr::LiteralString("test".to_string()),
FilterExpr::LiteralInt(42),
FilterExpr::LiteralFloat(3.14),
FilterExpr::LiteralBool(true),
FilterExpr::LiteralArray(vec![]),
FilterExpr::Field("name".to_string()),
FilterExpr::Eq(
Box::new(FilterExpr::Field("x".to_string())),
Box::new(FilterExpr::LiteralInt(1)),
),
FilterExpr::Ne(
Box::new(FilterExpr::Field("x".to_string())),
Box::new(FilterExpr::LiteralInt(1)),
),
FilterExpr::Lt(
Box::new(FilterExpr::Field("x".to_string())),
Box::new(FilterExpr::LiteralInt(1)),
),
FilterExpr::Le(
Box::new(FilterExpr::Field("x".to_string())),
Box::new(FilterExpr::LiteralInt(1)),
),
FilterExpr::Gt(
Box::new(FilterExpr::Field("x".to_string())),
Box::new(FilterExpr::LiteralInt(1)),
),
FilterExpr::Ge(
Box::new(FilterExpr::Field("x".to_string())),
Box::new(FilterExpr::LiteralInt(1)),
),
FilterExpr::Contains(
Box::new(FilterExpr::Field("x".to_string())),
Box::new(FilterExpr::LiteralString("a".to_string())),
),
FilterExpr::StartsWith(
Box::new(FilterExpr::Field("x".to_string())),
Box::new(FilterExpr::LiteralString("a".to_string())),
),
FilterExpr::EndsWith(
Box::new(FilterExpr::Field("x".to_string())),
Box::new(FilterExpr::LiteralString("a".to_string())),
),
FilterExpr::Like(
Box::new(FilterExpr::Field("x".to_string())),
Box::new(FilterExpr::LiteralString("a%".to_string())),
),
FilterExpr::In(
Box::new(FilterExpr::Field("x".to_string())),
Box::new(FilterExpr::LiteralArray(vec![])),
),
FilterExpr::NotIn(
Box::new(FilterExpr::Field("x".to_string())),
Box::new(FilterExpr::LiteralArray(vec![])),
),
FilterExpr::Any(
Box::new(FilterExpr::Field("x".to_string())),
Box::new(FilterExpr::LiteralArray(vec![])),
),
FilterExpr::All(
Box::new(FilterExpr::Field("x".to_string())),
Box::new(FilterExpr::LiteralArray(vec![])),
),
FilterExpr::None(
Box::new(FilterExpr::Field("x".to_string())),
Box::new(FilterExpr::LiteralArray(vec![])),
),
FilterExpr::Between(
Box::new(FilterExpr::Field("x".to_string())),
Box::new(FilterExpr::LiteralInt(0)),
Box::new(FilterExpr::LiteralInt(100)),
),
FilterExpr::And(
Box::new(FilterExpr::LiteralBool(true)),
Box::new(FilterExpr::LiteralBool(false)),
),
FilterExpr::Or(
Box::new(FilterExpr::LiteralBool(true)),
Box::new(FilterExpr::LiteralBool(false)),
),
FilterExpr::Not(Box::new(FilterExpr::LiteralBool(true))),
FilterExpr::IsNull(Box::new(FilterExpr::Field("x".to_string()))),
FilterExpr::IsNotNull(Box::new(FilterExpr::Field("x".to_string()))),
];
assert_eq!(
variants.len(),
27,
"FilterExpr should have exactly 27 variants"
);
}
#[test]
fn test_derive_debug() {
let expr = FilterExpr::LiteralString("test".to_string());
let debug_str = format!("{expr:?}");
assert!(debug_str.contains("LiteralString"));
}
#[test]
fn test_derive_clone() {
let expr = FilterExpr::And(
Box::new(FilterExpr::Field("category".to_string())),
Box::new(FilterExpr::LiteralBool(true)),
);
let cloned = expr.clone();
assert_eq!(expr, cloned);
}
#[test]
fn test_derive_partial_eq() {
let expr1 = FilterExpr::LiteralInt(42);
let expr2 = FilterExpr::LiteralInt(42);
let expr3 = FilterExpr::LiteralInt(43);
assert_eq!(expr1, expr2);
assert_ne!(expr1, expr3);
}
#[test]
fn test_derive_serialize_deserialize() {
let expr = FilterExpr::Eq(
Box::new(FilterExpr::Field("category".to_string())),
Box::new(FilterExpr::LiteralString("gpu".to_string())),
);
let json = serde_json::to_string(&expr).unwrap();
let parsed: FilterExpr = serde_json::from_str(&json).unwrap();
assert_eq!(expr, parsed);
}
#[test]
fn test_box_recursion() {
let mut expr = FilterExpr::LiteralBool(true);
for _ in 0..100 {
expr = FilterExpr::Not(Box::new(expr));
}
assert_eq!(expr.depth(), 101);
}
#[test]
fn test_is_literal() {
assert!(FilterExpr::LiteralString("test".to_string()).is_literal());
assert!(FilterExpr::LiteralInt(42).is_literal());
assert!(FilterExpr::LiteralFloat(3.14).is_literal());
assert!(FilterExpr::LiteralBool(true).is_literal());
assert!(FilterExpr::LiteralArray(vec![]).is_literal());
assert!(!FilterExpr::Field("x".to_string()).is_literal());
}
#[test]
fn test_is_field() {
assert!(FilterExpr::Field("name".to_string()).is_field());
assert!(!FilterExpr::LiteralString("name".to_string()).is_field());
}
#[test]
fn test_is_comparison() {
let field = Box::new(FilterExpr::Field("x".to_string()));
let value = Box::new(FilterExpr::LiteralInt(1));
assert!(FilterExpr::Eq(field.clone(), value.clone()).is_comparison());
assert!(FilterExpr::Ne(field.clone(), value.clone()).is_comparison());
assert!(FilterExpr::Lt(field.clone(), value.clone()).is_comparison());
assert!(FilterExpr::Le(field.clone(), value.clone()).is_comparison());
assert!(FilterExpr::Gt(field.clone(), value.clone()).is_comparison());
assert!(FilterExpr::Ge(field, value).is_comparison());
assert!(!FilterExpr::LiteralBool(true).is_comparison());
}
#[test]
fn test_is_string_op() {
let field = Box::new(FilterExpr::Field("x".to_string()));
let value = Box::new(FilterExpr::LiteralString("a".to_string()));
assert!(FilterExpr::Contains(field.clone(), value.clone()).is_string_op());
assert!(FilterExpr::StartsWith(field.clone(), value.clone()).is_string_op());
assert!(FilterExpr::EndsWith(field.clone(), value.clone()).is_string_op());
assert!(FilterExpr::Like(field, value).is_string_op());
}
#[test]
fn test_is_array_op() {
let field = Box::new(FilterExpr::Field("x".to_string()));
let arr = Box::new(FilterExpr::LiteralArray(vec![]));
assert!(FilterExpr::In(field.clone(), arr.clone()).is_array_op());
assert!(FilterExpr::NotIn(field.clone(), arr.clone()).is_array_op());
assert!(FilterExpr::Any(field.clone(), arr.clone()).is_array_op());
assert!(FilterExpr::All(field.clone(), arr.clone()).is_array_op());
assert!(FilterExpr::None(field, arr).is_array_op());
}
#[test]
fn test_is_logical() {
assert!(FilterExpr::And(
Box::new(FilterExpr::LiteralBool(true)),
Box::new(FilterExpr::LiteralBool(false))
)
.is_logical());
assert!(FilterExpr::Or(
Box::new(FilterExpr::LiteralBool(true)),
Box::new(FilterExpr::LiteralBool(false))
)
.is_logical());
assert!(FilterExpr::Not(Box::new(FilterExpr::LiteralBool(true))).is_logical());
}
#[test]
fn test_is_null_check() {
assert!(FilterExpr::IsNull(Box::new(FilterExpr::Field("x".to_string()))).is_null_check());
assert!(
FilterExpr::IsNotNull(Box::new(FilterExpr::Field("x".to_string()))).is_null_check()
);
}
#[test]
fn test_as_string() {
assert_eq!(
FilterExpr::LiteralString("hello".to_string()).as_string(),
Some("hello")
);
assert_eq!(FilterExpr::LiteralInt(42).as_string(), Option::None);
}
#[test]
fn test_as_int() {
assert_eq!(FilterExpr::LiteralInt(42).as_int(), Some(42));
assert_eq!(FilterExpr::LiteralFloat(42.0).as_int(), Option::None);
}
#[test]
fn test_as_float() {
assert_eq!(FilterExpr::LiteralFloat(3.14).as_float(), Some(3.14));
assert_eq!(FilterExpr::LiteralInt(3).as_float(), Option::None);
}
#[test]
fn test_as_bool() {
assert_eq!(FilterExpr::LiteralBool(true).as_bool(), Some(true));
assert_eq!(FilterExpr::LiteralInt(1).as_bool(), Option::None);
}
#[test]
fn test_as_field() {
assert_eq!(
FilterExpr::Field("category".to_string()).as_field(),
Some("category")
);
assert_eq!(
FilterExpr::LiteralString("category".to_string()).as_field(),
Option::None
);
}
#[test]
fn test_as_array() {
let arr = vec![FilterExpr::LiteralInt(1), FilterExpr::LiteralInt(2)];
let expr = FilterExpr::LiteralArray(arr.clone());
assert_eq!(expr.as_array(), Some(&arr[..]));
assert_eq!(FilterExpr::LiteralInt(1).as_array(), Option::None);
}
#[test]
fn test_operator_name() {
assert_eq!(
FilterExpr::LiteralString("".to_string()).operator_name(),
"LiteralString"
);
assert_eq!(FilterExpr::LiteralInt(0).operator_name(), "LiteralInt");
assert_eq!(FilterExpr::Field("x".to_string()).operator_name(), "Field");
assert_eq!(
FilterExpr::And(
Box::new(FilterExpr::LiteralBool(true)),
Box::new(FilterExpr::LiteralBool(false))
)
.operator_name(),
"And"
);
}
#[test]
fn test_depth_literal() {
assert_eq!(FilterExpr::LiteralInt(42).depth(), 1);
assert_eq!(FilterExpr::LiteralString("test".to_string()).depth(), 1);
}
#[test]
fn test_depth_unary() {
let expr = FilterExpr::Not(Box::new(FilterExpr::LiteralBool(true)));
assert_eq!(expr.depth(), 2);
}
#[test]
fn test_depth_binary() {
let expr = FilterExpr::And(
Box::new(FilterExpr::LiteralBool(true)),
Box::new(FilterExpr::Not(Box::new(FilterExpr::LiteralBool(false)))),
);
assert_eq!(expr.depth(), 3);
}
#[test]
fn test_depth_ternary() {
let expr = FilterExpr::Between(
Box::new(FilterExpr::Field("x".to_string())),
Box::new(FilterExpr::LiteralInt(0)),
Box::new(FilterExpr::LiteralInt(100)),
);
assert_eq!(expr.depth(), 2);
}
#[test]
fn test_depth_array() {
let expr =
FilterExpr::LiteralArray(vec![FilterExpr::LiteralInt(1), FilterExpr::LiteralInt(2)]);
assert_eq!(expr.depth(), 2);
}
#[test]
fn test_depth_empty_array() {
let expr = FilterExpr::LiteralArray(vec![]);
assert_eq!(expr.depth(), 1);
}
#[test]
fn test_referenced_fields_single() {
let expr = FilterExpr::Eq(
Box::new(FilterExpr::Field("category".to_string())),
Box::new(FilterExpr::LiteralString("gpu".to_string())),
);
assert_eq!(expr.referenced_fields(), vec!["category"]);
}
#[test]
fn test_referenced_fields_multiple() {
let expr = FilterExpr::And(
Box::new(FilterExpr::Eq(
Box::new(FilterExpr::Field("category".to_string())),
Box::new(FilterExpr::LiteralString("gpu".to_string())),
)),
Box::new(FilterExpr::Lt(
Box::new(FilterExpr::Field("price".to_string())),
Box::new(FilterExpr::LiteralInt(500)),
)),
);
let fields = expr.referenced_fields();
assert_eq!(fields.len(), 2);
assert!(fields.contains(&"category"));
assert!(fields.contains(&"price"));
}
#[test]
fn test_referenced_fields_no_fields() {
let expr = FilterExpr::LiteralBool(true);
assert!(expr.referenced_fields().is_empty());
}
#[test]
fn test_serialization_complex() {
let expr = FilterExpr::And(
Box::new(FilterExpr::Eq(
Box::new(FilterExpr::Field("category".to_string())),
Box::new(FilterExpr::LiteralString("gpu".to_string())),
)),
Box::new(FilterExpr::Or(
Box::new(FilterExpr::Lt(
Box::new(FilterExpr::Field("price".to_string())),
Box::new(FilterExpr::LiteralInt(500)),
)),
Box::new(FilterExpr::In(
Box::new(FilterExpr::Field("brand".to_string())),
Box::new(FilterExpr::LiteralArray(vec![
FilterExpr::LiteralString("nvidia".to_string()),
FilterExpr::LiteralString("amd".to_string()),
])),
)),
)),
);
let json = serde_json::to_string(&expr).unwrap();
let parsed: FilterExpr = serde_json::from_str(&json).unwrap();
assert_eq!(expr, parsed);
}
#[test]
fn test_serialization_all_literals() {
let exprs = vec![
FilterExpr::LiteralString("hello".to_string()),
FilterExpr::LiteralInt(i64::MAX),
FilterExpr::LiteralInt(i64::MIN),
FilterExpr::LiteralFloat(f64::MAX),
FilterExpr::LiteralFloat(f64::MIN_POSITIVE),
FilterExpr::LiteralBool(true),
FilterExpr::LiteralBool(false),
FilterExpr::LiteralArray(vec![FilterExpr::LiteralInt(1), FilterExpr::LiteralInt(2)]),
];
for expr in exprs {
let json = serde_json::to_string(&expr).unwrap();
let parsed: FilterExpr = serde_json::from_str(&json).unwrap();
assert_eq!(expr, parsed);
}
}
}