Hi all! I'm a beginner in Rust and coming from Python.
TLDR - DSL that filters arrays of jsons recursively evaluates the ast on EACH iteration. How to avoid it?
Rust Playground link
Long story follows. Warning! A lot of letters!
Lately I've been toying with the idea of making a little interpreter for DSL that is able to filter out an array of jsons ( serde_json::Value
). The criteria is known only at runtime (say, accepted via a network request or supplied by the user via stdin).
I kinda did it, but it is wildly inefficient due to the way it is implemented. I was hoping that more experienced peers might help me out.
So, I have a few "primitives" in my "language".
Path
- which represents a json pointer
It is defined like so:
enum Path {
Path(String),
}
impl Path {
pub fn get_path(&self) -> &str {
match self {
Self::Path(x) => x,
}
}
}
Ops
- which essentially represents or your usual comparison operators ( ==
, >=
, etc.)
This is the definition
enum Ops {
Eq,
Ne,
Gt,
Lt,
Gte,
Lte,
}
Expr
- this is an enum which represent my AST
Definition follows
enum Expr {
And(Box<Expr>, Box<Expr>),
Or(Box<Expr>, Box<Expr>),
Xor(Box<Expr>, Box<Expr>),
Compare { lhs: Path, op: Ops, rhs: i64 },
Exists { path: Path },
}
And
, Or
, Xor
variants are pretty self explanatory,
Compare
variant represents an operation where I take the value from my json which lies by pointer in lhs
and compare it using Ops
to some value.
Exists
simply checks that value in Path
exists in supplied json.
Compare
and Exists
are evaluated by using macroses, here they are
macro_rules! compare {
($json_identifier:ident, $path:expr, $operation:tt, $value_to_compare_with:expr) => {
{
let p_str = $path.get_path();
let value_in_pointer_option = $json_identifier.pointer(p_str);
if let Some(inner) = value_in_pointer_option {
inner.as_i64().unwrap() $operation *$value_to_compare_with
} else {
false
}
}
};
}
macro_rules! exists {
($json_identifier:ident, $pointer_string:expr) => {
$json_identifier.pointer($pointer_string).is_some()
};
}
And finally the function that evaluates all of that is defined like that
fn ast_eval(ast: &Expr, v: &Value) -> bool {
match ast {
Expr::Compare { lhs, op, rhs } => match *op {
Ops::Eq => compare!(v, lhs, ==, rhs),
Ops::Gt => compare!(v, lhs, >, rhs),
Ops::Lt => compare!(v, lhs, <, rhs),
Ops::Gte => compare!(v, lhs, >=, rhs),
Ops::Lte => compare!(v, lhs, <=, rhs),
Ops::Ne => compare!(v, lhs, !=, rhs),
},
Expr::Exists { path } => exists!(v, path.get_path()),
Expr::And(x, y) => ast_eval(x.as_ref(), v) & ast_eval(y.as_ref(), v),
Expr::Or(x, y) => ast_eval(x.as_ref(), v) | ast_eval(y.as_ref(), v),
Expr::Xor(x, y) => ast_eval(x.as_ref(), v) ^ ast_eval(y.as_ref(), v),
}
}
Here is the problem. To find out if some json v
satisfies condition in my ast, I have to recursively traverse it for EACH of v in the array.
I was thinking of making a macro out of ast_eval
so it would "collect" all of the ifs
that are generated by compare!
and exists
macroses, and use it in a .filter
method as a closure or something along the lines. But, alas, I couldn't wrap my head around how I would approach it. Or maybe there is another way to "compile" the ast
once and reuse it for each json. Thank you in advance for any advice or critique!