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}
28
29impl Filter {
30 pub fn eq(field: impl Into<String>, value: impl Into<Value>) -> Self {
31 Filter::Eq {
32 field: field.into(),
33 value: value.into(),
34 }
35 }
36
37 pub fn is_in(field: impl Into<String>, values: impl IntoIterator<Item = Value>) -> Self {
38 Filter::In {
39 field: field.into(),
40 values: values.into_iter().collect(),
41 }
42 }
43
44 pub fn between(field: impl Into<String>, gte: Option<f64>, lte: Option<f64>) -> Self {
46 Filter::Range {
47 field: field.into(),
48 gt: None,
49 gte,
50 lt: None,
51 lte,
52 }
53 }
54
55 pub fn matches(&self, metadata: Option<&Value>) -> bool {
56 match self {
57 Filter::And(filters) => filters.iter().all(|f| f.matches(metadata)),
58 Filter::Eq { field, value } => lookup(metadata, field).is_some_and(|v| same(v, value)),
59 Filter::In { field, values } => {
60 lookup(metadata, field).is_some_and(|v| values.iter().any(|x| same(v, x)))
61 }
62 Filter::Range {
63 field,
64 gt,
65 gte,
66 lt,
67 lte,
68 } => match lookup(metadata, field).and_then(Value::as_f64) {
69 Some(x) => {
70 gt.is_none_or(|b| x > b)
71 && gte.is_none_or(|b| x >= b)
72 && lt.is_none_or(|b| x < b)
73 && lte.is_none_or(|b| x <= b)
74 }
75 None => false,
76 },
77 }
78 }
79
80 pub fn from_json(value: &Value) -> Result<Self> {
89 let object = value
90 .as_object()
91 .ok_or_else(|| Error::InvalidArgument("filter must be a JSON object".into()))?;
92 let mut parts = Vec::with_capacity(object.len());
93 for (field, condition) in object {
94 match condition {
95 Value::Object(ops) if !ops.is_empty() && ops.keys().all(|k| k.starts_with('$')) => {
96 parts.push(parse_operators(field, ops)?);
97 }
98 other => parts.push(Filter::eq(field.clone(), other.clone())),
99 }
100 }
101 Ok(combine(parts))
102 }
103}
104
105fn parse_operators(field: &str, ops: &Map<String, Value>) -> Result<Filter> {
106 let mut parts = Vec::new();
107 let (mut gt, mut gte, mut lt, mut lte) = (None, None, None, None);
108 for (op, arg) in ops {
109 match op.as_str() {
110 "$eq" => parts.push(Filter::eq(field, arg.clone())),
111 "$in" => {
112 let values = arg.as_array().ok_or_else(|| {
113 Error::InvalidArgument(format!("$in on '{field}' expects an array"))
114 })?;
115 parts.push(Filter::is_in(field, values.iter().cloned()));
116 }
117 "$gt" | "$gte" | "$lt" | "$lte" => {
118 let bound = arg.as_f64().ok_or_else(|| {
119 Error::InvalidArgument(format!("{op} on '{field}' expects a number"))
120 })?;
121 match op.as_str() {
122 "$gt" => gt = Some(bound),
123 "$gte" => gte = Some(bound),
124 "$lt" => lt = Some(bound),
125 _ => lte = Some(bound),
126 }
127 }
128 other => {
129 return Err(Error::InvalidArgument(format!(
130 "unsupported filter operator {other}"
131 )));
132 }
133 }
134 }
135 if gt.is_some() || gte.is_some() || lt.is_some() || lte.is_some() {
136 parts.push(Filter::Range {
137 field: field.to_owned(),
138 gt,
139 gte,
140 lt,
141 lte,
142 });
143 }
144 Ok(combine(parts))
145}
146
147fn combine(mut parts: Vec<Filter>) -> Filter {
148 if parts.len() == 1 {
149 parts.pop().unwrap()
150 } else {
151 Filter::And(parts)
152 }
153}
154
155fn lookup<'a>(metadata: Option<&'a Value>, path: &str) -> Option<&'a Value> {
156 path.split('.')
157 .try_fold(metadata?, |value, key| value.get(key))
158}
159
160fn same(a: &Value, b: &Value) -> bool {
162 match (a, b) {
163 (Value::Number(x), Value::Number(y)) => x.as_f64() == y.as_f64(),
164 _ => a == b,
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use serde_json::json;
171
172 use super::*;
173
174 #[test]
175 fn parses_and_matches_mongo_style_filters() {
176 let filter = Filter::from_json(&json!({
177 "lang": "en",
178 "year": {"$gte": 2020, "$lt": 2025},
179 "source.kind": {"$in": ["docs", "blog"]}
180 }))
181 .unwrap();
182 let doc = json!({"lang": "en", "year": 2022, "source": {"kind": "docs"}});
183 assert!(filter.matches(Some(&doc)));
184 assert!(!filter.matches(Some(
185 &json!({"lang": "en", "year": 2025, "source": {"kind": "docs"}})
186 )));
187 assert!(!filter.matches(Some(
188 &json!({"lang": "de", "year": 2022, "source": {"kind": "docs"}})
189 )));
190 assert!(!filter.matches(None));
191 }
192
193 #[test]
194 fn numbers_compare_by_value() {
195 assert!(Filter::eq("n", json!(1)).matches(Some(&json!({"n": 1.0}))));
196 }
197
198 #[test]
199 fn empty_filter_matches_everything() {
200 let filter = Filter::from_json(&json!({})).unwrap();
201 assert!(filter.matches(None));
202 }
203
204 #[test]
205 fn rejects_unknown_operators() {
206 assert!(Filter::from_json(&json!({"a": {"$regex": "x"}})).is_err());
207 assert!(Filter::from_json(&json!([1, 2])).is_err());
208 }
209}