1317 lines
41 KiB
Rust
1317 lines
41 KiB
Rust
//! # Expression Evaluator
|
|
//!
|
|
//! Walks the AST and produces a `JsonValue` result.
|
|
|
|
use super::ast::{BinaryOp, Expr, UnaryOp};
|
|
use regex::Regex;
|
|
use serde_json::{json, Value as JsonValue};
|
|
use thiserror::Error;
|
|
|
|
/// Result type for evaluation operations.
|
|
pub type EvalResult<T> = Result<T, EvalError>;
|
|
|
|
/// Errors that can occur during expression evaluation.
|
|
#[derive(Debug, Error)]
|
|
pub enum EvalError {
|
|
#[error("Variable not found: {0}")]
|
|
VariableNotFound(String),
|
|
|
|
#[error("Type error: {0}")]
|
|
TypeError(String),
|
|
|
|
#[error("Division by zero")]
|
|
DivisionByZero,
|
|
|
|
#[error("Index out of bounds: {0}")]
|
|
IndexOutOfBounds(String),
|
|
|
|
#[error("Unknown function: {0}")]
|
|
UnknownFunction(String),
|
|
|
|
#[error("Wrong number of arguments for {0}: expected {1}, got {2}")]
|
|
WrongArgCount(String, String, usize),
|
|
|
|
#[error("Parse error: {0}")]
|
|
ParseError(String),
|
|
|
|
#[error("Regex error: {0}")]
|
|
RegexError(String),
|
|
}
|
|
|
|
/// Trait for resolving variables and workflow-specific functions from
|
|
/// the execution context.
|
|
pub trait EvalContext {
|
|
/// Resolve a top-level variable name to its JSON value.
|
|
fn resolve_variable(&self, name: &str) -> EvalResult<JsonValue>;
|
|
|
|
/// Try to call a workflow-specific function (e.g., `result()`, `succeeded()`).
|
|
/// Return `Ok(Some(value))` if handled, `Ok(None)` if not recognized.
|
|
fn call_workflow_function(
|
|
&self,
|
|
name: &str,
|
|
args: &[JsonValue],
|
|
) -> EvalResult<Option<JsonValue>>;
|
|
}
|
|
|
|
/// Evaluate an AST expression against the given context.
|
|
pub fn eval(expr: &Expr, ctx: &dyn EvalContext) -> EvalResult<JsonValue> {
|
|
match expr {
|
|
Expr::Literal(v) => Ok(v.clone()),
|
|
|
|
Expr::Array(elements) => {
|
|
let mut arr = Vec::with_capacity(elements.len());
|
|
for elem in elements {
|
|
arr.push(eval(elem, ctx)?);
|
|
}
|
|
Ok(JsonValue::Array(arr))
|
|
}
|
|
|
|
Expr::Ident(name) => ctx.resolve_variable(name),
|
|
|
|
Expr::BinaryOp { op, left, right } => {
|
|
// Short-circuit for `and` / `or`
|
|
if *op == BinaryOp::And {
|
|
let lv = eval(left, ctx)?;
|
|
if !is_truthy(&lv) {
|
|
return Ok(json!(false));
|
|
}
|
|
let rv = eval(right, ctx)?;
|
|
return Ok(json!(is_truthy(&rv)));
|
|
}
|
|
if *op == BinaryOp::Or {
|
|
let lv = eval(left, ctx)?;
|
|
if is_truthy(&lv) {
|
|
return Ok(json!(true));
|
|
}
|
|
let rv = eval(right, ctx)?;
|
|
return Ok(json!(is_truthy(&rv)));
|
|
}
|
|
|
|
let lv = eval(left, ctx)?;
|
|
let rv = eval(right, ctx)?;
|
|
eval_binary_op(*op, &lv, &rv)
|
|
}
|
|
|
|
Expr::UnaryOp { op, operand } => {
|
|
let v = eval(operand, ctx)?;
|
|
eval_unary_op(*op, &v)
|
|
}
|
|
|
|
Expr::DotAccess { object, field } => {
|
|
let obj = eval(object, ctx)?;
|
|
dot_access(&obj, field)
|
|
}
|
|
|
|
Expr::IndexAccess { object, index } => {
|
|
let obj = eval(object, ctx)?;
|
|
let idx = eval(index, ctx)?;
|
|
index_access(&obj, &idx)
|
|
}
|
|
|
|
Expr::FunctionCall { name, args } => {
|
|
// First, try workflow-specific functions (result(), succeeded(), etc.)
|
|
// We evaluate args lazily for workflow fns that take 0 args.
|
|
let evaluated_args: Vec<JsonValue> = args
|
|
.iter()
|
|
.map(|a| eval(a, ctx))
|
|
.collect::<EvalResult<Vec<_>>>()?;
|
|
|
|
if let Some(val) = ctx.call_workflow_function(name, &evaluated_args)? {
|
|
return Ok(val);
|
|
}
|
|
|
|
// Built-in functions
|
|
eval_builtin_function(name, &evaluated_args)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Truthiness
|
|
// ---------------------------------------------------------------
|
|
|
|
/// Determine if a JSON value is "truthy" (Python-like semantics).
|
|
pub fn is_truthy(v: &JsonValue) -> bool {
|
|
match v {
|
|
JsonValue::Null => false,
|
|
JsonValue::Bool(b) => *b,
|
|
JsonValue::Number(n) => {
|
|
if let Some(i) = n.as_i64() {
|
|
i != 0
|
|
} else if let Some(f) = n.as_f64() {
|
|
f != 0.0
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
JsonValue::String(s) => !s.is_empty(),
|
|
JsonValue::Array(a) => !a.is_empty(),
|
|
JsonValue::Object(o) => !o.is_empty(),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Binary operations
|
|
// ---------------------------------------------------------------
|
|
|
|
fn eval_binary_op(op: BinaryOp, left: &JsonValue, right: &JsonValue) -> EvalResult<JsonValue> {
|
|
match op {
|
|
// Arithmetic
|
|
BinaryOp::Add => eval_add(left, right),
|
|
BinaryOp::Sub => eval_arithmetic(left, right, |a, b| a - b, |a, b| a - b, "-"),
|
|
BinaryOp::Mul => eval_arithmetic(left, right, |a, b| a * b, |a, b| a * b, "*"),
|
|
BinaryOp::Div => eval_div(left, right),
|
|
BinaryOp::Mod => eval_mod(left, right),
|
|
|
|
// Comparison
|
|
BinaryOp::Eq => Ok(json!(json_eq(left, right))),
|
|
BinaryOp::Ne => Ok(json!(!json_eq(left, right))),
|
|
BinaryOp::Lt => eval_ordering(left, right, |o| o == std::cmp::Ordering::Less),
|
|
BinaryOp::Gt => eval_ordering(left, right, |o| o == std::cmp::Ordering::Greater),
|
|
BinaryOp::Le => eval_ordering(left, right, |o| o != std::cmp::Ordering::Greater),
|
|
BinaryOp::Ge => eval_ordering(left, right, |o| o != std::cmp::Ordering::Less),
|
|
|
|
// Membership
|
|
BinaryOp::In => eval_in(left, right),
|
|
|
|
// And/Or handled in eval() with short-circuit
|
|
BinaryOp::And | BinaryOp::Or => unreachable!(),
|
|
}
|
|
}
|
|
|
|
fn eval_add(left: &JsonValue, right: &JsonValue) -> EvalResult<JsonValue> {
|
|
// String concatenation
|
|
if left.is_string() && right.is_string() {
|
|
let l = left.as_str().unwrap();
|
|
let r = right.as_str().unwrap();
|
|
return Ok(json!(format!("{}{}", l, r)));
|
|
}
|
|
|
|
// Array concatenation
|
|
if left.is_array() && right.is_array() {
|
|
let mut result = left.as_array().unwrap().clone();
|
|
result.extend(right.as_array().unwrap().iter().cloned());
|
|
return Ok(JsonValue::Array(result));
|
|
}
|
|
|
|
// Numeric addition
|
|
eval_arithmetic(left, right, |a, b| a + b, |a, b| a + b, "+")
|
|
}
|
|
|
|
fn eval_arithmetic(
|
|
left: &JsonValue,
|
|
right: &JsonValue,
|
|
int_op: impl Fn(i64, i64) -> i64,
|
|
float_op: impl Fn(f64, f64) -> f64,
|
|
op_name: &str,
|
|
) -> EvalResult<JsonValue> {
|
|
match (as_numeric(left), as_numeric(right)) {
|
|
(Some(NumericValue::Int(a)), Some(NumericValue::Int(b))) => Ok(json!(int_op(a, b))),
|
|
(Some(a), Some(b)) => Ok(json!(float_op(a.as_f64(), b.as_f64()))),
|
|
_ => Err(EvalError::TypeError(format!(
|
|
"Cannot apply '{}' to {} and {}",
|
|
op_name,
|
|
type_name(left),
|
|
type_name(right)
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn eval_div(left: &JsonValue, right: &JsonValue) -> EvalResult<JsonValue> {
|
|
match (as_numeric(left), as_numeric(right)) {
|
|
(Some(_), Some(b)) if b.as_f64() == 0.0 => Err(EvalError::DivisionByZero),
|
|
(Some(NumericValue::Int(a)), Some(NumericValue::Int(b))) => {
|
|
// Integer division stays integer if divisible
|
|
if a % b == 0 {
|
|
Ok(json!(a / b))
|
|
} else {
|
|
Ok(json!(a as f64 / b as f64))
|
|
}
|
|
}
|
|
(Some(a), Some(b)) => Ok(json!(a.as_f64() / b.as_f64())),
|
|
_ => Err(EvalError::TypeError(format!(
|
|
"Cannot apply '/' to {} and {}",
|
|
type_name(left),
|
|
type_name(right)
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn eval_mod(left: &JsonValue, right: &JsonValue) -> EvalResult<JsonValue> {
|
|
match (as_numeric(left), as_numeric(right)) {
|
|
(Some(_), Some(b)) if b.as_f64() == 0.0 => Err(EvalError::DivisionByZero),
|
|
(Some(NumericValue::Int(a)), Some(NumericValue::Int(b))) => Ok(json!(a % b)),
|
|
(Some(a), Some(b)) => Ok(json!(a.as_f64() % b.as_f64())),
|
|
_ => Err(EvalError::TypeError(format!(
|
|
"Cannot apply '%' to {} and {}",
|
|
type_name(left),
|
|
type_name(right)
|
|
))),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Comparison helpers
|
|
// ---------------------------------------------------------------
|
|
|
|
/// Deep equality that allows int/float cross-comparison.
|
|
fn json_eq(a: &JsonValue, b: &JsonValue) -> bool {
|
|
match (a, b) {
|
|
(JsonValue::Null, JsonValue::Null) => true,
|
|
(JsonValue::Bool(a), JsonValue::Bool(b)) => a == b,
|
|
(JsonValue::Number(_), JsonValue::Number(_)) => {
|
|
// Allow int/float comparison
|
|
match (as_numeric(a), as_numeric(b)) {
|
|
(Some(a), Some(b)) => a.as_f64() == b.as_f64(),
|
|
_ => false,
|
|
}
|
|
}
|
|
(JsonValue::String(a), JsonValue::String(b)) => a == b,
|
|
(JsonValue::Array(a), JsonValue::Array(b)) => {
|
|
if a.len() != b.len() {
|
|
return false;
|
|
}
|
|
a.iter().zip(b.iter()).all(|(x, y)| json_eq(x, y))
|
|
}
|
|
(JsonValue::Object(a), JsonValue::Object(b)) => {
|
|
if a.len() != b.len() {
|
|
return false;
|
|
}
|
|
a.iter()
|
|
.all(|(k, v)| b.get(k).map_or(false, |bv| json_eq(v, bv)))
|
|
}
|
|
// Different types (other than number cross-compare) are never equal
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn eval_ordering(
|
|
left: &JsonValue,
|
|
right: &JsonValue,
|
|
predicate: impl Fn(std::cmp::Ordering) -> bool,
|
|
) -> EvalResult<JsonValue> {
|
|
// Number comparison (int/float cross-allowed)
|
|
if let (Some(a), Some(b)) = (as_numeric(left), as_numeric(right)) {
|
|
let af = a.as_f64();
|
|
let bf = b.as_f64();
|
|
let ord = af.partial_cmp(&bf).unwrap_or(std::cmp::Ordering::Equal);
|
|
return Ok(json!(predicate(ord)));
|
|
}
|
|
|
|
// String comparison
|
|
if let (Some(a), Some(b)) = (left.as_str(), right.as_str()) {
|
|
return Ok(json!(predicate(a.cmp(b))));
|
|
}
|
|
|
|
// List comparison (lexicographic)
|
|
if let (Some(a), Some(b)) = (left.as_array(), right.as_array()) {
|
|
let ord = compare_arrays(a, b)?;
|
|
return Ok(json!(predicate(ord)));
|
|
}
|
|
|
|
Err(EvalError::TypeError(format!(
|
|
"Cannot compare {} and {} with ordering operators",
|
|
type_name(left),
|
|
type_name(right)
|
|
)))
|
|
}
|
|
|
|
fn compare_arrays(a: &[JsonValue], b: &[JsonValue]) -> EvalResult<std::cmp::Ordering> {
|
|
for (x, y) in a.iter().zip(b.iter()) {
|
|
if let (Some(xn), Some(yn)) = (as_numeric(x), as_numeric(y)) {
|
|
let ord = xn
|
|
.as_f64()
|
|
.partial_cmp(&yn.as_f64())
|
|
.unwrap_or(std::cmp::Ordering::Equal);
|
|
if ord != std::cmp::Ordering::Equal {
|
|
return Ok(ord);
|
|
}
|
|
} else if let (Some(xs), Some(ys)) = (x.as_str(), y.as_str()) {
|
|
let ord = xs.cmp(ys);
|
|
if ord != std::cmp::Ordering::Equal {
|
|
return Ok(ord);
|
|
}
|
|
} else {
|
|
return Err(EvalError::TypeError(
|
|
"Cannot compare heterogeneous array elements for ordering".to_string(),
|
|
));
|
|
}
|
|
}
|
|
Ok(a.len().cmp(&b.len()))
|
|
}
|
|
|
|
fn eval_in(needle: &JsonValue, haystack: &JsonValue) -> EvalResult<JsonValue> {
|
|
match haystack {
|
|
JsonValue::Array(arr) => Ok(json!(arr.iter().any(|item| json_eq(needle, item)))),
|
|
JsonValue::Object(obj) => {
|
|
if let Some(key) = needle.as_str() {
|
|
Ok(json!(obj.contains_key(key)))
|
|
} else {
|
|
Err(EvalError::TypeError(
|
|
"Only string keys can be tested for membership in objects".to_string(),
|
|
))
|
|
}
|
|
}
|
|
JsonValue::String(s) => {
|
|
if let Some(sub) = needle.as_str() {
|
|
Ok(json!(s.contains(sub)))
|
|
} else {
|
|
Err(EvalError::TypeError(
|
|
"Only strings can be tested for substring membership".to_string(),
|
|
))
|
|
}
|
|
}
|
|
_ => Err(EvalError::TypeError(format!(
|
|
"'in' requires array, object, or string on right side, got {}",
|
|
type_name(haystack)
|
|
))),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Unary operations
|
|
// ---------------------------------------------------------------
|
|
|
|
fn eval_unary_op(op: UnaryOp, val: &JsonValue) -> EvalResult<JsonValue> {
|
|
match op {
|
|
UnaryOp::Neg => {
|
|
if let Some(n) = as_numeric(val) {
|
|
match n {
|
|
NumericValue::Int(i) => Ok(json!(-i)),
|
|
NumericValue::Float(f) => Ok(json!(-f)),
|
|
}
|
|
} else {
|
|
Err(EvalError::TypeError(format!(
|
|
"Cannot negate {}",
|
|
type_name(val)
|
|
)))
|
|
}
|
|
}
|
|
UnaryOp::Not => Ok(json!(!is_truthy(val))),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Property / index access
|
|
// ---------------------------------------------------------------
|
|
|
|
fn dot_access(obj: &JsonValue, field: &str) -> EvalResult<JsonValue> {
|
|
match obj {
|
|
JsonValue::Object(map) => map
|
|
.get(field)
|
|
.cloned()
|
|
.ok_or_else(|| EvalError::VariableNotFound(format!("field '{}'", field))),
|
|
_ => Err(EvalError::TypeError(format!(
|
|
"Cannot access property '{}' on {}",
|
|
field,
|
|
type_name(obj)
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn index_access(obj: &JsonValue, index: &JsonValue) -> EvalResult<JsonValue> {
|
|
match obj {
|
|
JsonValue::Array(arr) => {
|
|
if let Some(i) = index.as_i64() {
|
|
let i = if i < 0 {
|
|
// Negative indexing
|
|
(arr.len() as i64 + i) as usize
|
|
} else {
|
|
i as usize
|
|
};
|
|
arr.get(i)
|
|
.cloned()
|
|
.ok_or_else(|| EvalError::IndexOutOfBounds(format!("{}", i)))
|
|
} else {
|
|
Err(EvalError::TypeError(
|
|
"Array index must be an integer".to_string(),
|
|
))
|
|
}
|
|
}
|
|
JsonValue::Object(map) => {
|
|
if let Some(key) = index.as_str() {
|
|
map.get(key)
|
|
.cloned()
|
|
.ok_or_else(|| EvalError::VariableNotFound(format!("key '{}'", key)))
|
|
} else {
|
|
Err(EvalError::TypeError(
|
|
"Object key must be a string".to_string(),
|
|
))
|
|
}
|
|
}
|
|
JsonValue::String(s) => {
|
|
if let Some(i) = index.as_i64() {
|
|
let chars: Vec<char> = s.chars().collect();
|
|
let i = if i < 0 {
|
|
(chars.len() as i64 + i) as usize
|
|
} else {
|
|
i as usize
|
|
};
|
|
chars
|
|
.get(i)
|
|
.map(|c| json!(c.to_string()))
|
|
.ok_or_else(|| EvalError::IndexOutOfBounds(format!("{}", i)))
|
|
} else {
|
|
Err(EvalError::TypeError(
|
|
"String index must be an integer".to_string(),
|
|
))
|
|
}
|
|
}
|
|
_ => Err(EvalError::TypeError(format!(
|
|
"Cannot index into {}",
|
|
type_name(obj)
|
|
))),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Built-in functions
|
|
// ---------------------------------------------------------------
|
|
|
|
fn eval_builtin_function(name: &str, args: &[JsonValue]) -> EvalResult<JsonValue> {
|
|
match name {
|
|
// -- Type conversion --
|
|
"string" => {
|
|
expect_args(name, args, 1)?;
|
|
Ok(json!(value_to_string(&args[0])))
|
|
}
|
|
"number" => {
|
|
expect_args(name, args, 1)?;
|
|
to_number(&args[0])
|
|
}
|
|
"int" => {
|
|
expect_args(name, args, 1)?;
|
|
to_int(&args[0])
|
|
}
|
|
"bool" => {
|
|
expect_args(name, args, 1)?;
|
|
Ok(json!(is_truthy(&args[0])))
|
|
}
|
|
|
|
// -- Introspection --
|
|
"type_of" => {
|
|
expect_args(name, args, 1)?;
|
|
Ok(json!(type_name(&args[0])))
|
|
}
|
|
"length" => {
|
|
expect_args(name, args, 1)?;
|
|
fn_length(&args[0])
|
|
}
|
|
"keys" => {
|
|
expect_args(name, args, 1)?;
|
|
fn_keys(&args[0])
|
|
}
|
|
"values" => {
|
|
expect_args(name, args, 1)?;
|
|
fn_values(&args[0])
|
|
}
|
|
|
|
// -- Math --
|
|
"abs" => {
|
|
expect_args(name, args, 1)?;
|
|
fn_abs(&args[0])
|
|
}
|
|
"floor" => {
|
|
expect_args(name, args, 1)?;
|
|
fn_floor(&args[0])
|
|
}
|
|
"ceil" => {
|
|
expect_args(name, args, 1)?;
|
|
fn_ceil(&args[0])
|
|
}
|
|
"round" => {
|
|
expect_args(name, args, 1)?;
|
|
fn_round(&args[0])
|
|
}
|
|
"min" => {
|
|
expect_args(name, args, 2)?;
|
|
fn_min(&args[0], &args[1])
|
|
}
|
|
"max" => {
|
|
expect_args(name, args, 2)?;
|
|
fn_max(&args[0], &args[1])
|
|
}
|
|
"sum" => {
|
|
expect_args(name, args, 1)?;
|
|
fn_sum(&args[0])
|
|
}
|
|
|
|
// -- String --
|
|
"lower" => {
|
|
expect_args(name, args, 1)?;
|
|
fn_lower(&args[0])
|
|
}
|
|
"upper" => {
|
|
expect_args(name, args, 1)?;
|
|
fn_upper(&args[0])
|
|
}
|
|
"trim" => {
|
|
expect_args(name, args, 1)?;
|
|
fn_trim(&args[0])
|
|
}
|
|
"split" => {
|
|
expect_args(name, args, 2)?;
|
|
fn_split(&args[0], &args[1])
|
|
}
|
|
"join" => {
|
|
expect_args(name, args, 2)?;
|
|
fn_join(&args[0], &args[1])
|
|
}
|
|
"replace" => {
|
|
expect_args(name, args, 3)?;
|
|
fn_replace(&args[0], &args[1], &args[2])
|
|
}
|
|
"starts_with" => {
|
|
expect_args(name, args, 2)?;
|
|
fn_starts_with(&args[0], &args[1])
|
|
}
|
|
"ends_with" => {
|
|
expect_args(name, args, 2)?;
|
|
fn_ends_with(&args[0], &args[1])
|
|
}
|
|
"match" => {
|
|
expect_args(name, args, 2)?;
|
|
fn_match(&args[0], &args[1])
|
|
}
|
|
|
|
// -- Collections --
|
|
"contains" => {
|
|
expect_args(name, args, 2)?;
|
|
eval_in(&args[1], &args[0])
|
|
}
|
|
"reversed" => {
|
|
expect_args(name, args, 1)?;
|
|
fn_reversed(&args[0])
|
|
}
|
|
"sort" => {
|
|
expect_args(name, args, 1)?;
|
|
fn_sort(&args[0])
|
|
}
|
|
"unique" => {
|
|
expect_args(name, args, 1)?;
|
|
fn_unique(&args[0])
|
|
}
|
|
"flat" => {
|
|
expect_args(name, args, 1)?;
|
|
fn_flat(&args[0])
|
|
}
|
|
"zip" => {
|
|
expect_args(name, args, 2)?;
|
|
fn_zip(&args[0], &args[1])
|
|
}
|
|
"range" => {
|
|
if args.len() == 1 {
|
|
fn_range_1(&args[0])
|
|
} else if args.len() == 2 {
|
|
fn_range_2(&args[0], &args[1])
|
|
} else {
|
|
Err(EvalError::WrongArgCount(
|
|
name.to_string(),
|
|
"1 or 2".to_string(),
|
|
args.len(),
|
|
))
|
|
}
|
|
}
|
|
"slice" => {
|
|
if args.len() == 2 {
|
|
fn_slice(&args[0], &args[1], &JsonValue::Null)
|
|
} else if args.len() == 3 {
|
|
fn_slice(&args[0], &args[1], &args[2])
|
|
} else {
|
|
Err(EvalError::WrongArgCount(
|
|
name.to_string(),
|
|
"2 or 3".to_string(),
|
|
args.len(),
|
|
))
|
|
}
|
|
}
|
|
"index_of" => {
|
|
expect_args(name, args, 2)?;
|
|
fn_index_of(&args[0], &args[1])
|
|
}
|
|
"count" => {
|
|
expect_args(name, args, 2)?;
|
|
fn_count(&args[0], &args[1])
|
|
}
|
|
"merge" => {
|
|
expect_args(name, args, 2)?;
|
|
fn_merge(&args[0], &args[1])
|
|
}
|
|
"chunks" => {
|
|
expect_args(name, args, 2)?;
|
|
fn_chunks(&args[0], &args[1])
|
|
}
|
|
|
|
_ => Err(EvalError::UnknownFunction(name.to_string())),
|
|
}
|
|
}
|
|
|
|
fn expect_args(name: &str, args: &[JsonValue], expected: usize) -> EvalResult<()> {
|
|
if args.len() != expected {
|
|
Err(EvalError::WrongArgCount(
|
|
name.to_string(),
|
|
expected.to_string(),
|
|
args.len(),
|
|
))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Numeric helpers
|
|
// ---------------------------------------------------------------
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
enum NumericValue {
|
|
Int(i64),
|
|
Float(f64),
|
|
}
|
|
|
|
impl NumericValue {
|
|
fn as_f64(self) -> f64 {
|
|
match self {
|
|
NumericValue::Int(i) => i as f64,
|
|
NumericValue::Float(f) => f,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn as_numeric(v: &JsonValue) -> Option<NumericValue> {
|
|
if let Some(i) = v.as_i64() {
|
|
Some(NumericValue::Int(i))
|
|
} else if let Some(f) = v.as_f64() {
|
|
Some(NumericValue::Float(f))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn type_name(v: &JsonValue) -> &'static str {
|
|
match v {
|
|
JsonValue::Null => "null",
|
|
JsonValue::Bool(_) => "bool",
|
|
JsonValue::Number(_) => "number",
|
|
JsonValue::String(_) => "string",
|
|
JsonValue::Array(_) => "array",
|
|
JsonValue::Object(_) => "object",
|
|
}
|
|
}
|
|
|
|
fn value_to_string(v: &JsonValue) -> String {
|
|
match v {
|
|
JsonValue::String(s) => s.clone(),
|
|
JsonValue::Null => "null".to_string(),
|
|
JsonValue::Bool(b) => b.to_string(),
|
|
JsonValue::Number(n) => n.to_string(),
|
|
other => serde_json::to_string(other).unwrap_or_default(),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Type conversion functions
|
|
// ---------------------------------------------------------------
|
|
|
|
fn to_number(v: &JsonValue) -> EvalResult<JsonValue> {
|
|
match v {
|
|
JsonValue::Number(_) => Ok(v.clone()),
|
|
JsonValue::String(s) => {
|
|
if let Ok(f) = s.parse::<f64>() {
|
|
Ok(json!(f))
|
|
} else {
|
|
Err(EvalError::TypeError(format!(
|
|
"Cannot convert string '{}' to number",
|
|
s
|
|
)))
|
|
}
|
|
}
|
|
JsonValue::Bool(b) => Ok(json!(if *b { 1.0 } else { 0.0 })),
|
|
_ => Err(EvalError::TypeError(format!(
|
|
"Cannot convert {} to number",
|
|
type_name(v)
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn to_int(v: &JsonValue) -> EvalResult<JsonValue> {
|
|
match v {
|
|
JsonValue::Number(n) => {
|
|
if let Some(i) = n.as_i64() {
|
|
Ok(json!(i))
|
|
} else if let Some(f) = n.as_f64() {
|
|
Ok(json!(f as i64))
|
|
} else {
|
|
Err(EvalError::TypeError("Cannot convert number to int".to_string()))
|
|
}
|
|
}
|
|
JsonValue::String(s) => {
|
|
// Try integer first, then float truncation
|
|
if let Ok(i) = s.parse::<i64>() {
|
|
Ok(json!(i))
|
|
} else if let Ok(f) = s.parse::<f64>() {
|
|
Ok(json!(f as i64))
|
|
} else {
|
|
Err(EvalError::TypeError(format!(
|
|
"Cannot convert string '{}' to int",
|
|
s
|
|
)))
|
|
}
|
|
}
|
|
JsonValue::Bool(b) => Ok(json!(if *b { 1 } else { 0 })),
|
|
_ => Err(EvalError::TypeError(format!(
|
|
"Cannot convert {} to int",
|
|
type_name(v)
|
|
))),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Introspection functions
|
|
// ---------------------------------------------------------------
|
|
|
|
fn fn_length(v: &JsonValue) -> EvalResult<JsonValue> {
|
|
match v {
|
|
JsonValue::String(s) => Ok(json!(s.len())),
|
|
JsonValue::Array(a) => Ok(json!(a.len())),
|
|
JsonValue::Object(o) => Ok(json!(o.len())),
|
|
_ => Err(EvalError::TypeError(format!(
|
|
"length() requires string, array, or object, got {}",
|
|
type_name(v)
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn fn_keys(v: &JsonValue) -> EvalResult<JsonValue> {
|
|
match v {
|
|
JsonValue::Object(obj) => {
|
|
let keys: Vec<JsonValue> = obj.keys().map(|k| json!(k)).collect();
|
|
Ok(JsonValue::Array(keys))
|
|
}
|
|
_ => Err(EvalError::TypeError(format!(
|
|
"keys() requires object, got {}",
|
|
type_name(v)
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn fn_values(v: &JsonValue) -> EvalResult<JsonValue> {
|
|
match v {
|
|
JsonValue::Object(obj) => {
|
|
let values: Vec<JsonValue> = obj.values().cloned().collect();
|
|
Ok(JsonValue::Array(values))
|
|
}
|
|
_ => Err(EvalError::TypeError(format!(
|
|
"values() requires object, got {}",
|
|
type_name(v)
|
|
))),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Math functions
|
|
// ---------------------------------------------------------------
|
|
|
|
fn fn_abs(v: &JsonValue) -> EvalResult<JsonValue> {
|
|
match as_numeric(v) {
|
|
Some(NumericValue::Int(i)) => Ok(json!(i.abs())),
|
|
Some(NumericValue::Float(f)) => Ok(json!(f.abs())),
|
|
None => Err(EvalError::TypeError(format!(
|
|
"abs() requires number, got {}",
|
|
type_name(v)
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn fn_floor(v: &JsonValue) -> EvalResult<JsonValue> {
|
|
match as_numeric(v) {
|
|
Some(NumericValue::Int(i)) => Ok(json!(i)),
|
|
Some(NumericValue::Float(f)) => Ok(json!(f.floor() as i64)),
|
|
None => Err(EvalError::TypeError(format!(
|
|
"floor() requires number, got {}",
|
|
type_name(v)
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn fn_ceil(v: &JsonValue) -> EvalResult<JsonValue> {
|
|
match as_numeric(v) {
|
|
Some(NumericValue::Int(i)) => Ok(json!(i)),
|
|
Some(NumericValue::Float(f)) => Ok(json!(f.ceil() as i64)),
|
|
None => Err(EvalError::TypeError(format!(
|
|
"ceil() requires number, got {}",
|
|
type_name(v)
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn fn_round(v: &JsonValue) -> EvalResult<JsonValue> {
|
|
match as_numeric(v) {
|
|
Some(NumericValue::Int(i)) => Ok(json!(i)),
|
|
Some(NumericValue::Float(f)) => Ok(json!(f.round() as i64)),
|
|
None => Err(EvalError::TypeError(format!(
|
|
"round() requires number, got {}",
|
|
type_name(v)
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn fn_min(a: &JsonValue, b: &JsonValue) -> EvalResult<JsonValue> {
|
|
match (as_numeric(a), as_numeric(b)) {
|
|
(Some(NumericValue::Int(x)), Some(NumericValue::Int(y))) => Ok(json!(x.min(y))),
|
|
(Some(x), Some(y)) => Ok(json!(x.as_f64().min(y.as_f64()))),
|
|
_ => {
|
|
// String min
|
|
if let (Some(sa), Some(sb)) = (a.as_str(), b.as_str()) {
|
|
Ok(json!(sa.min(sb)))
|
|
} else {
|
|
Err(EvalError::TypeError(
|
|
"min() requires two numbers or two strings".to_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn fn_max(a: &JsonValue, b: &JsonValue) -> EvalResult<JsonValue> {
|
|
match (as_numeric(a), as_numeric(b)) {
|
|
(Some(NumericValue::Int(x)), Some(NumericValue::Int(y))) => Ok(json!(x.max(y))),
|
|
(Some(x), Some(y)) => Ok(json!(x.as_f64().max(y.as_f64()))),
|
|
_ => {
|
|
if let (Some(sa), Some(sb)) = (a.as_str(), b.as_str()) {
|
|
Ok(json!(sa.max(sb)))
|
|
} else {
|
|
Err(EvalError::TypeError(
|
|
"max() requires two numbers or two strings".to_string(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn fn_sum(v: &JsonValue) -> EvalResult<JsonValue> {
|
|
match v {
|
|
JsonValue::Array(arr) => {
|
|
let mut has_float = false;
|
|
let mut int_sum: i64 = 0;
|
|
let mut float_sum: f64 = 0.0;
|
|
|
|
for item in arr {
|
|
match as_numeric(item) {
|
|
Some(NumericValue::Int(i)) => {
|
|
int_sum += i;
|
|
float_sum += i as f64;
|
|
}
|
|
Some(NumericValue::Float(f)) => {
|
|
has_float = true;
|
|
float_sum += f;
|
|
}
|
|
None => {
|
|
return Err(EvalError::TypeError(format!(
|
|
"sum() requires array of numbers, found {}",
|
|
type_name(item)
|
|
)));
|
|
}
|
|
}
|
|
}
|
|
|
|
if has_float {
|
|
Ok(json!(float_sum))
|
|
} else {
|
|
Ok(json!(int_sum))
|
|
}
|
|
}
|
|
_ => Err(EvalError::TypeError(format!(
|
|
"sum() requires array, got {}",
|
|
type_name(v)
|
|
))),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// String functions
|
|
// ---------------------------------------------------------------
|
|
|
|
fn fn_lower(v: &JsonValue) -> EvalResult<JsonValue> {
|
|
require_string("lower", v).map(|s| json!(s.to_lowercase()))
|
|
}
|
|
|
|
fn fn_upper(v: &JsonValue) -> EvalResult<JsonValue> {
|
|
require_string("upper", v).map(|s| json!(s.to_uppercase()))
|
|
}
|
|
|
|
fn fn_trim(v: &JsonValue) -> EvalResult<JsonValue> {
|
|
require_string("trim", v).map(|s| json!(s.trim()))
|
|
}
|
|
|
|
fn fn_split(s: &JsonValue, sep: &JsonValue) -> EvalResult<JsonValue> {
|
|
let s = require_string("split", s)?;
|
|
let sep = require_string("split", sep)?;
|
|
let parts: Vec<JsonValue> = s.split(sep).map(|p| json!(p)).collect();
|
|
Ok(JsonValue::Array(parts))
|
|
}
|
|
|
|
fn fn_join(arr: &JsonValue, sep: &JsonValue) -> EvalResult<JsonValue> {
|
|
let arr = arr.as_array().ok_or_else(|| {
|
|
EvalError::TypeError(format!(
|
|
"join() first argument must be array, got {}",
|
|
type_name(arr)
|
|
))
|
|
})?;
|
|
let sep = require_string("join", sep)?;
|
|
let strings: Result<Vec<String>, _> = arr.iter().map(|v| {
|
|
Ok(value_to_string(v))
|
|
}).collect();
|
|
Ok(json!(strings?.join(sep)))
|
|
}
|
|
|
|
fn fn_replace(s: &JsonValue, old: &JsonValue, new: &JsonValue) -> EvalResult<JsonValue> {
|
|
let s = require_string("replace", s)?;
|
|
let old = require_string("replace", old)?;
|
|
let new_s = require_string("replace", new)?;
|
|
Ok(json!(s.replace(old, new_s)))
|
|
}
|
|
|
|
fn fn_starts_with(s: &JsonValue, prefix: &JsonValue) -> EvalResult<JsonValue> {
|
|
let s = require_string("starts_with", s)?;
|
|
let prefix = require_string("starts_with", prefix)?;
|
|
Ok(json!(s.starts_with(prefix)))
|
|
}
|
|
|
|
fn fn_ends_with(s: &JsonValue, suffix: &JsonValue) -> EvalResult<JsonValue> {
|
|
let s = require_string("ends_with", s)?;
|
|
let suffix = require_string("ends_with", suffix)?;
|
|
Ok(json!(s.ends_with(suffix)))
|
|
}
|
|
|
|
fn fn_match(pattern: &JsonValue, s: &JsonValue) -> EvalResult<JsonValue> {
|
|
let pattern = require_string("match", pattern)?;
|
|
let s = require_string("match", s)?;
|
|
let re = Regex::new(pattern)
|
|
.map_err(|e| EvalError::RegexError(format!("{}", e)))?;
|
|
Ok(json!(re.is_match(s)))
|
|
}
|
|
|
|
fn require_string<'a>(func: &str, v: &'a JsonValue) -> EvalResult<&'a str> {
|
|
v.as_str().ok_or_else(|| {
|
|
EvalError::TypeError(format!(
|
|
"{}() requires string argument, got {}",
|
|
func,
|
|
type_name(v)
|
|
))
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Collection functions
|
|
// ---------------------------------------------------------------
|
|
|
|
fn fn_reversed(v: &JsonValue) -> EvalResult<JsonValue> {
|
|
match v {
|
|
JsonValue::Array(arr) => {
|
|
let mut rev = arr.clone();
|
|
rev.reverse();
|
|
Ok(JsonValue::Array(rev))
|
|
}
|
|
JsonValue::String(s) => {
|
|
Ok(json!(s.chars().rev().collect::<String>()))
|
|
}
|
|
_ => Err(EvalError::TypeError(format!(
|
|
"reversed() requires array or string, got {}",
|
|
type_name(v)
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn fn_sort(v: &JsonValue) -> EvalResult<JsonValue> {
|
|
let arr = v.as_array().ok_or_else(|| {
|
|
EvalError::TypeError(format!("sort() requires array, got {}", type_name(v)))
|
|
})?;
|
|
|
|
let mut sorted = arr.clone();
|
|
// Sort stably; numbers first, then strings
|
|
let mut err: Option<EvalError> = None;
|
|
sorted.sort_by(|a, b| {
|
|
if err.is_some() {
|
|
return std::cmp::Ordering::Equal;
|
|
}
|
|
match (as_numeric(a), as_numeric(b)) {
|
|
(Some(x), Some(y)) => x
|
|
.as_f64()
|
|
.partial_cmp(&y.as_f64())
|
|
.unwrap_or(std::cmp::Ordering::Equal),
|
|
_ => {
|
|
if let (Some(sa), Some(sb)) = (a.as_str(), b.as_str()) {
|
|
sa.cmp(sb)
|
|
} else {
|
|
err = Some(EvalError::TypeError(
|
|
"sort() requires array of numbers or strings".to_string(),
|
|
));
|
|
std::cmp::Ordering::Equal
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
if let Some(e) = err {
|
|
return Err(e);
|
|
}
|
|
|
|
Ok(JsonValue::Array(sorted))
|
|
}
|
|
|
|
fn fn_unique(v: &JsonValue) -> EvalResult<JsonValue> {
|
|
let arr = v.as_array().ok_or_else(|| {
|
|
EvalError::TypeError(format!("unique() requires array, got {}", type_name(v)))
|
|
})?;
|
|
|
|
let mut seen = Vec::new();
|
|
let mut result = Vec::new();
|
|
for item in arr {
|
|
if !seen.iter().any(|s| json_eq(s, item)) {
|
|
seen.push(item.clone());
|
|
result.push(item.clone());
|
|
}
|
|
}
|
|
|
|
Ok(JsonValue::Array(result))
|
|
}
|
|
|
|
fn fn_flat(v: &JsonValue) -> EvalResult<JsonValue> {
|
|
let arr = v.as_array().ok_or_else(|| {
|
|
EvalError::TypeError(format!("flat() requires array, got {}", type_name(v)))
|
|
})?;
|
|
|
|
let mut result = Vec::new();
|
|
for item in arr {
|
|
if let JsonValue::Array(inner) = item {
|
|
result.extend(inner.iter().cloned());
|
|
} else {
|
|
result.push(item.clone());
|
|
}
|
|
}
|
|
|
|
Ok(JsonValue::Array(result))
|
|
}
|
|
|
|
fn fn_zip(a: &JsonValue, b: &JsonValue) -> EvalResult<JsonValue> {
|
|
let a_arr = a.as_array().ok_or_else(|| {
|
|
EvalError::TypeError(format!("zip() first argument must be array, got {}", type_name(a)))
|
|
})?;
|
|
let b_arr = b.as_array().ok_or_else(|| {
|
|
EvalError::TypeError(format!(
|
|
"zip() second argument must be array, got {}",
|
|
type_name(b)
|
|
))
|
|
})?;
|
|
|
|
let pairs: Vec<JsonValue> = a_arr
|
|
.iter()
|
|
.zip(b_arr.iter())
|
|
.map(|(x, y)| json!([x, y]))
|
|
.collect();
|
|
|
|
Ok(JsonValue::Array(pairs))
|
|
}
|
|
|
|
fn fn_range_1(end: &JsonValue) -> EvalResult<JsonValue> {
|
|
let n = end.as_i64().ok_or_else(|| {
|
|
EvalError::TypeError("range() requires integer argument".to_string())
|
|
})?;
|
|
let arr: Vec<JsonValue> = (0..n).map(|i| json!(i)).collect();
|
|
Ok(JsonValue::Array(arr))
|
|
}
|
|
|
|
fn fn_range_2(start: &JsonValue, end: &JsonValue) -> EvalResult<JsonValue> {
|
|
let s = start.as_i64().ok_or_else(|| {
|
|
EvalError::TypeError("range() requires integer arguments".to_string())
|
|
})?;
|
|
let e = end.as_i64().ok_or_else(|| {
|
|
EvalError::TypeError("range() requires integer arguments".to_string())
|
|
})?;
|
|
let arr: Vec<JsonValue> = (s..e).map(|i| json!(i)).collect();
|
|
Ok(JsonValue::Array(arr))
|
|
}
|
|
|
|
fn fn_slice(v: &JsonValue, start: &JsonValue, end: &JsonValue) -> EvalResult<JsonValue> {
|
|
let s = start.as_i64().ok_or_else(|| {
|
|
EvalError::TypeError("slice() start must be integer".to_string())
|
|
})? as usize;
|
|
|
|
match v {
|
|
JsonValue::Array(arr) => {
|
|
let e = if end.is_null() {
|
|
arr.len()
|
|
} else {
|
|
end.as_i64()
|
|
.ok_or_else(|| EvalError::TypeError("slice() end must be integer".to_string()))?
|
|
as usize
|
|
};
|
|
let e = e.min(arr.len());
|
|
let s = s.min(e);
|
|
Ok(JsonValue::Array(arr[s..e].to_vec()))
|
|
}
|
|
JsonValue::String(str_val) => {
|
|
let chars: Vec<char> = str_val.chars().collect();
|
|
let e = if end.is_null() {
|
|
chars.len()
|
|
} else {
|
|
end.as_i64()
|
|
.ok_or_else(|| EvalError::TypeError("slice() end must be integer".to_string()))?
|
|
as usize
|
|
};
|
|
let e = e.min(chars.len());
|
|
let s = s.min(e);
|
|
Ok(json!(chars[s..e].iter().collect::<String>()))
|
|
}
|
|
_ => Err(EvalError::TypeError(format!(
|
|
"slice() requires array or string, got {}",
|
|
type_name(v)
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn fn_index_of(haystack: &JsonValue, needle: &JsonValue) -> EvalResult<JsonValue> {
|
|
match haystack {
|
|
JsonValue::Array(arr) => {
|
|
for (i, item) in arr.iter().enumerate() {
|
|
if json_eq(item, needle) {
|
|
return Ok(json!(i as i64));
|
|
}
|
|
}
|
|
Ok(json!(-1))
|
|
}
|
|
JsonValue::String(s) => {
|
|
let needle = needle.as_str().ok_or_else(|| {
|
|
EvalError::TypeError("index_of() needle must be string for string search".to_string())
|
|
})?;
|
|
match s.find(needle) {
|
|
Some(pos) => Ok(json!(pos as i64)),
|
|
None => Ok(json!(-1)),
|
|
}
|
|
}
|
|
_ => Err(EvalError::TypeError(format!(
|
|
"index_of() requires array or string, got {}",
|
|
type_name(haystack)
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn fn_count(haystack: &JsonValue, needle: &JsonValue) -> EvalResult<JsonValue> {
|
|
match haystack {
|
|
JsonValue::Array(arr) => {
|
|
let count = arr.iter().filter(|item| json_eq(item, needle)).count();
|
|
Ok(json!(count as i64))
|
|
}
|
|
JsonValue::String(s) => {
|
|
let needle = needle.as_str().ok_or_else(|| {
|
|
EvalError::TypeError("count() needle must be string for string search".to_string())
|
|
})?;
|
|
Ok(json!(s.matches(needle).count() as i64))
|
|
}
|
|
_ => Err(EvalError::TypeError(format!(
|
|
"count() requires array or string, got {}",
|
|
type_name(haystack)
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn fn_merge(a: &JsonValue, b: &JsonValue) -> EvalResult<JsonValue> {
|
|
match (a, b) {
|
|
(JsonValue::Object(obj_a), JsonValue::Object(obj_b)) => {
|
|
let mut result = obj_a.clone();
|
|
for (k, v) in obj_b {
|
|
result.insert(k.clone(), v.clone());
|
|
}
|
|
Ok(JsonValue::Object(result))
|
|
}
|
|
_ => Err(EvalError::TypeError(format!(
|
|
"merge() requires two objects, got {} and {}",
|
|
type_name(a),
|
|
type_name(b)
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn fn_chunks(v: &JsonValue, size: &JsonValue) -> EvalResult<JsonValue> {
|
|
let n = size.as_i64().ok_or_else(|| {
|
|
EvalError::TypeError("chunks() size must be a positive integer".to_string())
|
|
})?;
|
|
if n <= 0 {
|
|
return Err(EvalError::TypeError(
|
|
"chunks() size must be a positive integer".to_string(),
|
|
));
|
|
}
|
|
let n = n as usize;
|
|
|
|
match v {
|
|
JsonValue::Array(arr) => {
|
|
let chunks: Vec<JsonValue> = arr
|
|
.chunks(n)
|
|
.map(|c| JsonValue::Array(c.to_vec()))
|
|
.collect();
|
|
Ok(JsonValue::Array(chunks))
|
|
}
|
|
_ => Err(EvalError::TypeError(format!(
|
|
"chunks() requires array, got {}",
|
|
type_name(v)
|
|
))),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn test_is_truthy() {
|
|
assert!(!is_truthy(&json!(null)));
|
|
assert!(!is_truthy(&json!(false)));
|
|
assert!(!is_truthy(&json!(0)));
|
|
assert!(!is_truthy(&json!("")));
|
|
assert!(!is_truthy(&json!([])));
|
|
assert!(!is_truthy(&json!({})));
|
|
|
|
assert!(is_truthy(&json!(true)));
|
|
assert!(is_truthy(&json!(1)));
|
|
assert!(is_truthy(&json!("a")));
|
|
assert!(is_truthy(&json!([1])));
|
|
assert!(is_truthy(&json!({"a": 1})));
|
|
}
|
|
|
|
#[test]
|
|
fn test_json_eq_cross_numeric() {
|
|
assert!(json_eq(&json!(3), &json!(3.0)));
|
|
assert!(json_eq(&json!(3.0), &json!(3)));
|
|
assert!(!json_eq(&json!(3), &json!(3.1)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_json_eq_recursive() {
|
|
assert!(json_eq(
|
|
&json!({"a": [1, 2], "b": {"c": 3}}),
|
|
&json!({"b": {"c": 3}, "a": [1, 2]})
|
|
));
|
|
assert!(!json_eq(
|
|
&json!({"a": [1, 2]}),
|
|
&json!({"a": [1, 3]})
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_negative_indexing() {
|
|
let arr = json!([10, 20, 30]);
|
|
assert_eq!(index_access(&arr, &json!(-1)).unwrap(), json!(30));
|
|
assert_eq!(index_access(&arr, &json!(-2)).unwrap(), json!(20));
|
|
}
|
|
|
|
#[test]
|
|
fn test_integer_division() {
|
|
// 10 / 5 = 2 (integer)
|
|
assert_eq!(eval_div(&json!(10), &json!(5)).unwrap(), json!(2));
|
|
// 10 / 3 = 3.333... (float because not evenly divisible)
|
|
let result = eval_div(&json!(10), &json!(3)).unwrap();
|
|
assert!(result.is_f64());
|
|
}
|
|
}
|