Skip to main content

sheeter_engine/
filter.rs

1use crate::CanonicalValue;
2use std::error::Error;
3
4/// Comparison operators supported by the filter mini-DSL.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Op {
7    Eq,
8    Ne,
9    Lt,
10    Le,
11    Gt,
12    Ge,
13}
14
15/// A parsed filter expression.
16///
17/// The mini-DSL supports a conjunction of comparisons joined by `and`:
18///
19/// ```text
20/// filter := clause ( " and " clause )*
21/// clause := field op literal
22/// op     := "==" | "!=" | "<=" | ">=" | "<" | ">"
23/// literal := number | 'string' | "string" | true | false | null | bareword
24/// ```
25///
26/// # Examples
27///
28/// ```
29/// use sheeter_engine::Filter;
30/// use sheeter_engine::CanonicalValue;
31///
32/// let f = Filter::parse("age > 30 and name == 'Bob'").unwrap();
33/// let row = CanonicalValue::Object(vec![
34///     ("age".to_string(), CanonicalValue::Number(serde_json::Number::from(40))),
35///     ("name".to_string(), CanonicalValue::String("Bob".to_string())),
36/// ]);
37/// assert!(f.matches(&row));
38/// ```
39#[derive(Debug, Clone)]
40pub struct Filter {
41    clauses: Vec<Clause>,
42}
43
44#[derive(Debug, Clone)]
45struct Clause {
46    field: String,
47    op: Op,
48    literal: Literal,
49}
50
51#[derive(Debug, Clone)]
52enum Literal {
53    Null,
54    Bool(bool),
55    Number(f64),
56    String(String),
57}
58
59impl Filter {
60    /// Parses a filter expression.
61    ///
62    /// # Errors
63    /// Returns an error if the expression has no valid clause.
64    pub fn parse(input: &str) -> Result<Self, Box<dyn Error>> {
65        let mut clauses = Vec::new();
66        for raw in input.split(" and ") {
67            let part = raw.trim();
68            if part.is_empty() {
69                continue;
70            }
71            let mut found = false;
72            for (op_str, op) in [
73                ("==", Op::Eq),
74                ("!=", Op::Ne),
75                ("<=", Op::Le),
76                (">=", Op::Ge),
77                ("<", Op::Lt),
78                (">", Op::Gt),
79            ] {
80                if let Some(idx) = part.find(op_str) {
81                    let field = part[..idx].trim().to_string();
82                    let lit_str = part[idx + op_str.len()..].trim();
83                    if field.is_empty() || lit_str.is_empty() {
84                        return Err(format!("filter: empty field or value in {part:?}").into());
85                    }
86                    clauses.push(Clause {
87                        field,
88                        op,
89                        literal: parse_literal(lit_str),
90                    });
91                    found = true;
92                    break;
93                }
94            }
95            if !found {
96                return Err(format!("filter: no operator in {part:?}").into());
97            }
98        }
99        if clauses.is_empty() {
100            return Err("filter: no valid clauses".into());
101        }
102        Ok(Self { clauses })
103    }
104
105    /// Returns true if the row (a `CanonicalValue::Object`) satisfies the filter.
106    pub fn matches(&self, row: &CanonicalValue) -> bool {
107        let obj = match row {
108            CanonicalValue::Object(o) => o,
109            _ => return false,
110        };
111        self.clauses.iter().all(|c| c.matches(obj))
112    }
113}
114
115fn parse_literal(s: &str) -> Literal {
116    if s.eq_ignore_ascii_case("null") {
117        return Literal::Null;
118    }
119    if s.eq_ignore_ascii_case("true") {
120        return Literal::Bool(true);
121    }
122    if s.eq_ignore_ascii_case("false") {
123        return Literal::Bool(false);
124    }
125    if s.len() >= 2 {
126        let b = s.as_bytes();
127        if (b[0] == b'\'' && b[b.len() - 1] == b'\'') || (b[0] == b'"' && b[b.len() - 1] == b'"') {
128            return Literal::String(s[1..s.len() - 1].to_string());
129        }
130    }
131    if let Ok(n) = s.parse::<f64>() {
132        return Literal::Number(n);
133    }
134    Literal::String(s.to_string())
135}
136
137impl Clause {
138    fn matches(&self, obj: &[(String, CanonicalValue)]) -> bool {
139        let cell = match obj.iter().find(|(k, _)| k == &self.field) {
140            Some((_, v)) => v,
141            None => return false,
142        };
143        match self.op {
144            Op::Eq => eq(cell, &self.literal),
145            Op::Ne => !eq(cell, &self.literal),
146            Op::Lt => ord(cell, &self.literal) == Some(std::cmp::Ordering::Less),
147            Op::Le => matches!(
148                ord(cell, &self.literal),
149                Some(std::cmp::Ordering::Less) | Some(std::cmp::Ordering::Equal)
150            ),
151            Op::Gt => ord(cell, &self.literal) == Some(std::cmp::Ordering::Greater),
152            Op::Ge => matches!(
153                ord(cell, &self.literal),
154                Some(std::cmp::Ordering::Greater) | Some(std::cmp::Ordering::Equal)
155            ),
156        }
157    }
158}
159
160fn as_f64(v: &CanonicalValue) -> Option<f64> {
161    match v {
162        CanonicalValue::Number(n) => n.as_f64(),
163        CanonicalValue::String(s) => s.parse().ok(),
164        CanonicalValue::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
165        _ => None,
166    }
167}
168
169fn eq(cell: &CanonicalValue, lit: &Literal) -> bool {
170    match (cell, lit) {
171        (CanonicalValue::Null, Literal::Null) => true,
172        (CanonicalValue::Bool(b), Literal::Bool(l)) => *b == *l,
173        (CanonicalValue::String(s), Literal::String(l)) => s == l,
174        _ => match (as_f64(cell), as_f64_lit(lit)) {
175            (Some(a), Some(b)) => (a - b).abs() < 1e-10,
176            _ => false,
177        },
178    }
179}
180
181fn as_f64_lit(l: &Literal) -> Option<f64> {
182    match l {
183        Literal::Number(n) => Some(*n),
184        Literal::String(s) => s.parse().ok(),
185        Literal::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
186        Literal::Null => None,
187    }
188}
189
190fn ord(cell: &CanonicalValue, lit: &Literal) -> Option<std::cmp::Ordering> {
191    if let (CanonicalValue::String(a), Literal::String(b)) = (cell, lit) {
192        return Some(a.cmp(b));
193    }
194    let a = as_f64(cell)?;
195    let b = as_f64_lit(lit)?;
196    a.partial_cmp(&b)
197}