Skip to main content

sockudo_filter/
node.rs

1use serde::{Deserialize, Deserializer, Serialize};
2use sonic_rs::Value;
3use sonic_rs::prelude::*;
4
5use super::ops::{CompareOp, LogicalOp};
6
7/// Custom deserializer that accepts both strings and numbers for the val field
8fn deserialize_string_or_number<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
9where
10    D: Deserializer<'de>,
11{
12    let value: Option<Value> = Option::deserialize(deserializer)?;
13    Ok(value.map(|v| {
14        if let Some(s) = v.as_str() {
15            s.to_string()
16        } else if let Some(n) = v.as_number() {
17            n.to_string()
18        } else if let Some(b) = v.as_bool() {
19            b.to_string()
20        } else {
21            v.to_string()
22        }
23    }))
24}
25
26/// Custom deserializer that accepts both strings and numbers for the vals array
27fn deserialize_string_or_number_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
28where
29    D: Deserializer<'de>,
30{
31    let values: Vec<Value> = Vec::deserialize(deserializer)?;
32    Ok(values
33        .into_iter()
34        .map(|v| {
35            if let Some(s) = v.as_str() {
36                s.to_string()
37            } else if let Some(n) = v.as_number() {
38                n.to_string()
39            } else if let Some(b) = v.as_bool() {
40                b.to_string()
41            } else {
42                v.to_string()
43            }
44        })
45        .collect())
46}
47
48fn default_empty_vec() -> Vec<String> {
49    Vec::new()
50}
51
52/// A filter node that can be a leaf (comparison) or a branch (logical operation).
53///
54/// This structure is designed to be serialized/deserialized from JSON and can be
55/// constructed programmatically using the builder pattern.
56///
57/// Design goals:
58/// - Zero allocations during evaluation
59/// - Easy serialization to/from JSON
60/// - Programmatic construction via builder
61/// - Tree structure for complex filters
62///
63/// Performance optimizations:
64/// - HashSet cache for IN/NIN operators with many values (O(1) lookup)
65/// - Arc-wrapped HashSet shared across clones (zero-copy for socket subscriptions)
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct FilterNode {
68    /// Logical operator for branch nodes: "and", "or", "not"
69    /// If None or empty, this is a leaf node (comparison)
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub op: Option<String>,
72
73    /// Key for comparison (leaf nodes only)
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub key: Option<String>,
76
77    /// Comparison operator for leaf nodes
78    /// "eq", "neq", "in", "nin", "ex", "nex", "sw", "ew", "ct", "gt", "gte", "lt", "lte"
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub cmp: Option<String>,
81
82    /// Single value for most comparisons
83    #[serde(
84        skip_serializing_if = "Option::is_none",
85        default,
86        deserialize_with = "deserialize_string_or_number"
87    )]
88    pub val: Option<String>,
89
90    /// Multiple values for set operations (in, nin)
91    #[serde(
92        skip_serializing_if = "Vec::is_empty",
93        default = "default_empty_vec",
94        deserialize_with = "deserialize_string_or_number_vec"
95    )]
96    pub vals: Vec<String>,
97
98    /// Child nodes for logical operations
99    #[serde(skip_serializing_if = "Vec::is_empty", default)]
100    pub nodes: Vec<FilterNode>,
101
102    /// Flag indicating if the vals vector has been sorted for fast binary search
103    #[serde(skip)]
104    is_sorted: bool,
105}
106
107// Manual PartialEq to ignore is_sorted cache field
108impl PartialEq for FilterNode {
109    fn eq(&self, other: &Self) -> bool {
110        self.op == other.op
111            && self.key == other.key
112            && self.cmp == other.cmp
113            && self.val == other.val
114            && self.vals == other.vals
115            && self.nodes == other.nodes
116    }
117}
118
119impl FilterNode {
120    /// Creates a new leaf node with a comparison operation.
121    pub fn new_comparison(key: String, cmp: CompareOp, val: String) -> Self {
122        Self {
123            op: None,
124            key: Some(key),
125            cmp: Some(cmp.to_string()),
126            val: Some(val),
127            vals: Vec::new(),
128            nodes: Vec::new(),
129            is_sorted: false,
130        }
131    }
132
133    /// Creates a new leaf node with a set comparison operation (in/nin).
134    pub fn new_set_comparison(key: String, cmp: CompareOp, vals: Vec<String>) -> Self {
135        Self {
136            op: None,
137            key: Some(key),
138            cmp: Some(cmp.to_string()),
139            val: None,
140            vals,
141            nodes: Vec::new(),
142            is_sorted: false,
143        }
144    }
145
146    /// Creates a new leaf node with an existence check.
147    pub fn new_existence(key: String, cmp: CompareOp) -> Self {
148        Self {
149            op: None,
150            key: Some(key),
151            cmp: Some(cmp.to_string()),
152            val: None,
153            vals: Vec::new(),
154            nodes: Vec::new(),
155            is_sorted: false,
156        }
157    }
158
159    /// Creates a new branch node with a logical operation.
160    pub fn new_logical(op: LogicalOp, nodes: Vec<FilterNode>) -> Self {
161        Self {
162            op: Some(op.to_string()),
163            key: None,
164            cmp: None,
165            val: None,
166            vals: Vec::new(),
167            nodes,
168            is_sorted: false,
169        }
170    }
171
172    /// Returns true if the vals vector is sorted and ready for binary search.
173    #[inline]
174    pub fn is_sorted(&self) -> bool {
175        self.is_sorted
176    }
177
178    /// Optimizes the filter node by sorting vectors for binary search.
179    /// This avoids the overhead of building HashSets (allocations) while still
180    /// providing O(log n) lookup performance.
181    pub fn optimize(&mut self) {
182        // Sort and deduplicate values for binary search
183        if !self.vals.is_empty() && !self.is_sorted {
184            self.vals.sort();
185            self.vals.dedup();
186            self.is_sorted = true;
187        }
188
189        // Recursively optimize child nodes
190        for node in &mut self.nodes {
191            node.optimize();
192        }
193    }
194
195    /// Returns the logical operator if this is a branch node.
196    #[inline]
197    pub fn logical_op(&self) -> Option<LogicalOp> {
198        self.op.as_ref().and_then(|s| LogicalOp::parse(s))
199    }
200
201    /// Returns the comparison operator if this is a leaf node.
202    #[inline]
203    pub fn compare_op(&self) -> CompareOp {
204        self.cmp
205            .as_ref()
206            .and_then(|s| CompareOp::parse(s))
207            .unwrap_or(CompareOp::Equal)
208    }
209
210    /// Returns the key for leaf node comparisons.
211    #[inline]
212    pub fn key(&self) -> &str {
213        self.key.as_deref().unwrap_or("")
214    }
215
216    /// Returns the single value for leaf node comparisons.
217    #[inline]
218    pub fn val(&self) -> &str {
219        self.val.as_deref().unwrap_or("")
220    }
221
222    /// Returns the multiple values for set comparisons.
223    #[inline]
224    pub fn vals(&self) -> &[String] {
225        &self.vals
226    }
227
228    /// Returns the child nodes for logical operations.
229    #[inline]
230    pub fn nodes(&self) -> &[FilterNode] {
231        &self.nodes
232    }
233
234    /// Validates the filter node structure.
235    ///
236    /// Returns an error message if the node is invalid, None otherwise.
237    pub fn validate(&self) -> Option<String> {
238        if let Some(ref op) = self.op {
239            // This is a logical node
240            let logical_op = LogicalOp::parse(op)?;
241
242            match logical_op {
243                LogicalOp::And | LogicalOp::Or => {
244                    if self.nodes.is_empty() {
245                        return Some(format!("{op} operation requires at least one child node"));
246                    }
247                }
248                LogicalOp::Not => {
249                    if self.nodes.len() != 1 {
250                        return Some(format!(
251                            "not operation requires exactly one child node, got {}",
252                            self.nodes.len()
253                        ));
254                    }
255                }
256            }
257
258            // Validate all children recursively
259            for (i, child) in self.nodes.iter().enumerate() {
260                if let Some(err) = child.validate() {
261                    return Some(format!("Child node {i}: {err}"));
262                }
263            }
264        } else {
265            // This is a leaf node
266            if self.key.is_none() || self.key.as_ref().is_none_or(|k| k.is_empty()) {
267                return Some("Leaf node requires a non-empty key".to_string());
268            }
269
270            let cmp = self.cmp.as_ref()?;
271            let compare_op = CompareOp::parse(cmp)?;
272
273            match compare_op {
274                CompareOp::In | CompareOp::NotIn => {
275                    if self.vals.is_empty() {
276                        return Some(format!(
277                            "{cmp} operation requires at least one value in vals"
278                        ));
279                    }
280                }
281                CompareOp::Exists | CompareOp::NotExists => {
282                    // No value needed
283                }
284                _ => {
285                    if self.val.is_none() || self.val.as_ref().is_none_or(|v| v.is_empty()) {
286                        return Some(format!("{cmp} operation requires a non-empty val"));
287                    }
288                }
289            }
290        }
291
292        None
293    }
294}
295
296/// Builder for constructing filter nodes programmatically.
297///
298/// This provides a clean API for creating filters without dealing with the
299/// internal structure directly.
300pub struct FilterNodeBuilder;
301
302impl FilterNodeBuilder {
303    /// Creates an equality comparison: key == val
304    pub fn eq(key: impl Into<String>, val: impl Into<String>) -> FilterNode {
305        FilterNode::new_comparison(key.into(), CompareOp::Equal, val.into())
306    }
307
308    /// Creates an inequality comparison: key != val
309    pub fn neq(key: impl Into<String>, val: impl Into<String>) -> FilterNode {
310        FilterNode::new_comparison(key.into(), CompareOp::NotEqual, val.into())
311    }
312
313    /// Creates a set membership comparison: key in [vals...]
314    pub fn in_set(key: impl Into<String>, vals: &[impl ToString]) -> FilterNode {
315        FilterNode::new_set_comparison(
316            key.into(),
317            CompareOp::In,
318            vals.iter().map(|v| v.to_string()).collect(),
319        )
320    }
321
322    /// Creates a set non-membership comparison: key not in [vals...]
323    pub fn nin(key: impl Into<String>, vals: &[impl ToString]) -> FilterNode {
324        FilterNode::new_set_comparison(
325            key.into(),
326            CompareOp::NotIn,
327            vals.iter().map(|v| v.to_string()).collect(),
328        )
329    }
330
331    /// Creates an existence check: key exists
332    pub fn exists(key: impl Into<String>) -> FilterNode {
333        FilterNode::new_existence(key.into(), CompareOp::Exists)
334    }
335
336    /// Creates a non-existence check: key does not exist
337    pub fn not_exists(key: impl Into<String>) -> FilterNode {
338        FilterNode::new_existence(key.into(), CompareOp::NotExists)
339    }
340
341    /// Creates a starts-with comparison: key starts with val
342    pub fn starts_with(key: impl Into<String>, val: impl Into<String>) -> FilterNode {
343        FilterNode::new_comparison(key.into(), CompareOp::StartsWith, val.into())
344    }
345
346    /// Creates an ends-with comparison: key ends with val
347    pub fn ends_with(key: impl Into<String>, val: impl Into<String>) -> FilterNode {
348        FilterNode::new_comparison(key.into(), CompareOp::EndsWith, val.into())
349    }
350
351    /// Creates a contains comparison: key contains val
352    pub fn contains(key: impl Into<String>, val: impl Into<String>) -> FilterNode {
353        FilterNode::new_comparison(key.into(), CompareOp::Contains, val.into())
354    }
355
356    /// Creates a greater-than comparison: key > val (numeric)
357    pub fn gt(key: impl Into<String>, val: impl Into<String>) -> FilterNode {
358        FilterNode::new_comparison(key.into(), CompareOp::GreaterThan, val.into())
359    }
360
361    /// Creates a greater-than-or-equal comparison: key >= val (numeric)
362    pub fn gte(key: impl Into<String>, val: impl Into<String>) -> FilterNode {
363        FilterNode::new_comparison(key.into(), CompareOp::GreaterThanOrEqual, val.into())
364    }
365
366    /// Creates a less-than comparison: key < val (numeric)
367    pub fn lt(key: impl Into<String>, val: impl Into<String>) -> FilterNode {
368        FilterNode::new_comparison(key.into(), CompareOp::LessThan, val.into())
369    }
370
371    /// Creates a less-than-or-equal comparison: key <= val (numeric)
372    pub fn lte(key: impl Into<String>, val: impl Into<String>) -> FilterNode {
373        FilterNode::new_comparison(key.into(), CompareOp::LessThanOrEqual, val.into())
374    }
375
376    /// Creates an AND logical operation: all children must match
377    pub fn and(nodes: Vec<FilterNode>) -> FilterNode {
378        FilterNode::new_logical(LogicalOp::And, nodes)
379    }
380
381    /// Creates an OR logical operation: at least one child must match
382    pub fn or(nodes: Vec<FilterNode>) -> FilterNode {
383        FilterNode::new_logical(LogicalOp::Or, nodes)
384    }
385
386    /// Creates a NOT logical operation: negates the child
387    pub fn not(node: FilterNode) -> FilterNode {
388        FilterNode::new_logical(LogicalOp::Not, vec![node])
389    }
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395
396    #[test]
397    fn test_serialize_simple_filter() {
398        let filter = FilterNodeBuilder::eq("event_type", "goal");
399        let json = sonic_rs::to_string(&filter).unwrap();
400        let parsed: FilterNode = sonic_rs::from_str(&json).unwrap();
401        assert_eq!(filter, parsed);
402    }
403
404    #[test]
405    fn test_serialize_complex_filter() {
406        let filter = FilterNodeBuilder::or(vec![
407            FilterNodeBuilder::eq("event_type", "goal"),
408            FilterNodeBuilder::and(vec![
409                FilterNodeBuilder::eq("event_type", "shot"),
410                FilterNodeBuilder::gte("xG", "0.8"),
411            ]),
412        ]);
413
414        let json = sonic_rs::to_string(&filter).unwrap();
415        let parsed: FilterNode = sonic_rs::from_str(&json).unwrap();
416        assert_eq!(filter, parsed);
417    }
418
419    #[test]
420    fn test_validate_valid_leaf() {
421        let filter = FilterNodeBuilder::eq("key", "value");
422        assert_eq!(filter.validate(), None);
423    }
424
425    #[test]
426    fn test_validate_invalid_leaf_missing_key() {
427        let filter = FilterNode {
428            op: None,
429            key: None,
430            cmp: Some("eq".to_string()),
431            val: Some("value".to_string()),
432            vals: Vec::new(),
433            nodes: Vec::new(),
434            is_sorted: false,
435        };
436        assert!(filter.validate().is_some());
437    }
438
439    #[test]
440    fn test_validate_invalid_leaf_missing_value() {
441        let filter = FilterNode {
442            op: None,
443            key: Some("key".to_string()),
444            cmp: Some("eq".to_string()),
445            val: None,
446            vals: Vec::new(),
447            nodes: Vec::new(),
448            is_sorted: false,
449        };
450        assert!(filter.validate().is_some());
451    }
452
453    #[test]
454    fn test_validate_valid_set_operation() {
455        let filter = FilterNodeBuilder::in_set("key", &["a", "b", "c"]);
456        assert_eq!(filter.validate(), None);
457    }
458
459    #[test]
460    fn test_validate_invalid_set_operation_empty_vals() {
461        let filter = FilterNode {
462            op: None,
463            key: Some("key".to_string()),
464            cmp: Some("in".to_string()),
465            val: None,
466            vals: Vec::new(),
467            nodes: Vec::new(),
468            is_sorted: false,
469        };
470        assert!(filter.validate().is_some());
471    }
472
473    #[test]
474    fn test_validate_valid_and() {
475        let filter = FilterNodeBuilder::and(vec![
476            FilterNodeBuilder::eq("a", "1"),
477            FilterNodeBuilder::eq("b", "2"),
478        ]);
479        assert_eq!(filter.validate(), None);
480    }
481
482    #[test]
483    fn test_validate_invalid_and_no_children() {
484        let filter = FilterNode {
485            op: Some("and".to_string()),
486            key: None,
487            cmp: None,
488            val: None,
489            vals: Vec::new(),
490            nodes: Vec::new(),
491            is_sorted: false,
492        };
493        assert!(filter.validate().is_some());
494    }
495
496    #[test]
497    fn test_validate_valid_not() {
498        let filter = FilterNodeBuilder::not(FilterNodeBuilder::eq("key", "value"));
499        assert_eq!(filter.validate(), None);
500    }
501
502    #[test]
503    fn test_validate_invalid_not_multiple_children() {
504        let filter = FilterNode {
505            op: Some("not".to_string()),
506            key: None,
507            cmp: None,
508            val: None,
509            vals: Vec::new(),
510            nodes: vec![
511                FilterNodeBuilder::eq("a", "1"),
512                FilterNodeBuilder::eq("b", "2"),
513            ],
514            is_sorted: false,
515        };
516        assert!(filter.validate().is_some());
517    }
518
519    #[test]
520    fn test_validate_existence_checks() {
521        let exists = FilterNodeBuilder::exists("key");
522        let not_exists = FilterNodeBuilder::not_exists("key");
523        assert_eq!(exists.validate(), None);
524        assert_eq!(not_exists.validate(), None);
525    }
526
527    #[test]
528    fn test_deserialize_numeric_val() {
529        // Test that numeric values in JSON are properly converted to strings
530        let json = r#"{"key":"category_id","cmp":"eq","val":501}"#;
531        let filter: FilterNode = sonic_rs::from_str(json).unwrap();
532        assert_eq!(filter.key, Some("category_id".to_string()));
533        assert_eq!(filter.cmp, Some("eq".to_string()));
534        assert_eq!(filter.val, Some("501".to_string()));
535    }
536
537    #[test]
538    fn test_deserialize_numeric_vals() {
539        // Test that numeric values in vals array are properly converted to strings
540        let json = r#"{"key":"category_id","cmp":"in","vals":[501,1,56]}"#;
541        let filter: FilterNode = sonic_rs::from_str(json).unwrap();
542        assert_eq!(filter.key, Some("category_id".to_string()));
543        assert_eq!(filter.cmp, Some("in".to_string()));
544        assert_eq!(filter.vals, vec!["501", "1", "56"]);
545    }
546
547    #[test]
548    fn test_deserialize_and_with_numeric_values() {
549        // Test the exact user scenario: AND filter with numeric values
550        let json = r#"{
551            "op": "and",
552            "nodes": [
553                {
554                    "key": "category_id",
555                    "cmp": "eq",
556                    "val": 501
557                },
558                {
559                    "key": "item_id",
560                    "cmp": "eq",
561                    "val": "item-abc-001"
562                }
563            ]
564        }"#;
565        let filter: FilterNode = sonic_rs::from_str(json).unwrap();
566
567        // Validate structure
568        assert_eq!(filter.op, Some("and".to_string()));
569        assert_eq!(filter.nodes.len(), 2);
570
571        // Validate first node (numeric value)
572        assert_eq!(filter.nodes[0].key, Some("category_id".to_string()));
573        assert_eq!(filter.nodes[0].cmp, Some("eq".to_string()));
574        assert_eq!(filter.nodes[0].val, Some("501".to_string()));
575
576        // Validate second node (string value)
577        assert_eq!(filter.nodes[1].key, Some("item_id".to_string()));
578        assert_eq!(filter.nodes[1].cmp, Some("eq".to_string()));
579        assert_eq!(filter.nodes[1].val, Some("item-abc-001".to_string()));
580
581        // Validate the filter
582        assert_eq!(filter.validate(), None);
583    }
584
585    #[test]
586    fn test_deserialize_mixed_types() {
587        // Test that booleans and other types are also converted
588        let json = r#"{"key":"active","cmp":"eq","val":true}"#;
589        let filter: FilterNode = sonic_rs::from_str(json).unwrap();
590        assert_eq!(filter.val, Some("true".to_string()));
591    }
592}