filter_expr/
lib.rs

1//! A library for parsing the filter expression.
2
3mod ctx;
4mod error;
5mod expr;
6mod parser;
7mod token;
8
9pub use ctx::{SimpleContext, Context};
10pub use error::Error;
11pub use expr::{Expr, ExprValue, BoxedExprFn, ExprFn};
12
13/// The filter expression.
14pub struct FilterExpr {
15    /// The expression of the filter. Possibly empty.
16    expr: Option<Expr>,
17}
18
19impl FilterExpr {
20    pub fn parse(expr: &str) -> Result<Self, Error> {
21        let expr = parse_expr(expr)?;
22        Ok(Self { expr: Some(expr) })
23    }
24
25    pub fn new(expr: Option<Expr>) -> Self {
26        Self { expr }
27    }
28
29    pub fn expr(&self) -> Option<&Expr> {
30        self.expr.as_ref()
31    }
32
33    pub async fn eval(&self, ctx: &dyn Context) -> Result<bool, Error> {
34        if let Some(expr) = &self.expr {
35            let value = expr.eval(ctx).await?;
36            match value {
37                ExprValue::Bool(b) => Ok(b),
38                _ => Err(Error::InvalidValue(format!("{:?}", value))),
39            }
40        } else {
41            Ok(true)
42        }
43    }
44}
45
46fn parse_expr(input: &str) -> Result<Expr, Error> {
47    let tokens = token::parse_token(input)?;
48    let mut parser = parser::Parser::new(tokens);
49    Ok(parser.parse_expr()?)
50}
51
52#[cfg(test)]
53mod tests {
54    use crate::expr::{ExprFn, ExprFnContext};
55
56    use super::*;
57
58    #[tokio::test]
59    async fn test_parse_expr_and_then_eval() {
60        // Parse the filter-expr:
61        //
62        //     name = 'John' AND age > 18 AND 1 > 0
63        // =====================================================================
64
65        let input = "name = 'John' AND age > 18 AND 1 > 0";
66        let filter_expr = FilterExpr::parse(input).unwrap();
67
68        let ctx = simple_context! {
69            "name": "John",
70            "age": 19,
71        };
72        let result = filter_expr.eval(&ctx).await.unwrap();
73        assert_eq!(result, true);
74
75        let ctx = simple_context! {
76            "name": "John",
77            "age": 18,
78        };
79        let result = filter_expr.eval(&ctx).await.unwrap();
80        assert_eq!(result, false);
81
82        // Parse the filter-expr:
83        //
84        //     name = "John" AND age IN [18, 19, 20, 22] AND 1 > 0
85        // =====================================================================
86
87        let input = r#"name = "John" AND age IN [18, 19, 20, 22] AND 1 > 0"#;
88        let filter_expr = FilterExpr::parse(input).unwrap();
89
90        let ctx = simple_context! {
91            "name": "John",
92            "age": 19,
93        };
94        let result = filter_expr.eval(&ctx).await.unwrap();
95        assert_eq!(result, true.into());
96
97        let ctx = simple_context! {
98            "name": "John",
99            "age": 23,
100        };
101        let result = filter_expr.eval(&ctx).await.unwrap();
102        assert_eq!(result, false.into());
103
104        // Parse the filter-expr:
105        //
106        //     matches(name, "^J.*n$")
107        // =====================================================================
108
109        let input = r#"matches(name, "^J.*n$")"#;
110        let filter_expr = FilterExpr::parse(input).unwrap();
111
112        let ctx = simple_context! {
113            "name": "John",
114        };
115        let result = filter_expr.eval(&ctx).await.unwrap();
116        assert_eq!(result, true);
117
118        let ctx = simple_context! {
119            "name": "Jane",
120        };
121        let result = filter_expr.eval(&ctx).await.unwrap();
122        assert_eq!(result, false);
123
124        // Parse the filter-expr:
125        //
126        //     custom_add(1, 2) = 3
127        // =====================================================================
128
129        let input = r#"custom_add(1, 2) = a"#;
130        let filter_expr = FilterExpr::parse(input).unwrap();
131
132        struct CustomAddFn;
133        #[async_trait::async_trait]
134        impl ExprFn for CustomAddFn {
135            async fn call(&self, ctx: ExprFnContext) -> Result<ExprValue, Error> {
136                if ctx.args.len() != 2 {
137                    return Err(Error::InvalidArgumentCount {
138                        expected: 2,
139                        got: ctx.args.len(),
140                    });
141                }
142                let a = match ctx.args[0] {
143                    ExprValue::Int(a) => a,
144                    _ => {
145                        return Err(Error::InvalidArgumentType {
146                            expected: "integer".to_string(),
147                            got: format!("{:?}", ctx.args[0]),
148                        });
149                    }
150                };
151                let b = match ctx.args[1] {
152                    ExprValue::Int(b) => b,
153                    _ => {
154                        return Err(Error::InvalidArgumentType {
155                            expected: "integer".to_string(),
156                            got: format!("{:?}", ctx.args[1]),
157                        });
158                    }
159                };
160                Ok((a + b).into())
161            }
162        }
163
164        let mut ctx = simple_context! {
165            "a": 3,
166        };
167        ctx.add_fn("custom_add".to_string(), Box::new(CustomAddFn));
168        let result = filter_expr.eval(&ctx).await.unwrap();
169        assert_eq!(result, true);
170
171        let mut ctx = simple_context! {
172            "a": 4,
173        };
174        ctx.add_fn("custom_add".to_string(), Box::new(CustomAddFn));
175        let result = filter_expr.eval(&ctx).await.unwrap();
176        assert_eq!(result, false);
177
178        // Parse the filter-expr:
179        //
180        //     name != null
181        // =====================================================================
182
183        let input = r#"name != null"#;
184        let filter_expr = FilterExpr::parse(input).unwrap();
185
186        let ctx = simple_context! {
187            "name": ExprValue::Null,
188        };
189        let result = filter_expr.eval(&ctx).await.unwrap();
190        assert_eq!(result, false);
191
192        let ctx = simple_context! {
193            "name": "John",
194        };
195        let result = filter_expr.eval(&ctx).await.unwrap();
196        assert_eq!(result, true);
197
198        // Parse the filter-expr:
199        //
200        //     open > 1.5 AND age > 17.5 AND age < 18.5 AND is_peter = true
201        // =====================================================================
202
203        let input = r#"open > 1.5 AND age > 17.5 AND age < 18.5 AND is_peter = true"#;
204        let filter_expr = FilterExpr::parse(input).unwrap();
205
206        let ctx = simple_context! {
207            "open": 1.6,
208            "age": 18,
209            "is_peter": true,
210        };
211        let result = filter_expr.eval(&ctx).await.unwrap();
212        assert_eq!(result, true);
213    }
214}