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