use serde::{Deserialize, Deserializer, Serialize};
use sonic_rs::Value;
use sonic_rs::prelude::*;
use super::ops::{CompareOp, LogicalOp};
fn deserialize_string_or_number<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
let value: Option<Value> = Option::deserialize(deserializer)?;
Ok(value.map(|v| {
if let Some(s) = v.as_str() {
s.to_string()
} else if let Some(n) = v.as_number() {
n.to_string()
} else if let Some(b) = v.as_bool() {
b.to_string()
} else {
v.to_string()
}
}))
}
fn deserialize_string_or_number_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
let values: Vec<Value> = Vec::deserialize(deserializer)?;
Ok(values
.into_iter()
.map(|v| {
if let Some(s) = v.as_str() {
s.to_string()
} else if let Some(n) = v.as_number() {
n.to_string()
} else if let Some(b) = v.as_bool() {
b.to_string()
} else {
v.to_string()
}
})
.collect())
}
fn default_empty_vec() -> Vec<String> {
Vec::new()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FilterNode {
#[serde(skip_serializing_if = "Option::is_none")]
pub op: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cmp: Option<String>,
#[serde(
skip_serializing_if = "Option::is_none",
default,
deserialize_with = "deserialize_string_or_number"
)]
pub val: Option<String>,
#[serde(
skip_serializing_if = "Vec::is_empty",
default = "default_empty_vec",
deserialize_with = "deserialize_string_or_number_vec"
)]
pub vals: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub nodes: Vec<FilterNode>,
#[serde(skip)]
is_sorted: bool,
}
impl PartialEq for FilterNode {
fn eq(&self, other: &Self) -> bool {
self.op == other.op
&& self.key == other.key
&& self.cmp == other.cmp
&& self.val == other.val
&& self.vals == other.vals
&& self.nodes == other.nodes
}
}
impl FilterNode {
pub fn new_comparison(key: String, cmp: CompareOp, val: String) -> Self {
Self {
op: None,
key: Some(key),
cmp: Some(cmp.to_string()),
val: Some(val),
vals: Vec::new(),
nodes: Vec::new(),
is_sorted: false,
}
}
pub fn new_set_comparison(key: String, cmp: CompareOp, vals: Vec<String>) -> Self {
Self {
op: None,
key: Some(key),
cmp: Some(cmp.to_string()),
val: None,
vals,
nodes: Vec::new(),
is_sorted: false,
}
}
pub fn new_existence(key: String, cmp: CompareOp) -> Self {
Self {
op: None,
key: Some(key),
cmp: Some(cmp.to_string()),
val: None,
vals: Vec::new(),
nodes: Vec::new(),
is_sorted: false,
}
}
pub fn new_logical(op: LogicalOp, nodes: Vec<FilterNode>) -> Self {
Self {
op: Some(op.to_string()),
key: None,
cmp: None,
val: None,
vals: Vec::new(),
nodes,
is_sorted: false,
}
}
#[inline]
pub fn is_sorted(&self) -> bool {
self.is_sorted
}
pub fn optimize(&mut self) {
if !self.vals.is_empty() && !self.is_sorted {
self.vals.sort();
self.vals.dedup();
self.is_sorted = true;
}
for node in &mut self.nodes {
node.optimize();
}
}
#[inline]
pub fn logical_op(&self) -> Option<LogicalOp> {
self.op.as_ref().and_then(|s| LogicalOp::parse(s))
}
#[inline]
pub fn compare_op(&self) -> CompareOp {
self.cmp
.as_ref()
.and_then(|s| CompareOp::parse(s))
.unwrap_or(CompareOp::Equal)
}
#[inline]
pub fn key(&self) -> &str {
self.key.as_deref().unwrap_or("")
}
#[inline]
pub fn val(&self) -> &str {
self.val.as_deref().unwrap_or("")
}
#[inline]
pub fn vals(&self) -> &[String] {
&self.vals
}
#[inline]
pub fn nodes(&self) -> &[FilterNode] {
&self.nodes
}
pub fn validate(&self) -> Option<String> {
if let Some(ref op) = self.op {
let logical_op = LogicalOp::parse(op)?;
match logical_op {
LogicalOp::And | LogicalOp::Or => {
if self.nodes.is_empty() {
return Some(format!("{op} operation requires at least one child node"));
}
}
LogicalOp::Not => {
if self.nodes.len() != 1 {
return Some(format!(
"not operation requires exactly one child node, got {}",
self.nodes.len()
));
}
}
}
for (i, child) in self.nodes.iter().enumerate() {
if let Some(err) = child.validate() {
return Some(format!("Child node {i}: {err}"));
}
}
} else {
if self.key.is_none() || self.key.as_ref().is_none_or(|k| k.is_empty()) {
return Some("Leaf node requires a non-empty key".to_string());
}
let cmp = self.cmp.as_ref()?;
let compare_op = CompareOp::parse(cmp)?;
match compare_op {
CompareOp::In | CompareOp::NotIn => {
if self.vals.is_empty() {
return Some(format!(
"{cmp} operation requires at least one value in vals"
));
}
}
CompareOp::Exists | CompareOp::NotExists => {
}
_ => {
if self.val.is_none() || self.val.as_ref().is_none_or(|v| v.is_empty()) {
return Some(format!("{cmp} operation requires a non-empty val"));
}
}
}
}
None
}
}
pub struct FilterNodeBuilder;
impl FilterNodeBuilder {
pub fn eq(key: impl Into<String>, val: impl Into<String>) -> FilterNode {
FilterNode::new_comparison(key.into(), CompareOp::Equal, val.into())
}
pub fn neq(key: impl Into<String>, val: impl Into<String>) -> FilterNode {
FilterNode::new_comparison(key.into(), CompareOp::NotEqual, val.into())
}
pub fn in_set(key: impl Into<String>, vals: &[impl ToString]) -> FilterNode {
FilterNode::new_set_comparison(
key.into(),
CompareOp::In,
vals.iter().map(|v| v.to_string()).collect(),
)
}
pub fn nin(key: impl Into<String>, vals: &[impl ToString]) -> FilterNode {
FilterNode::new_set_comparison(
key.into(),
CompareOp::NotIn,
vals.iter().map(|v| v.to_string()).collect(),
)
}
pub fn exists(key: impl Into<String>) -> FilterNode {
FilterNode::new_existence(key.into(), CompareOp::Exists)
}
pub fn not_exists(key: impl Into<String>) -> FilterNode {
FilterNode::new_existence(key.into(), CompareOp::NotExists)
}
pub fn starts_with(key: impl Into<String>, val: impl Into<String>) -> FilterNode {
FilterNode::new_comparison(key.into(), CompareOp::StartsWith, val.into())
}
pub fn ends_with(key: impl Into<String>, val: impl Into<String>) -> FilterNode {
FilterNode::new_comparison(key.into(), CompareOp::EndsWith, val.into())
}
pub fn contains(key: impl Into<String>, val: impl Into<String>) -> FilterNode {
FilterNode::new_comparison(key.into(), CompareOp::Contains, val.into())
}
pub fn gt(key: impl Into<String>, val: impl Into<String>) -> FilterNode {
FilterNode::new_comparison(key.into(), CompareOp::GreaterThan, val.into())
}
pub fn gte(key: impl Into<String>, val: impl Into<String>) -> FilterNode {
FilterNode::new_comparison(key.into(), CompareOp::GreaterThanOrEqual, val.into())
}
pub fn lt(key: impl Into<String>, val: impl Into<String>) -> FilterNode {
FilterNode::new_comparison(key.into(), CompareOp::LessThan, val.into())
}
pub fn lte(key: impl Into<String>, val: impl Into<String>) -> FilterNode {
FilterNode::new_comparison(key.into(), CompareOp::LessThanOrEqual, val.into())
}
pub fn and(nodes: Vec<FilterNode>) -> FilterNode {
FilterNode::new_logical(LogicalOp::And, nodes)
}
pub fn or(nodes: Vec<FilterNode>) -> FilterNode {
FilterNode::new_logical(LogicalOp::Or, nodes)
}
pub fn not(node: FilterNode) -> FilterNode {
FilterNode::new_logical(LogicalOp::Not, vec![node])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialize_simple_filter() {
let filter = FilterNodeBuilder::eq("event_type", "goal");
let json = sonic_rs::to_string(&filter).unwrap();
let parsed: FilterNode = sonic_rs::from_str(&json).unwrap();
assert_eq!(filter, parsed);
}
#[test]
fn test_serialize_complex_filter() {
let filter = FilterNodeBuilder::or(vec![
FilterNodeBuilder::eq("event_type", "goal"),
FilterNodeBuilder::and(vec![
FilterNodeBuilder::eq("event_type", "shot"),
FilterNodeBuilder::gte("xG", "0.8"),
]),
]);
let json = sonic_rs::to_string(&filter).unwrap();
let parsed: FilterNode = sonic_rs::from_str(&json).unwrap();
assert_eq!(filter, parsed);
}
#[test]
fn test_validate_valid_leaf() {
let filter = FilterNodeBuilder::eq("key", "value");
assert_eq!(filter.validate(), None);
}
#[test]
fn test_validate_invalid_leaf_missing_key() {
let filter = FilterNode {
op: None,
key: None,
cmp: Some("eq".to_string()),
val: Some("value".to_string()),
vals: Vec::new(),
nodes: Vec::new(),
is_sorted: false,
};
assert!(filter.validate().is_some());
}
#[test]
fn test_validate_invalid_leaf_missing_value() {
let filter = FilterNode {
op: None,
key: Some("key".to_string()),
cmp: Some("eq".to_string()),
val: None,
vals: Vec::new(),
nodes: Vec::new(),
is_sorted: false,
};
assert!(filter.validate().is_some());
}
#[test]
fn test_validate_valid_set_operation() {
let filter = FilterNodeBuilder::in_set("key", &["a", "b", "c"]);
assert_eq!(filter.validate(), None);
}
#[test]
fn test_validate_invalid_set_operation_empty_vals() {
let filter = FilterNode {
op: None,
key: Some("key".to_string()),
cmp: Some("in".to_string()),
val: None,
vals: Vec::new(),
nodes: Vec::new(),
is_sorted: false,
};
assert!(filter.validate().is_some());
}
#[test]
fn test_validate_valid_and() {
let filter = FilterNodeBuilder::and(vec![
FilterNodeBuilder::eq("a", "1"),
FilterNodeBuilder::eq("b", "2"),
]);
assert_eq!(filter.validate(), None);
}
#[test]
fn test_validate_invalid_and_no_children() {
let filter = FilterNode {
op: Some("and".to_string()),
key: None,
cmp: None,
val: None,
vals: Vec::new(),
nodes: Vec::new(),
is_sorted: false,
};
assert!(filter.validate().is_some());
}
#[test]
fn test_validate_valid_not() {
let filter = FilterNodeBuilder::not(FilterNodeBuilder::eq("key", "value"));
assert_eq!(filter.validate(), None);
}
#[test]
fn test_validate_invalid_not_multiple_children() {
let filter = FilterNode {
op: Some("not".to_string()),
key: None,
cmp: None,
val: None,
vals: Vec::new(),
nodes: vec![
FilterNodeBuilder::eq("a", "1"),
FilterNodeBuilder::eq("b", "2"),
],
is_sorted: false,
};
assert!(filter.validate().is_some());
}
#[test]
fn test_validate_existence_checks() {
let exists = FilterNodeBuilder::exists("key");
let not_exists = FilterNodeBuilder::not_exists("key");
assert_eq!(exists.validate(), None);
assert_eq!(not_exists.validate(), None);
}
#[test]
fn test_deserialize_numeric_val() {
let json = r#"{"key":"category_id","cmp":"eq","val":501}"#;
let filter: FilterNode = sonic_rs::from_str(json).unwrap();
assert_eq!(filter.key, Some("category_id".to_string()));
assert_eq!(filter.cmp, Some("eq".to_string()));
assert_eq!(filter.val, Some("501".to_string()));
}
#[test]
fn test_deserialize_numeric_vals() {
let json = r#"{"key":"category_id","cmp":"in","vals":[501,1,56]}"#;
let filter: FilterNode = sonic_rs::from_str(json).unwrap();
assert_eq!(filter.key, Some("category_id".to_string()));
assert_eq!(filter.cmp, Some("in".to_string()));
assert_eq!(filter.vals, vec!["501", "1", "56"]);
}
#[test]
fn test_deserialize_and_with_numeric_values() {
let json = r#"{
"op": "and",
"nodes": [
{
"key": "category_id",
"cmp": "eq",
"val": 501
},
{
"key": "item_id",
"cmp": "eq",
"val": "item-abc-001"
}
]
}"#;
let filter: FilterNode = sonic_rs::from_str(json).unwrap();
assert_eq!(filter.op, Some("and".to_string()));
assert_eq!(filter.nodes.len(), 2);
assert_eq!(filter.nodes[0].key, Some("category_id".to_string()));
assert_eq!(filter.nodes[0].cmp, Some("eq".to_string()));
assert_eq!(filter.nodes[0].val, Some("501".to_string()));
assert_eq!(filter.nodes[1].key, Some("item_id".to_string()));
assert_eq!(filter.nodes[1].cmp, Some("eq".to_string()));
assert_eq!(filter.nodes[1].val, Some("item-abc-001".to_string()));
assert_eq!(filter.validate(), None);
}
#[test]
fn test_deserialize_mixed_types() {
let json = r#"{"key":"active","cmp":"eq","val":true}"#;
let filter: FilterNode = sonic_rs::from_str(json).unwrap();
assert_eq!(filter.val, Some("true".to_string()));
}
}