apif_source_row/
filter.rs1use crate::SourceRow;
2use serde::Deserialize;
3use std::collections::HashSet;
4
5#[derive(Debug, Clone, Deserialize)]
6pub struct FilterCondition {
7 pub field: String,
8 #[serde(default)]
9 pub equals: Option<String>,
10 #[serde(default)]
11 pub contains: Option<String>,
12 #[serde(default)]
13 pub gte: Option<String>,
14 #[serde(default)]
15 pub lt: Option<String>,
16 #[serde(default, rename = "in")]
17 pub in_values: Option<Vec<String>>,
18 #[serde(skip)]
19 in_set: Option<HashSet<String>>,
20}
21
22impl FilterCondition {
23 pub fn matches(&self, row: &SourceRow) -> bool {
24 let Some(actual) = row.get(&self.field) else {
25 return false;
26 };
27
28 if let Some(expected) = &self.equals
29 && actual != expected
30 {
31 return false;
32 }
33
34 if let Some(needle) = &self.contains
35 && !actual.contains(needle)
36 {
37 return false;
38 }
39
40 if let Some(min) = &self.gte
41 && actual < min.as_str()
42 {
43 return false;
44 }
45
46 if let Some(max) = &self.lt
47 && actual >= max.as_str()
48 {
49 return false;
50 }
51
52 if let Some(values) = &self.in_values {
53 let found = if let Some(set) = &self.in_set {
54 set.contains(actual)
55 } else {
56 values.iter().any(|v| v == actual)
57 };
58 if !found {
59 return false;
60 }
61 }
62
63 true
64 }
65
66 pub fn optimize(&mut self) {
67 if let Some(values) = &self.in_values {
68 self.in_set = Some(values.iter().cloned().collect());
69 }
70 }
71}
72
73pub fn matches_all(row: &SourceRow, conditions: &[FilterCondition]) -> bool {
74 conditions.iter().all(|c| c.matches(row))
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 fn row() -> SourceRow {
82 SourceRow::from_pairs(vec![
83 ("status".into(), "active".into()),
84 ("region_id".into(), "R01".into()),
85 ("created_at".into(), "2024-02-15".into()),
86 ("name".into(), "PVZ Alpha".into()),
87 ])
88 }
89
90 #[test]
91 fn equals_match() {
92 let cond = FilterCondition {
93 field: "status".into(),
94 equals: Some("active".into()),
95 contains: None,
96 gte: None,
97 lt: None,
98 in_values: None,
99 in_set: None,
100 };
101 assert!(cond.matches(&row()));
102 }
103
104 #[test]
105 fn in_match() {
106 let cond = FilterCondition {
107 field: "status".into(),
108 equals: None,
109 contains: None,
110 gte: None,
111 lt: None,
112 in_values: Some(vec!["inactive".into(), "active".into()]),
113 in_set: None,
114 };
115 assert!(cond.matches(&row()));
116 }
117
118 #[test]
119 fn contains_match() {
120 let cond = FilterCondition {
121 field: "name".into(),
122 equals: None,
123 contains: Some("Alpha".into()),
124 gte: None,
125 lt: None,
126 in_values: None,
127 in_set: None,
128 };
129 assert!(cond.matches(&row()));
130 }
131
132 #[test]
133 fn range_match() {
134 let cond = FilterCondition {
135 field: "created_at".into(),
136 equals: None,
137 contains: None,
138 gte: Some("2024-01-01".into()),
139 lt: Some("2025-01-01".into()),
140 in_values: None,
141 in_set: None,
142 };
143 assert!(cond.matches(&row()));
144 }
145
146 #[test]
147 fn matches_all_false_on_any_failure() {
148 let conds = vec![
149 FilterCondition {
150 field: "status".into(),
151 equals: Some("active".into()),
152 contains: None,
153 gte: None,
154 lt: None,
155 in_values: None,
156 in_set: None,
157 },
158 FilterCondition {
159 field: "region_id".into(),
160 equals: Some("R02".into()),
161 contains: None,
162 gte: None,
163 lt: None,
164 in_values: None,
165 in_set: None,
166 },
167 ];
168 assert!(!matches_all(&row(), &conds));
169 }
170}