Files
attune/crates/common/src/workflow/expression/evaluator.rs
2026-03-01 20:43:48 -06:00

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());
}
}