1use serde_json::{Map, Value};
2
3use crate::error::{Error, Result};
4
5#[derive(Clone, Debug, PartialEq)]
10pub enum Filter {
11 Eq {
12 field: String,
13 value: Value,
14 },
15 In {
16 field: String,
17 values: Vec<Value>,
18 },
19 Range {
20 field: String,
21 gt: Option<f64>,
22 gte: Option<f64>,
23 lt: Option<f64>,
24 lte: Option<f64>,
25 },
26 And(Vec<Filter>),
27 Or(Vec<Filter>),
28 Not(Box<Filter>),
29}
30
31impl Filter {
32 pub fn eq(field: impl Into<String>, value: impl Into<Value>) -> Self {
33 Filter::Eq {
34 field: field.into(),
35 value: value.into(),
36 }
37 }
38
39 pub fn is_in(field: impl Into<String>, values: impl IntoIterator<Item = Value>) -> Self {
40 Filter::In {
41 field: field.into(),
42 values: values.into_iter().collect(),
43 }
44 }
45
46 pub fn between(field: impl Into<String>, gte: Option<f64>, lte: Option<f64>) -> Self {
48 Filter::Range {
49 field: field.into(),
50 gt: None,
51 gte,
52 lt: None,
53 lte,
54 }
55 }
56
57 pub fn matches(&self, metadata: Option<&Value>) -> bool {
58 match self {
59 Filter::Or(filters) => filters.iter().any(|f| f.matches(metadata)),
60 Filter::Not(filter) => !filter.matches(metadata),
61 Filter::And(filters) => filters.iter().all(|f| f.matches(metadata)),
62 Filter::Eq { field, value } => lookup(metadata, field).is_some_and(|v| same(v, value)),
63 Filter::In { field, values } => {
64 lookup(metadata, field).is_some_and(|v| values.iter().any(|x| same(v, x)))
65 }
66 Filter::Range {
67 field,
68 gt,
69 gte,
70 lt,
71 lte,
72 } => match lookup(metadata, field).and_then(Value::as_f64) {
73 Some(x) => {
74 gt.is_none_or(|b| x > b)
75 && gte.is_none_or(|b| x >= b)
76 && lt.is_none_or(|b| x < b)
77 && lte.is_none_or(|b| x <= b)
78 }
79 None => false,
80 },
81 }
82 }
83
84 pub fn from_json(value: &Value) -> Result<Self> {
93 Self::parse(value, 0)
94 }
95
96 fn parse(value: &Value, depth: usize) -> Result<Self> {
97 if depth > 64 {
98 return Err(Error::InvalidArgument("filter nesting exceeds 64".into()));
99 }
100 let object = value
101 .as_object()
102 .ok_or_else(|| Error::InvalidArgument("filter must be a JSON object".into()))?;
103 let mut parts = Vec::with_capacity(object.len());
104 for (field, condition) in object {
105 if field.starts_with('$') {
106 parts.push(match field.as_str() {
107 "$and" | "$or" => {
108 let items =
109 condition
110 .as_array()
111 .filter(|a| !a.is_empty())
112 .ok_or_else(|| {
113 Error::InvalidArgument(format!(
114 "{field} expects a nonempty array"
115 ))
116 })?;
117 let filters = items
118 .iter()
119 .map(|v| Self::parse(v, depth + 1))
120 .collect::<Result<Vec<_>>>()?;
121 if field == "$and" {
122 Self::And(filters)
123 } else {
124 Self::Or(filters)
125 }
126 }
127 "$not" => Self::Not(Box::new(Self::parse(condition, depth + 1)?)),
128 _ => {
129 return Err(Error::InvalidArgument(format!(
130 "unsupported filter operator {field}"
131 )));
132 }
133 });
134 continue;
135 }
136 match condition {
137 Value::Object(ops) if !ops.is_empty() && ops.keys().any(|k| k.starts_with('$')) => {
138 parts.push(parse_operators(field, ops)?);
139 }
140 other => parts.push(Filter::eq(field.clone(), other.clone())),
141 }
142 }
143 Ok(combine(parts))
144 }
145}
146
147fn parse_operators(field: &str, ops: &Map<String, Value>) -> Result<Filter> {
148 let mut parts = Vec::new();
149 let (mut gt, mut gte, mut lt, mut lte) = (None, None, None, None);
150 for (op, arg) in ops {
151 match op.as_str() {
152 "$eq" => parts.push(Filter::eq(field, arg.clone())),
153 "$in" => {
154 let values = arg.as_array().ok_or_else(|| {
155 Error::InvalidArgument(format!("$in on '{field}' expects an array"))
156 })?;
157 parts.push(Filter::is_in(field, values.iter().cloned()));
158 }
159 "$gt" | "$gte" | "$lt" | "$lte" => {
160 let bound = arg.as_f64().ok_or_else(|| {
161 Error::InvalidArgument(format!("{op} on '{field}' expects a number"))
162 })?;
163 match op.as_str() {
164 "$gt" => gt = Some(bound),
165 "$gte" => gte = Some(bound),
166 "$lt" => lt = Some(bound),
167 _ => lte = Some(bound),
168 }
169 }
170 other => {
171 return Err(Error::InvalidArgument(format!(
172 "unsupported filter operator {other}"
173 )));
174 }
175 }
176 }
177 if gt.is_some() || gte.is_some() || lt.is_some() || lte.is_some() {
178 parts.push(Filter::Range {
179 field: field.to_owned(),
180 gt,
181 gte,
182 lt,
183 lte,
184 });
185 }
186 Ok(combine(parts))
187}
188
189fn combine(mut parts: Vec<Filter>) -> Filter {
190 if parts.len() == 1 {
191 parts.pop().unwrap()
192 } else {
193 Filter::And(parts)
194 }
195}
196
197fn lookup<'a>(metadata: Option<&'a Value>, path: &str) -> Option<&'a Value> {
198 path.split('.')
199 .try_fold(metadata?, |value, key| value.get(key))
200}
201
202fn same(a: &Value, b: &Value) -> bool {
204 match (a, b) {
205 (Value::Number(x), Value::Number(y)) => x.as_f64() == y.as_f64(),
206 _ => a == b,
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use serde_json::json;
213
214 use super::*;
215
216 #[test]
217 fn parses_and_matches_mongo_style_filters() {
218 let filter = Filter::from_json(&json!({
219 "lang": "en",
220 "year": {"$gte": 2020, "$lt": 2025},
221 "source.kind": {"$in": ["docs", "blog"]}
222 }))
223 .unwrap();
224 let doc = json!({"lang": "en", "year": 2022, "source": {"kind": "docs"}});
225 assert!(filter.matches(Some(&doc)));
226 assert!(!filter.matches(Some(
227 &json!({"lang": "en", "year": 2025, "source": {"kind": "docs"}})
228 )));
229 assert!(!filter.matches(Some(
230 &json!({"lang": "de", "year": 2022, "source": {"kind": "docs"}})
231 )));
232 assert!(!filter.matches(None));
233 }
234
235 #[test]
236 fn numbers_compare_by_value() {
237 assert!(Filter::eq("n", json!(1)).matches(Some(&json!({"n": 1.0}))));
238 }
239
240 #[test]
241 fn empty_filter_matches_everything() {
242 let filter = Filter::from_json(&json!({})).unwrap();
243 assert!(filter.matches(None));
244 }
245
246 #[test]
247 fn rejects_unknown_operators() {
248 assert!(Filter::from_json(&json!({"a": {"$regex": "x"}})).is_err());
249 assert!(Filter::from_json(&json!([1, 2])).is_err());
250 }
251}