1use std::cmp::Ordering;
2
3use serde_json::Value;
4
5use crate::schema::HaysonRecord;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ComparisonOp {
9 Eq,
10 Ne,
11 Gt,
12 Gte,
13 Lt,
14 Lte,
15 In,
16 Contains,
17 StartsWith,
18}
19
20#[derive(Debug, Clone, PartialEq)]
21pub struct Predicate {
22 pub field: String,
23 pub op: ComparisonOp,
24 pub value: Value,
25}
26
27impl Predicate {
28 pub fn new(field: impl Into<String>, op: ComparisonOp, value: Value) -> Self {
29 Predicate {
30 field: field.into(),
31 op,
32 value,
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum Combine {
39 And,
40 Or,
41}
42
43#[derive(Debug, Clone, PartialEq)]
44pub enum PredicateNode {
45 Leaf(Predicate),
46 Group {
47 combine: Combine,
48 nodes: Vec<PredicateNode>,
49 },
50}
51
52impl PredicateNode {
53 pub fn leaf(field: impl Into<String>, op: ComparisonOp, value: Value) -> Self {
54 PredicateNode::Leaf(Predicate::new(field, op, value))
55 }
56
57 pub fn and(nodes: Vec<PredicateNode>) -> Self {
58 PredicateNode::Group {
59 combine: Combine::And,
60 nodes,
61 }
62 }
63
64 pub fn or(nodes: Vec<PredicateNode>) -> Self {
65 PredicateNode::Group {
66 combine: Combine::Or,
67 nodes,
68 }
69 }
70}
71
72pub fn same_value(a: &Value, b: &Value) -> bool {
73 match (a, b) {
74 (Value::Array(left), Value::Array(right)) => {
75 left.len() == right.len() && left.iter().zip(right.iter()).all(|(x, y)| x == y)
76 }
77 _ => a == b,
78 }
79}
80
81fn is_nullish(value: &Value) -> bool {
82 matches!(value, Value::Null)
83}
84
85pub fn compare_values(a: &Value, b: &Value) -> Ordering {
87 if a == b {
88 return Ordering::Equal;
89 }
90 if is_nullish(a) {
91 return Ordering::Less;
92 }
93 if is_nullish(b) {
94 return Ordering::Greater;
95 }
96 if let (Value::Number(x), Value::Number(y)) = (a, b) {
97 if let (Some(x), Some(y)) = (x.as_f64(), y.as_f64()) {
98 return x.partial_cmp(&y).unwrap_or(Ordering::Equal);
99 }
100 }
101 if let (Value::Bool(x), Value::Bool(y)) = (a, b) {
102 return x.cmp(y);
103 }
104 let left = value_as_string(a);
105 let right = value_as_string(b);
106 left.cmp(&right)
107}
108
109fn value_as_string(value: &Value) -> String {
110 match value {
111 Value::String(s) => s.clone(),
112 other => other.to_string(),
113 }
114}
115
116pub fn test_predicate(record: &HaysonRecord, predicate: &Predicate) -> bool {
117 let value = record.get(&predicate.field);
118 match predicate.op {
119 ComparisonOp::Eq => value
120 .map(|v| same_value(v, &predicate.value))
121 .unwrap_or_else(|| same_value(&Value::Null, &predicate.value)),
122 ComparisonOp::Ne => !test_predicate(
123 record,
124 &Predicate {
125 field: predicate.field.clone(),
126 op: ComparisonOp::Eq,
127 value: predicate.value.clone(),
128 },
129 ),
130 ComparisonOp::Gt => {
131 value.is_some_and(|v| !is_nullish(v) && compare_values(v, &predicate.value) == Ordering::Greater)
132 }
133 ComparisonOp::Gte => {
134 value.is_some_and(|v| !is_nullish(v) && compare_values(v, &predicate.value) != Ordering::Less)
135 }
136 ComparisonOp::Lt => {
137 value.is_some_and(|v| !is_nullish(v) && compare_values(v, &predicate.value) == Ordering::Less)
138 }
139 ComparisonOp::Lte => {
140 value.is_some_and(|v| !is_nullish(v) && compare_values(v, &predicate.value) != Ordering::Greater)
141 }
142 ComparisonOp::In => match &predicate.value {
143 Value::Array(items) => value.is_some_and(|v| items.iter().any(|item| same_value(v, item))),
144 _ => false,
145 },
146 ComparisonOp::Contains => match value {
147 Some(Value::Array(items)) => items.iter().any(|item| item == &predicate.value),
148 Some(Value::String(text)) => match &predicate.value {
149 Value::String(needle) => text.contains(needle.as_str()),
150 _ => false,
151 },
152 _ => false,
153 },
154 ComparisonOp::StartsWith => match (value, &predicate.value) {
155 (Some(Value::String(text)), Value::String(prefix)) => text.starts_with(prefix.as_str()),
156 _ => false,
157 },
158 }
159}
160
161pub fn test_node(record: &HaysonRecord, node: &PredicateNode) -> bool {
162 match node {
163 PredicateNode::Leaf(predicate) => test_predicate(record, predicate),
164 PredicateNode::Group {
165 combine: Combine::And,
166 nodes,
167 } => nodes.iter().all(|child| test_node(record, child)),
168 PredicateNode::Group {
169 combine: Combine::Or,
170 nodes,
171 } => {
172 if nodes.is_empty() {
173 return true;
174 }
175 nodes.iter().any(|child| test_node(record, child))
176 }
177 }
178}
179
180pub fn leaves_of(nodes: &[PredicateNode]) -> Vec<&Predicate> {
182 nodes
183 .iter()
184 .filter_map(|node| match node {
185 PredicateNode::Leaf(predicate) => Some(predicate),
186 PredicateNode::Group { .. } => None,
187 })
188 .collect()
189}
190
191pub fn all_leaves(nodes: &[PredicateNode]) -> Vec<&Predicate> {
192 let mut leaves = Vec::new();
193 collect_leaves(nodes, &mut leaves);
194 leaves
195}
196
197fn collect_leaves<'a>(nodes: &'a [PredicateNode], out: &mut Vec<&'a Predicate>) {
198 for node in nodes {
199 match node {
200 PredicateNode::Leaf(predicate) => out.push(predicate),
201 PredicateNode::Group { nodes, .. } => collect_leaves(nodes, out),
202 }
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use serde_json::json;
209
210 use super::*;
211
212 fn record(pairs: &[(&str, Value)]) -> HaysonRecord {
213 pairs.iter().map(|(k, v)| (k.to_string(), v.clone())).collect()
214 }
215
216 #[test]
217 fn tests_every_comparison_operator() {
218 let row = record(&[
219 ("age", json!(31)),
220 ("tags", json!(["backend", "node"])),
221 ("name", json!("Ania")),
222 ]);
223
224 assert!(test_predicate(
225 &row,
226 &Predicate::new("age", ComparisonOp::Eq, json!(31))
227 ));
228 assert!(test_predicate(&row, &Predicate::new("age", ComparisonOp::Ne, json!(1))));
229 assert!(test_predicate(
230 &row,
231 &Predicate::new("age", ComparisonOp::Gt, json!(30))
232 ));
233 assert!(test_predicate(
234 &row,
235 &Predicate::new("age", ComparisonOp::Gte, json!(31))
236 ));
237 assert!(test_predicate(
238 &row,
239 &Predicate::new("age", ComparisonOp::Lt, json!(32))
240 ));
241 assert!(test_predicate(
242 &row,
243 &Predicate::new("age", ComparisonOp::Lte, json!(31))
244 ));
245 assert!(test_predicate(
246 &row,
247 &Predicate::new("age", ComparisonOp::In, json!([1, 31]))
248 ));
249 assert!(test_predicate(
250 &row,
251 &Predicate::new("tags", ComparisonOp::Contains, json!("node"))
252 ));
253 assert!(test_predicate(
254 &row,
255 &Predicate::new("name", ComparisonOp::StartsWith, json!("An"))
256 ));
257 assert!(!test_predicate(
258 &row,
259 &Predicate::new("name", ComparisonOp::StartsWith, json!("Zz"))
260 ));
261 }
262
263 #[test]
264 fn combines_and_and_or_with_arbitrary_nesting() {
265 let row = record(&[("role", json!("admin")), ("age", json!(40))]);
266
267 let nested = PredicateNode::and(vec![
268 PredicateNode::leaf("role", ComparisonOp::Eq, json!("admin")),
269 PredicateNode::or(vec![
270 PredicateNode::leaf("age", ComparisonOp::Lt, json!(20)),
271 PredicateNode::leaf("age", ComparisonOp::Gt, json!(30)),
272 ]),
273 ]);
274 assert!(test_node(&row, &nested));
275
276 let unmatched = PredicateNode::and(vec![
277 PredicateNode::leaf("role", ComparisonOp::Eq, json!("user")),
278 PredicateNode::or(vec![PredicateNode::leaf("age", ComparisonOp::Gt, json!(30))]),
279 ]);
280 assert!(!test_node(&row, &unmatched));
281 }
282
283 #[test]
284 fn treats_an_empty_or_group_as_no_predicate_at_all() {
285 let row = record(&[("role", json!("admin"))]);
286 assert!(test_node(&row, &PredicateNode::or(vec![])));
287 }
288}