nodedb_query/
metadata_filter.rs1use nodedb_types::filter::MetadataFilter;
10
11use crate::json_ops::{coerced_eq, compare_json_optional};
12
13pub fn matches_metadata_filter(doc: &serde_json::Value, filter: &MetadataFilter) -> bool {
17 match filter {
18 MetadataFilter::Eq { field, value } => {
19 let field_val = doc.get(field.as_str());
20 let filter_val = value_to_json(value);
21 match field_val {
22 Some(fv) => coerced_eq(fv, &filter_val),
23 None => filter_val.is_null(),
24 }
25 }
26 MetadataFilter::Ne { field, value } => {
27 let field_val = doc.get(field.as_str());
28 let filter_val = value_to_json(value);
29 match field_val {
30 Some(fv) => !coerced_eq(fv, &filter_val),
31 None => !filter_val.is_null(),
32 }
33 }
34 MetadataFilter::Gt { field, value } => {
35 let field_val = doc.get(field.as_str());
36 let filter_val = value_to_json(value);
37 compare_json_optional(field_val, Some(&filter_val)) == std::cmp::Ordering::Greater
38 }
39 MetadataFilter::Gte { field, value } => {
40 let field_val = doc.get(field.as_str());
41 let filter_val = value_to_json(value);
42 let cmp = compare_json_optional(field_val, Some(&filter_val));
43 cmp == std::cmp::Ordering::Greater || cmp == std::cmp::Ordering::Equal
44 }
45 MetadataFilter::Lt { field, value } => {
46 let field_val = doc.get(field.as_str());
47 let filter_val = value_to_json(value);
48 compare_json_optional(field_val, Some(&filter_val)) == std::cmp::Ordering::Less
49 }
50 MetadataFilter::Lte { field, value } => {
51 let field_val = doc.get(field.as_str());
52 let filter_val = value_to_json(value);
53 let cmp = compare_json_optional(field_val, Some(&filter_val));
54 cmp == std::cmp::Ordering::Less || cmp == std::cmp::Ordering::Equal
55 }
56 MetadataFilter::In { field, values } => {
57 let field_val = match doc.get(field.as_str()) {
58 Some(v) => v,
59 None => return false,
60 };
61 values
62 .iter()
63 .any(|v| coerced_eq(field_val, &value_to_json(v)))
64 }
65 MetadataFilter::NotIn { field, values } => {
66 let field_val = match doc.get(field.as_str()) {
67 Some(v) => v,
68 None => return true,
69 };
70 !values
71 .iter()
72 .any(|v| coerced_eq(field_val, &value_to_json(v)))
73 }
74 MetadataFilter::And(filters) => filters.iter().all(|f| matches_metadata_filter(doc, f)),
75 MetadataFilter::Or(filters) => filters.iter().any(|f| matches_metadata_filter(doc, f)),
76 MetadataFilter::Not(inner) => !matches_metadata_filter(doc, inner),
77 _ => false,
78 }
79}
80
81fn value_to_json(value: &nodedb_types::value::Value) -> serde_json::Value {
83 match value {
84 nodedb_types::value::Value::Null => serde_json::Value::Null,
85 nodedb_types::value::Value::Bool(b) => serde_json::Value::Bool(*b),
86 nodedb_types::value::Value::Integer(i) => serde_json::json!(i),
87 nodedb_types::value::Value::Float(f) => serde_json::Number::from_f64(*f)
88 .map(serde_json::Value::Number)
89 .unwrap_or(serde_json::Value::Null),
90 nodedb_types::value::Value::String(s) => serde_json::Value::String(s.clone()),
91 _ => serde_json::to_value(value).unwrap_or(serde_json::Value::Null),
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use nodedb_types::filter::MetadataFilter;
99 use nodedb_types::value::Value;
100 use serde_json::json;
101
102 #[test]
103 fn eq_match() {
104 let doc = json!({"status": "active", "age": 25});
105 let filter = MetadataFilter::eq("status", "active");
106 assert!(matches_metadata_filter(&doc, &filter));
107 }
108
109 #[test]
110 fn eq_no_match() {
111 let doc = json!({"status": "inactive"});
112 let filter = MetadataFilter::eq("status", "active");
113 assert!(!matches_metadata_filter(&doc, &filter));
114 }
115
116 #[test]
117 fn gt_numeric() {
118 let doc = json!({"age": 30});
119 let filter = MetadataFilter::Gt {
120 field: "age".into(),
121 value: Value::Integer(25),
122 };
123 assert!(matches_metadata_filter(&doc, &filter));
124 }
125
126 #[test]
127 fn and_filter() {
128 let doc = json!({"status": "active", "age": 30});
129 let filter = MetadataFilter::and(vec![
130 MetadataFilter::eq("status", "active"),
131 MetadataFilter::Gt {
132 field: "age".into(),
133 value: Value::Integer(25),
134 },
135 ]);
136 assert!(matches_metadata_filter(&doc, &filter));
137 }
138
139 #[test]
140 fn or_filter() {
141 let doc = json!({"status": "inactive", "age": 30});
142 let filter = MetadataFilter::or(vec![
143 MetadataFilter::eq("status", "active"),
144 MetadataFilter::Gt {
145 field: "age".into(),
146 value: Value::Integer(25),
147 },
148 ]);
149 assert!(matches_metadata_filter(&doc, &filter));
150 }
151
152 #[test]
153 fn not_filter() {
154 let doc = json!({"status": "active"});
155 let filter = MetadataFilter::Not(Box::new(MetadataFilter::eq("status", "inactive")));
156 assert!(matches_metadata_filter(&doc, &filter));
157 }
158
159 #[test]
160 fn in_filter() {
161 let doc = json!({"role": "admin"});
162 let filter = MetadataFilter::In {
163 field: "role".into(),
164 values: vec![Value::from("admin"), Value::from("superadmin")],
165 };
166 assert!(matches_metadata_filter(&doc, &filter));
167 }
168
169 #[test]
170 fn missing_field() {
171 let doc = json!({"name": "Alice"});
172 let filter = MetadataFilter::eq("status", "active");
173 assert!(!matches_metadata_filter(&doc, &filter));
174 }
175}