1use serde::{Deserialize, Deserializer, Serialize};
2use sonic_rs::Value;
3use sonic_rs::prelude::*;
4
5use super::ops::{CompareOp, LogicalOp};
6
7fn 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
26fn 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#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct FilterNode {
68 #[serde(skip_serializing_if = "Option::is_none")]
71 pub op: Option<String>,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub key: Option<String>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
80 pub cmp: Option<String>,
81
82 #[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 #[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 #[serde(skip_serializing_if = "Vec::is_empty", default)]
100 pub nodes: Vec<FilterNode>,
101
102 #[serde(skip)]
104 is_sorted: bool,
105}
106
107impl 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 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 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 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 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 #[inline]
174 pub fn is_sorted(&self) -> bool {
175 self.is_sorted
176 }
177
178 pub fn optimize(&mut self) {
182 if !self.vals.is_empty() && !self.is_sorted {
184 self.vals.sort();
185 self.vals.dedup();
186 self.is_sorted = true;
187 }
188
189 for node in &mut self.nodes {
191 node.optimize();
192 }
193 }
194
195 #[inline]
197 pub fn logical_op(&self) -> Option<LogicalOp> {
198 self.op.as_ref().and_then(|s| LogicalOp::parse(s))
199 }
200
201 #[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 #[inline]
212 pub fn key(&self) -> &str {
213 self.key.as_deref().unwrap_or("")
214 }
215
216 #[inline]
218 pub fn val(&self) -> &str {
219 self.val.as_deref().unwrap_or("")
220 }
221
222 #[inline]
224 pub fn vals(&self) -> &[String] {
225 &self.vals
226 }
227
228 #[inline]
230 pub fn nodes(&self) -> &[FilterNode] {
231 &self.nodes
232 }
233
234 pub fn validate(&self) -> Option<String> {
238 if let Some(ref op) = self.op {
239 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 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 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 }
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
296pub struct FilterNodeBuilder;
301
302impl FilterNodeBuilder {
303 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 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 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 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 pub fn exists(key: impl Into<String>) -> FilterNode {
333 FilterNode::new_existence(key.into(), CompareOp::Exists)
334 }
335
336 pub fn not_exists(key: impl Into<String>) -> FilterNode {
338 FilterNode::new_existence(key.into(), CompareOp::NotExists)
339 }
340
341 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 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 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 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 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 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 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 pub fn and(nodes: Vec<FilterNode>) -> FilterNode {
378 FilterNode::new_logical(LogicalOp::And, nodes)
379 }
380
381 pub fn or(nodes: Vec<FilterNode>) -> FilterNode {
383 FilterNode::new_logical(LogicalOp::Or, nodes)
384 }
385
386 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 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 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 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 assert_eq!(filter.op, Some("and".to_string()));
569 assert_eq!(filter.nodes.len(), 2);
570
571 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 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 assert_eq!(filter.validate(), None);
583 }
584
585 #[test]
586 fn test_deserialize_mixed_types() {
587 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}