Skip to main content

hayson_engine/query/
predicates.rs

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
85// NOTE: mirrors the TS numeric/string fallback of compareValues, not a general JSON ordering.
86pub 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
180/// Direct-level leaves of `nodes` — nested groups are skipped, not descended into.
181pub 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}