use std::cmp::Ordering;
use serde_json::Value;
use crate::schema::HaysonRecord;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComparisonOp {
Eq,
Ne,
Gt,
Gte,
Lt,
Lte,
In,
Contains,
StartsWith,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Predicate {
pub field: String,
pub op: ComparisonOp,
pub value: Value,
}
impl Predicate {
pub fn new(field: impl Into<String>, op: ComparisonOp, value: Value) -> Self {
Predicate {
field: field.into(),
op,
value,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Combine {
And,
Or,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PredicateNode {
Leaf(Predicate),
Group {
combine: Combine,
nodes: Vec<PredicateNode>,
},
}
impl PredicateNode {
pub fn leaf(field: impl Into<String>, op: ComparisonOp, value: Value) -> Self {
PredicateNode::Leaf(Predicate::new(field, op, value))
}
pub fn and(nodes: Vec<PredicateNode>) -> Self {
PredicateNode::Group {
combine: Combine::And,
nodes,
}
}
pub fn or(nodes: Vec<PredicateNode>) -> Self {
PredicateNode::Group {
combine: Combine::Or,
nodes,
}
}
}
pub fn same_value(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Array(left), Value::Array(right)) => {
left.len() == right.len() && left.iter().zip(right.iter()).all(|(x, y)| x == y)
}
_ => a == b,
}
}
fn is_nullish(value: &Value) -> bool {
matches!(value, Value::Null)
}
pub fn compare_values(a: &Value, b: &Value) -> Ordering {
if a == b {
return Ordering::Equal;
}
if is_nullish(a) {
return Ordering::Less;
}
if is_nullish(b) {
return Ordering::Greater;
}
if let (Value::Number(x), Value::Number(y)) = (a, b) {
if let (Some(x), Some(y)) = (x.as_f64(), y.as_f64()) {
return x.partial_cmp(&y).unwrap_or(Ordering::Equal);
}
}
if let (Value::Bool(x), Value::Bool(y)) = (a, b) {
return x.cmp(y);
}
let left = value_as_string(a);
let right = value_as_string(b);
left.cmp(&right)
}
fn value_as_string(value: &Value) -> String {
match value {
Value::String(s) => s.clone(),
other => other.to_string(),
}
}
pub fn test_predicate(record: &HaysonRecord, predicate: &Predicate) -> bool {
let value = record.get(&predicate.field);
match predicate.op {
ComparisonOp::Eq => value
.map(|v| same_value(v, &predicate.value))
.unwrap_or_else(|| same_value(&Value::Null, &predicate.value)),
ComparisonOp::Ne => !test_predicate(
record,
&Predicate {
field: predicate.field.clone(),
op: ComparisonOp::Eq,
value: predicate.value.clone(),
},
),
ComparisonOp::Gt => {
value.is_some_and(|v| !is_nullish(v) && compare_values(v, &predicate.value) == Ordering::Greater)
}
ComparisonOp::Gte => {
value.is_some_and(|v| !is_nullish(v) && compare_values(v, &predicate.value) != Ordering::Less)
}
ComparisonOp::Lt => {
value.is_some_and(|v| !is_nullish(v) && compare_values(v, &predicate.value) == Ordering::Less)
}
ComparisonOp::Lte => {
value.is_some_and(|v| !is_nullish(v) && compare_values(v, &predicate.value) != Ordering::Greater)
}
ComparisonOp::In => match &predicate.value {
Value::Array(items) => value.is_some_and(|v| items.iter().any(|item| same_value(v, item))),
_ => false,
},
ComparisonOp::Contains => match value {
Some(Value::Array(items)) => items.iter().any(|item| item == &predicate.value),
Some(Value::String(text)) => match &predicate.value {
Value::String(needle) => text.contains(needle.as_str()),
_ => false,
},
_ => false,
},
ComparisonOp::StartsWith => match (value, &predicate.value) {
(Some(Value::String(text)), Value::String(prefix)) => text.starts_with(prefix.as_str()),
_ => false,
},
}
}
pub fn test_node(record: &HaysonRecord, node: &PredicateNode) -> bool {
match node {
PredicateNode::Leaf(predicate) => test_predicate(record, predicate),
PredicateNode::Group {
combine: Combine::And,
nodes,
} => nodes.iter().all(|child| test_node(record, child)),
PredicateNode::Group {
combine: Combine::Or,
nodes,
} => {
if nodes.is_empty() {
return true;
}
nodes.iter().any(|child| test_node(record, child))
}
}
}
pub fn leaves_of(nodes: &[PredicateNode]) -> Vec<&Predicate> {
nodes
.iter()
.filter_map(|node| match node {
PredicateNode::Leaf(predicate) => Some(predicate),
PredicateNode::Group { .. } => None,
})
.collect()
}
pub fn all_leaves(nodes: &[PredicateNode]) -> Vec<&Predicate> {
let mut leaves = Vec::new();
collect_leaves(nodes, &mut leaves);
leaves
}
fn collect_leaves<'a>(nodes: &'a [PredicateNode], out: &mut Vec<&'a Predicate>) {
for node in nodes {
match node {
PredicateNode::Leaf(predicate) => out.push(predicate),
PredicateNode::Group { nodes, .. } => collect_leaves(nodes, out),
}
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
fn record(pairs: &[(&str, Value)]) -> HaysonRecord {
pairs.iter().map(|(k, v)| (k.to_string(), v.clone())).collect()
}
#[test]
fn tests_every_comparison_operator() {
let row = record(&[
("age", json!(31)),
("tags", json!(["backend", "node"])),
("name", json!("Ania")),
]);
assert!(test_predicate(
&row,
&Predicate::new("age", ComparisonOp::Eq, json!(31))
));
assert!(test_predicate(&row, &Predicate::new("age", ComparisonOp::Ne, json!(1))));
assert!(test_predicate(
&row,
&Predicate::new("age", ComparisonOp::Gt, json!(30))
));
assert!(test_predicate(
&row,
&Predicate::new("age", ComparisonOp::Gte, json!(31))
));
assert!(test_predicate(
&row,
&Predicate::new("age", ComparisonOp::Lt, json!(32))
));
assert!(test_predicate(
&row,
&Predicate::new("age", ComparisonOp::Lte, json!(31))
));
assert!(test_predicate(
&row,
&Predicate::new("age", ComparisonOp::In, json!([1, 31]))
));
assert!(test_predicate(
&row,
&Predicate::new("tags", ComparisonOp::Contains, json!("node"))
));
assert!(test_predicate(
&row,
&Predicate::new("name", ComparisonOp::StartsWith, json!("An"))
));
assert!(!test_predicate(
&row,
&Predicate::new("name", ComparisonOp::StartsWith, json!("Zz"))
));
}
#[test]
fn combines_and_and_or_with_arbitrary_nesting() {
let row = record(&[("role", json!("admin")), ("age", json!(40))]);
let nested = PredicateNode::and(vec![
PredicateNode::leaf("role", ComparisonOp::Eq, json!("admin")),
PredicateNode::or(vec![
PredicateNode::leaf("age", ComparisonOp::Lt, json!(20)),
PredicateNode::leaf("age", ComparisonOp::Gt, json!(30)),
]),
]);
assert!(test_node(&row, &nested));
let unmatched = PredicateNode::and(vec![
PredicateNode::leaf("role", ComparisonOp::Eq, json!("user")),
PredicateNode::or(vec![PredicateNode::leaf("age", ComparisonOp::Gt, json!(30))]),
]);
assert!(!test_node(&row, &unmatched));
}
#[test]
fn treats_an_empty_or_group_as_no_predicate_at_all() {
let row = record(&[("role", json!("admin"))]);
assert!(test_node(&row, &PredicateNode::or(vec![])));
}
}