#![forbid(unsafe_code)]
pub mod clock;
pub mod datetime;
pub mod errors;
pub mod evaluator;
pub mod parser;
pub use errors::{Error, Span};
use clock::{Environment, RealEnvironment};
pub type Result<T> = std::result::Result<T, Error>;
pub struct EvalConfig<'a> {
pub max_depth: Option<usize>,
pub time_limit_ms: Option<u64>,
pub memory_limit_bytes: Option<usize>,
pub environment: &'a dyn Environment,
}
static DEFAULT_ENV: RealEnvironment = RealEnvironment::new_const();
impl Default for EvalConfig<'_> {
fn default() -> Self {
Self {
max_depth: Some(1000),
time_limit_ms: Some(5000),
memory_limit_bytes: None,
environment: &DEFAULT_ENV,
}
}
}
impl<'a> EvalConfig<'a> {
pub fn with_environment(env: &'a dyn Environment) -> Self {
Self {
environment: env,
..Default::default()
}
}
}
pub struct Seuil {
ast: parser::ast::Ast,
chain_ast: Option<parser::ast::Ast>,
}
impl Seuil {
pub fn compile(expr: &str) -> Result<Seuil> {
let ast = parser::parse(expr)?;
Ok(Seuil {
ast,
chain_ast: None,
})
}
pub fn evaluate(&self, input: &serde_json::Value) -> Result<serde_json::Value> {
self.evaluate_with_config(input, &EvalConfig::default())
}
pub fn evaluate_str(&self, input: &str) -> Result<serde_json::Value> {
let json: serde_json::Value =
serde_json::from_str(input).map_err(|e| Error::InvalidJsonInput(e.to_string()))?;
self.evaluate(&json)
}
pub fn evaluate_empty(&self) -> Result<serde_json::Value> {
self.evaluate(&serde_json::Value::Null)
}
pub fn evaluate_with_config(
&self,
input: &serde_json::Value,
config: &EvalConfig,
) -> Result<serde_json::Value> {
self.evaluate_with_config_and_bindings(input, config, None)
}
pub fn evaluate_with_config_and_bindings(
&self,
input: &serde_json::Value,
config: &EvalConfig,
bindings: Option<&serde_json::Map<String, serde_json::Value>>,
) -> Result<serde_json::Value> {
use bumpalo::Bump;
use evaluator::engine::Evaluator;
use evaluator::value::Value;
let arena = Bump::new();
let input_val = Value::from_json(&arena, input);
let max_depth = config.max_depth.unwrap_or(1000);
let time_limit_ms = config.time_limit_ms;
let evaluator = Evaluator::new(
&arena,
config.environment,
self.chain_ast.clone(),
max_depth,
time_limit_ms,
);
evaluator.bind_natives();
evaluator.bind("$", input_val);
if let Some(bindings) = bindings {
for (key, value) in bindings {
let val = Value::from_json(&arena, value);
let key_str = bumpalo::collections::String::from_str_in(key, &arena);
evaluator.bind(key_str.into_bump_str(), val);
}
}
let result = evaluator.evaluate(&self.ast, input_val)?;
Ok(value_to_json(result))
}
}
fn value_to_json(val: &evaluator::value::Value<'_>) -> serde_json::Value {
use evaluator::value::Value;
match val {
Value::Undefined => serde_json::Value::Null,
Value::Null => serde_json::Value::Null,
Value::Bool(b) => serde_json::Value::Bool(*b),
Value::Number(n) => serde_json::json!(*n),
Value::String(ref s) => serde_json::Value::String(s.as_str().to_string()),
Value::Array(ref a, _) => {
let arr: Vec<serde_json::Value> = a.iter().map(|v| value_to_json(v)).collect();
serde_json::Value::Array(arr)
}
Value::Object(ref o) => {
let mut map = serde_json::Map::new();
for (k, v) in o.iter() {
map.insert(k.as_str().to_string(), value_to_json(v));
}
serde_json::Value::Object(map)
}
Value::Range(ref r) => {
let arr: Vec<serde_json::Value> = (r.start()..=r.end())
.map(|i| serde_json::json!(i as f64))
.collect();
serde_json::Value::Array(arr)
}
_ => serde_json::Value::Null,
}
}
#[cfg(test)]
mod tests {
use super::*;
use clock::MockEnvironment;
#[test]
fn default_config() {
let config = EvalConfig::default();
assert_eq!(config.max_depth, Some(1000));
assert_eq!(config.time_limit_ms, Some(5000));
assert!(config.memory_limit_bytes.is_none());
}
#[test]
fn config_with_mock_env() {
let env = MockEnvironment::new(42);
let config = EvalConfig::with_environment(&env);
assert_eq!(config.environment.now_millis(), 1_000_000_000_000);
}
#[test]
fn test_public_api() {
let s = Seuil::compile("name").unwrap();
let result = s.evaluate(&serde_json::json!({"name": "Alice"})).unwrap();
assert_eq!(result, serde_json::json!("Alice"));
}
#[test]
fn test_evaluate_empty() {
let s = Seuil::compile("1 + 2").unwrap();
let result = s.evaluate_empty().unwrap();
assert_eq!(result, serde_json::json!(3.0));
}
#[test]
fn test_evaluate_str() {
let s = Seuil::compile("age * 2").unwrap();
let result = s.evaluate_str(r#"{"age": 21}"#).unwrap();
assert_eq!(result, serde_json::json!(42.0));
}
#[test]
fn test_compile_error() {
let result = Seuil::compile("(((");
assert!(result.is_err());
}
#[test]
fn test_evaluate_with_config() {
let env = MockEnvironment::new(42);
let config = EvalConfig::with_environment(&env);
let s = Seuil::compile("name").unwrap();
let result = s
.evaluate_with_config(&serde_json::json!({"name": "Bob"}), &config)
.unwrap();
assert_eq!(result, serde_json::json!("Bob"));
}
#[test]
fn test_evaluate_array_expression() {
let s = Seuil::compile("[1, 2, 3]").unwrap();
let result = s.evaluate_empty().unwrap();
assert_eq!(result, serde_json::json!([1.0, 2.0, 3.0]));
}
#[test]
fn test_evaluate_object_expression() {
let s = Seuil::compile(r#"{"a": 1, "b": 2}"#).unwrap();
let result = s.evaluate_empty().unwrap();
let r = result.as_object().unwrap();
assert_eq!(r.get("a"), Some(&serde_json::json!(1.0)));
assert_eq!(r.get("b"), Some(&serde_json::json!(2.0)));
}
#[test]
fn test_map() {
let s = Seuil::compile("$map([1,2,3], function($v){$v*2})").unwrap();
let result = s.evaluate_empty().unwrap();
assert_eq!(result, serde_json::json!([2.0, 4.0, 6.0]));
}
#[test]
fn test_filter() {
let s = Seuil::compile("$filter([1,2,3,4], function($v){$v > 2})").unwrap();
let result = s.evaluate_empty().unwrap();
assert_eq!(result, serde_json::json!([3.0, 4.0]));
}
#[test]
fn test_reduce() {
let s = Seuil::compile("$reduce([1,2,3,4,5], function($prev,$curr){$prev+$curr})").unwrap();
let result = s.evaluate_empty().unwrap();
assert_eq!(result, serde_json::json!(15.0));
}
#[test]
fn test_reduce_with_init() {
let s = Seuil::compile("$reduce([1,2,3], function($prev,$curr){$prev+$curr}, 10)").unwrap();
let result = s.evaluate_empty().unwrap();
assert_eq!(result, serde_json::json!(16.0));
}
#[test]
fn test_single() {
let s = Seuil::compile("$single([1,2,3,4], function($v){$v = 3})").unwrap();
let result = s.evaluate_empty().unwrap();
assert_eq!(result, serde_json::json!(3.0));
}
#[test]
fn test_single_no_match() {
let s = Seuil::compile("$single([1,2,3], function($v){$v > 10})").unwrap();
let result = s.evaluate_empty();
assert!(result.is_err());
}
#[test]
fn test_single_multiple_matches() {
let s = Seuil::compile("$single([1,2,3], function($v){$v > 1})").unwrap();
let result = s.evaluate_empty();
assert!(result.is_err());
}
#[test]
fn test_map_with_index() {
let s = Seuil::compile("$map([10,20,30], function($v, $i){$v + $i})").unwrap();
let result = s.evaluate_empty().unwrap();
assert_eq!(result, serde_json::json!([10.0, 21.0, 32.0]));
}
#[test]
fn test_each() {
let s = Seuil::compile(r#"$each({"a":1,"b":2}, function($v, $k){$v})"#).unwrap();
let result = s.evaluate_empty().unwrap();
assert!(result.is_array());
let arr = result.as_array().unwrap();
assert_eq!(arr.len(), 2);
let mut values: Vec<f64> = arr.iter().map(|v| v.as_f64().unwrap()).collect();
values.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert_eq!(values, vec![1.0, 2.0]);
}
#[test]
fn test_sift() {
let s = Seuil::compile(r#"$sift({"a":1,"b":2,"c":3}, function($v){$v > 1})"#).unwrap();
let result = s.evaluate_empty().unwrap();
let obj = result.as_object().unwrap();
assert!(!obj.contains_key("a"));
assert!(obj.contains_key("b"));
assert!(obj.contains_key("c"));
}
#[test]
fn test_nested_hof() {
let s =
Seuil::compile("$reduce($map([1,2,3], function($v){$v*$v}), function($a,$b){$a+$b})")
.unwrap();
let result = s.evaluate_empty().unwrap();
assert_eq!(result, serde_json::json!(14.0));
}
#[test]
fn test_value_to_json_roundtrip() {
use bumpalo::Bump;
use evaluator::value::Value;
let arena = Bump::new();
let val = Value::from_json(
&arena,
&serde_json::json!({"name": "test", "nums": [1, 2, 3]}),
);
let json = value_to_json(val);
assert_eq!(json["name"], "test");
assert_eq!(json["nums"], serde_json::json!([1.0, 2.0, 3.0]));
}
#[test]
fn test_nested_native_fn_calls() {
let s = Seuil::compile(
r#"$map(["aa bb", "cc dd"], function($i){ $match($i, /^(\w+\s\w+)/) }).match"#,
)
.unwrap();
let r = s.evaluate_empty().unwrap();
assert_eq!(r, serde_json::json!(["aa bb", "cc dd"]));
}
}