/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
//! A JEXL evaluator written in Rust //! This crate depends on a JEXL parser crate that handles all the parsing //! and is a part of the same workspace. //! JEXL is an expression language used by Mozilla, you can find more information here: https://github.com/mozilla/mozjexl //! //! # How to use //! The access point for this crate is the `eval` functions of the Evaluator Struct //! You can use the `eval` function directly to evaluate standalone statements //! //! For example: //! ```rust //! use jexl_eval::Evaluator; //! use serde_json::json as value; //! let evaluator = Evaluator::new(); //! assert_eq!(evaluator.eval("'Hello ' + 'World'").unwrap(), value!("Hello World")); //! ``` //! //! You can also run the statements against a context using the `eval_in_context` function //! The context can be any type that implements the `serde::Serializable` trait //! and the function will return errors if the statement doesn't match the context //! //! For example: //! ```rust //! use jexl_eval::Evaluator; //! use serde_json::json as value; //! let context = value!({"a": {"b": 2.0}}); //! let evaluator = Evaluator::new(); //! assert_eq!(evaluator.eval_in_context("a.b", context).unwrap(), value!(2.0)); //! ``` //!
use jexl_parser::{
ast::{Expression, OpCode},
Parser,
}; use serde_json::{json as value, Value};
pubmod error; use error::*; use std::collections::HashMap;
const EPSILON: f64 = 0.000001f64;
trait Truthy { fn is_truthy(&self) -> bool;
}
impl Truthy for Value { fn is_truthy(&self) -> bool { matchself {
Value::Bool(b) => *b,
Value::Null => false,
Value::Number(f) => f.as_f64().unwrap() != 0.0,
Value::String(s) => !s.is_empty(), // It would be better if these depended on the contents of the // object (empty array/object is falsey, non-empty is truthy, like // in Python) but this matches JS semantics. Is it worth changing?
Value::Array(_) => true,
Value::Object(_) => true,
}
}
}
/// TransformFn represents an arbitrary transform function /// Transform functions take an arbitrary number of `serde_json::Value`to represent their arguments /// and return a `serde_json::Value`. /// the transform function itself is responsible for checking if the format and number of /// the arguments is correct /// /// Returns a Result with an `anyhow::Error`. This allows consumers to return their own custom errors /// in the closure, and use `.into` to convert it into an `anyhow::Error`. The error message will be perserved pubtype TransformFn<'a> = Box<dyn Fn(&[Value]) -> Result<Value, anyhow::Error> + Send + Sync + 'a>;
/// Adds a custom transform function /// This is meant as a way to allow consumers to add their own custom functionality /// to the expression language. /// Note that the name added here has to match with /// the name that the transform will have when it's a part of the expression statement /// /// # Arguments: /// - `name`: The name of the transfrom /// - `transform`: The actual function. A closure the implements Fn(&[serde_json::Value]) -> Result<Value, anyhow::Error> /// /// # Example: /// /// ```rust /// use jexl_eval::Evaluator; /// use serde_json::{json as value, Value}; /// /// let mut evaluator = Evaluator::new().with_transform("lower", |v: &[Value]| { /// let s = v /// .first() /// .expect("Should have 1 argument!") /// .as_str() /// .expect("Should be a string!"); /// Ok(value!(s.to_lowercase())) /// }); /// /// assert_eq!(evaluator.eval("'JOHN DOe'|lower").unwrap(), value!("john doe")) /// ``` pubfn with_transform<F>(mutself, name: &str, transform: F) -> Self where
F: Fn(&[Value]) -> Result<Value, anyhow::Error> + Send + Sync + 'a,
{ self.transforms
.insert(name.to_string(), Box::new(transform)); self
}
Expression::IndexOperation { subject, index } => { let subject = self.eval_ast(*subject, context)?; iflet Expression::Filter { ident, op, right } = *index { let subject_arr = subject.as_array().ok_or(EvaluationError::InvalidFilter)?; let right = self.eval_ast(*right, context)?; let filtered = subject_arr
.iter()
.filter(|e| { let left = e.get(&ident).unwrap_or(&value!(null)); // returns false if any members fail the op, could happen if array members are missing the identifier Self::apply_op(op, left.clone(), right.clone())
.unwrap_or(value!(false))
.is_truthy()
})
.collect::<Vec<_>>(); return Ok(value!(filtered));
}
let index = self.eval_ast(*index, context)?; match index {
Value::String(inner) => {
Ok(subject.get(&inner).unwrap_or(&value!(null)).clone())
}
Value::Number(inner) => Ok(subject
.get(inner.as_f64().unwrap().floor() as usize)
.unwrap_or(&value!(null))
.clone()),
_ => Err(EvaluationError::InvalidIndexType),
}
}
Expression::Filter {
ident: _,
op: _,
right: _,
} => { // Filters shouldn't be evaluated individually // instead, they are evaluated as a part of an IndexOperation return Err(EvaluationError::InvalidFilter);
}
}
}
// We want to delay evaluating the right hand side in the cases of AND and OR. let eval_right = || self.eval_ast(*right, context);
Ok(match operation {
OpCode::Or => { if left.is_truthy() {
left?
} else {
eval_right()?
}
}
OpCode::And => { if left.is_truthy() {
eval_right()?
} else {
left?
}
}
_ => Self::apply_op(operation, left?, eval_right()?)?,
})
}
fn apply_op<'b>(operation: OpCode, left: Value, right: Value) -> Result<'b, Value> { match (operation, left, right) {
(OpCode::NotEqual, a, b) => { // Implement NotEquals as the inverse of Equals. let value = Self::apply_op(OpCode::Equal, a, b)?; let equality = value
.as_bool()
.unwrap_or_else(|| unreachable!("Equality always returns a bool"));
Ok(value!(!equality))
}
(OpCode::And, a, b) => Ok(if a.is_truthy() { b } else { a }),
(OpCode::Or, a, b) => Ok(if a.is_truthy() { a } else { b }),
#[test] // Test a very simple transform that applies to_lowercase to a string fn test_simple_transform() { let evaluator = Evaluator::new().with_transform("lower", |v: &[Value]| { let s = v
.get(0)
.expect("There should be one argument!")
.as_str()
.expect("Should be a string!");
Ok(value!(s.to_lowercase()))
});
assert_eq!(evaluator.eval("'T_T'|lower").unwrap(), value!("t_t"));
}
#[test] // Test returning an UnknownTransform error if a transform is unknown fn test_missing_transform() { let err = Evaluator::new().eval("'hello'|world").unwrap_err(); iflet EvaluationError::UnknownTransform(transform) = err {
assert_eq!(transform, "world")
} else {
panic!("Should have thrown an unknown transform error")
}
}
#[test] // Test returning an UndefinedIdentifier error if an identifier is unknown fn test_undefined_identifier() { let err = Evaluator::new().eval("not_defined").unwrap_err(); iflet EvaluationError::UndefinedIdentifier(id) = err {
assert_eq!(id, "not_defined")
} else {
panic!("Should have thrown an undefined identifier error")
}
}
#[test] // Test returning an UndefinedIdentifier error if an identifier is unknown fn test_undefined_identifier_truthy_ops() { let err = Evaluator::new().eval("not_defined").unwrap_err(); iflet EvaluationError::UndefinedIdentifier(id) = err {
assert_eq!(id, "not_defined")
} else {
panic!("Should have thrown an undefined identifier error")
}
let evaluator = Evaluator::new(); let context = value!({ "NULL": null, "DEFINED": "string",
});
let test = |expr: &str, is_ok: bool, exp: Value| { let obs = evaluator.eval_in_context(&expr, context.clone()); if !is_ok {
assert!(obs.is_err());
assert!(matches!(
obs.unwrap_err(),
EvaluationError::UndefinedIdentifier(_)
));
} else {
assert_eq!(obs.unwrap(), exp,);
}
};
#[test] fn test_add_multiple_transforms() { let evaluator = Evaluator::new()
.with_transform("sqrt", |v: &[Value]| { let num = v
.first()
.expect("There should be one argument!")
.as_f64()
.expect("Should be a valid number!");
Ok(value!(num.sqrt() as u64))
})
.with_transform("square", |v: &[Value]| { let num = v
.first()
.expect("There should be one argument!")
.as_f64()
.expect("Should be a valid number!");
Ok(value!((num as u64).pow(2)))
});
#[test] fn test_transform_with_argument() { let evaluator = Evaluator::new().with_transform("split", |args: &[Value]| { let s = args
.first()
.expect("Should be a first argument!")
.as_str()
.expect("Should be a string!"); let c = args
.get(1)
.expect("There should be a second argument!")
.as_str()
.expect("Should be a string"); let res: Vec<&str> = s.split_terminator(c).collect();
Ok(value!(res))
});
// we test equality in another test let expr = format!("{} == {}", l, r); let is_eq = evaluator
.eval_in_context(&expr, context.clone())
.unwrap()
.as_bool()
.unwrap();
#[test] fn test_lazy_eval_binary_op_and_or() { let evaluator = Evaluator::new(); // error is a missing transform let res = evaluator.eval("42 || 0|error");
assert!(res.is_ok());
assert_eq!(res.unwrap(), value!(42.0));
let res = evaluator.eval("false || 0|error");
assert!(res.is_err());
let res = evaluator.eval("42 && 0|error");
assert!(res.is_err());
let res = evaluator.eval("false && 0|error");
assert!(res.is_ok());
assert_eq!(res.unwrap(), value!(false));
}
#[test] fn test_lazy_eval_trinary_op() { let evaluator = Evaluator::new(); // error is a missing transform let res = evaluator.eval("true ? 42 : 0|error");
assert!(res.is_ok());
assert_eq!(res.unwrap(), value!(42.0));
let res = evaluator.eval("true ? 0|error : 42");
assert!(res.is_err());
let res = evaluator.eval("true ? 0|error : 42");
assert!(res.is_err());
let res = evaluator.eval("false ? 0|error : 42");
assert!(res.is_ok());
assert_eq!(res.unwrap(), value!(42.0));
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.