Skip to main content

apif_source_row/
filter.rs

1use 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}