use crate::ctx::EvalCtx;
use crate::error::ApplyError;
use serde_json::{Map, Number, Value};
fn js_to_string(v: &Value) -> String {
match v {
Value::Object(_) => "[object Object]".into(),
Value::Bool(b) => b.to_string(),
Value::Null => "null".into(),
Value::Number(n) => n.to_string(),
Value::String(s) => s.clone(),
Value::Array(arr) => arr
.iter()
.map(|i| match i {
Value::Null => String::new(),
_ => js_to_string(i),
})
.collect::<Vec<_>>()
.join(","),
}
}
fn js_to_number(v: &Value) -> Option<f64> {
match v {
Value::Null => Some(0.0),
Value::Bool(true) => Some(1.0),
Value::Bool(false) => Some(0.0),
Value::Number(n) => n.as_f64(),
Value::String(s) => {
let trimmed = s.trim();
if trimmed.is_empty() {
Some(0.0)
} else {
trimmed.parse::<f64>().ok()
}
}
Value::Array(_) => js_to_number(&Value::String(js_to_string(v))),
Value::Object(_) => None,
}
}
fn js_parse_float(v: &Value) -> Option<f64> {
match v {
Value::Number(n) => n.as_f64(),
Value::String(s) => parse_float_str(s),
_ => parse_float_str(&js_to_string(v)),
}
}
fn parse_float_str(s: &str) -> Option<f64> {
const NUMERICS: &[char] = &['0','1','2','3','4','5','6','7','8','9','.', '-', '+', 'e', 'E'];
let trimmed = s.trim();
let (mut chars, mut saw_decimal) = (Vec::new(), false);
let mut broke = false;
for c in trimmed.chars() {
if broke { break; }
if NUMERICS.contains(&c) {
if c == '.' {
if saw_decimal { broke = true; continue; }
saw_decimal = true;
}
chars.push(c);
} else {
broke = true;
}
}
if chars.is_empty() { return None; }
if matches!(chars.last(), Some('e') | Some('E')) { chars.pop(); }
chars.iter().collect::<String>().parse::<f64>().ok()
}
pub(crate) fn truthy(v: &Value) -> bool {
match v {
Value::Null => false,
Value::Bool(b) => *b,
Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
Value::String(s) => !s.is_empty(),
Value::Array(a) => !a.is_empty(),
Value::Object(_) => true,
}
}
fn abstract_eq(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Null, Value::Null) => true,
(Value::Number(x), Value::Number(y)) => {
x.as_f64().zip(y.as_f64()).map(|(xf, yf)| xf == yf).unwrap_or(false)
}
(Value::String(x), Value::String(y)) => x == y,
(Value::Bool(x), Value::Bool(y)) => x == y,
(Value::Bool(x), _) => {
let n = Value::Number(Number::from_f64(if *x { 1.0 } else { 0.0 }).unwrap());
abstract_eq(&n, b)
}
(_, Value::Bool(y)) => {
let n = Value::Number(Number::from_f64(if *y { 1.0 } else { 0.0 }).unwrap());
abstract_eq(a, &n)
}
(Value::Number(_), Value::String(_)) | (Value::String(_), Value::Number(_)) => {
let an = js_to_number(a);
let bn = js_to_number(b);
an.zip(bn).map(|(x, y)| x == y).unwrap_or(false)
}
(Value::String(_), Value::Array(_))
| (Value::Number(_), Value::Array(_))
| (Value::String(_), Value::Object(_))
| (Value::Number(_), Value::Object(_)) => {
abstract_eq(a, &Value::String(js_to_string(b)))
}
(Value::Array(_), Value::String(_))
| (Value::Array(_), Value::Number(_))
| (Value::Object(_), Value::String(_))
| (Value::Object(_), Value::Number(_)) => {
abstract_eq(&Value::String(js_to_string(a)), b)
}
_ => false,
}
}
fn abstract_lt(a: &Value, b: &Value) -> bool {
match (js_to_string_if_prim(a), js_to_string_if_prim(b)) {
(Some(sa), Some(sb)) => sa < sb,
_ => {
let an = js_to_number(a).unwrap_or(f64::NAN);
let bn = js_to_number(b).unwrap_or(f64::NAN);
an < bn
}
}
}
fn abstract_gt(a: &Value, b: &Value) -> bool { abstract_lt(b, a) }
fn abstract_lte(a: &Value, b: &Value) -> bool { abstract_lt(a, b) || abstract_eq(a, b) }
fn abstract_gte(a: &Value, b: &Value) -> bool { abstract_gt(a, b) || abstract_eq(a, b) }
fn js_to_string_if_prim(v: &Value) -> Option<String> {
match v {
Value::String(s) => Some(s.clone()),
_ => None,
}
}
fn to_json_number(n: f64) -> Value {
if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 {
Value::Number(Number::from(n as i64))
} else {
Number::from_f64(n)
.map(Value::Number)
.unwrap_or(Value::Null)
}
}
fn var_get(data: &Value, key: &str) -> Option<Value> {
if key.is_empty() {
return Some(data.clone());
}
key.split('.').fold(Some(data.clone()), |acc, segment| {
let cur = acc?;
match cur {
Value::Object(ref map) => map.get(segment).cloned(),
Value::Array(ref arr) => segment
.parse::<i64>()
.ok()
.and_then(|i| {
let idx = if i < 0 {
arr.len().checked_sub(i.unsigned_abs() as usize)?
} else {
i as usize
};
arr.get(idx).cloned()
}),
Value::String(ref s) => segment
.parse::<i64>()
.ok()
.and_then(|i| {
let chars: Vec<char> = s.chars().collect();
let idx = if i < 0 {
chars.len().checked_sub(i.unsigned_abs() as usize)?
} else {
i as usize
};
chars.get(idx).map(|c| Value::String(c.to_string()))
}),
_ => None,
}
})
}
fn eval_var(args: &[&Value], data: &Value) -> Result<Value, ApplyError> {
if args.is_empty() {
return Ok(data.clone());
}
let key_str = match args[0] {
Value::Null => return Ok(data.clone()),
Value::String(s) => s.clone(),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
i.to_string()
} else {
return Err(ApplyError::InvalidArguments("var key must be integer or string".into()));
}
}
other => return Err(ApplyError::InvalidArguments(
format!("var key must be string, number, or null; got {:?}", other)
)),
};
let val = var_get(data, &key_str);
Ok(val.unwrap_or_else(|| {
if args.len() >= 2 {
args[1].clone()
} else {
Value::Null
}
}))
}
fn eval_missing(args: &[&Value], data: &Value) -> Result<Value, ApplyError> {
let key_list: Vec<&Value> = if !args.is_empty() {
match args[0] {
Value::Array(inner) => inner.iter().collect(),
_ => args.to_vec(),
}
} else {
vec![]
};
let missing: Vec<Value> = key_list
.into_iter()
.filter(|k| {
let key_str = match k {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
_ => return false,
};
var_get(data, &key_str).is_none()
})
.map(|k| (*k).clone())
.collect();
Ok(Value::Array(missing))
}
fn eval_missing_some(args: &[&Value], data: &Value) -> Result<Value, ApplyError> {
if args.len() < 2 {
return Err(ApplyError::InvalidArguments("missing_some requires 2 args".into()));
}
let threshold = match args[0] {
Value::Number(n) => n.as_u64().ok_or_else(|| {
ApplyError::InvalidArguments("missing_some threshold must be non-negative integer".into())
})?,
_ => return Err(ApplyError::InvalidArguments("missing_some threshold must be a number".into())),
};
let keys = match args[1] {
Value::Array(arr) => arr,
_ => return Err(ApplyError::InvalidArguments("missing_some keys must be an array".into())),
};
let mut missing_keys: Vec<Value> = Vec::new();
let mut present = 0u64;
for key in keys {
if present >= threshold { break; }
let key_str = match key {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Null => continue,
_ => continue,
};
if var_get(data, &key_str).is_some() {
present += 1;
} else if !missing_keys.contains(key) {
missing_keys.push(key.clone());
}
}
if present >= threshold {
Ok(Value::Array(vec![]))
} else {
Ok(Value::Array(missing_keys))
}
}
pub(crate) fn apply_value(
logic: &Value,
data: &Value,
ctx: &EvalCtx,
fuel: &mut usize,
) -> Result<Value, ApplyError> {
if *fuel == 0 {
return Err(ApplyError::FuelExceeded);
}
*fuel -= 1;
let obj = match logic {
Value::Object(m) if m.len() == 1 => m,
_ => return Ok(logic.clone()),
};
let (op, args_val) = obj.iter().next().unwrap();
let op: &str = op.as_str();
let args: Vec<&Value> = match args_val {
Value::Array(arr) => arr.iter().collect(),
_ => vec![args_val],
};
match op {
"var" => eval_var(&args, data),
"missing" => eval_missing(&args, data),
"missing_some" => eval_missing_some(&args, data),
"==" => {
require_args(op, &args, 2, 2)?;
let a = apply_value(args[0], data, ctx, fuel)?;
let b = apply_value(args[1], data, ctx, fuel)?;
Ok(Value::Bool(abstract_eq(&a, &b)))
}
"!=" => {
require_args(op, &args, 2, 2)?;
let a = apply_value(args[0], data, ctx, fuel)?;
let b = apply_value(args[1], data, ctx, fuel)?;
Ok(Value::Bool(!abstract_eq(&a, &b)))
}
"===" => {
require_args(op, &args, 2, 2)?;
let a = apply_value(args[0], data, ctx, fuel)?;
let b = apply_value(args[1], data, ctx, fuel)?;
Ok(Value::Bool(a == b))
}
"!==" => {
require_args(op, &args, 2, 2)?;
let a = apply_value(args[0], data, ctx, fuel)?;
let b = apply_value(args[1], data, ctx, fuel)?;
Ok(Value::Bool(a != b))
}
"!" => {
require_args(op, &args, 1, 1)?;
let a = apply_value(args[0], data, ctx, fuel)?;
Ok(Value::Bool(!truthy(&a)))
}
"!!" => {
require_args(op, &args, 1, 1)?;
let a = apply_value(args[0], data, ctx, fuel)?;
Ok(Value::Bool(truthy(&a)))
}
"and" => {
if args.is_empty() {
return Err(ApplyError::InvalidArguments("'and' requires at least 1 arg".into()));
}
let mut last = Value::Null;
for arg in &args {
last = apply_value(arg, data, ctx, fuel)?;
if !truthy(&last) {
return Ok(last);
}
}
Ok(last)
}
"or" => {
if args.is_empty() {
return Err(ApplyError::InvalidArguments("'or' requires at least 1 arg".into()));
}
let mut last = Value::Null;
for arg in &args {
last = apply_value(arg, data, ctx, fuel)?;
if truthy(&last) {
return Ok(last);
}
}
Ok(last)
}
"if" | "?:" => eval_if(&args, data, ctx, fuel),
"<" => {
require_args_range(op, &args, 2, 3)?;
let vals = eval_all(&args, data, ctx, fuel)?;
if vals.len() == 2 {
Ok(Value::Bool(abstract_lt(&vals[0], &vals[1])))
} else {
Ok(Value::Bool(abstract_lt(&vals[0], &vals[1]) && abstract_lt(&vals[1], &vals[2])))
}
}
"<=" => {
require_args_range(op, &args, 2, 3)?;
let vals = eval_all(&args, data, ctx, fuel)?;
if vals.len() == 2 {
Ok(Value::Bool(abstract_lte(&vals[0], &vals[1])))
} else {
Ok(Value::Bool(abstract_lte(&vals[0], &vals[1]) && abstract_lte(&vals[1], &vals[2])))
}
}
">" => {
require_args_range(op, &args, 2, 3)?;
let vals = eval_all(&args, data, ctx, fuel)?;
if vals.len() == 2 {
Ok(Value::Bool(abstract_gt(&vals[0], &vals[1])))
} else {
Ok(Value::Bool(abstract_gt(&vals[0], &vals[1]) && abstract_gt(&vals[1], &vals[2])))
}
}
">=" => {
require_args_range(op, &args, 2, 3)?;
let vals = eval_all(&args, data, ctx, fuel)?;
if vals.len() == 2 {
Ok(Value::Bool(abstract_gte(&vals[0], &vals[1])))
} else {
Ok(Value::Bool(abstract_gte(&vals[0], &vals[1]) && abstract_gte(&vals[1], &vals[2])))
}
}
"+" => {
let vals = eval_all(&args, data, ctx, fuel)?;
let sum = vals.iter().map(|v| {
js_parse_float(v).ok_or_else(|| {
ApplyError::InvalidArguments(format!("'+' arg could not be converted to float: {:?}", v))
})
}).try_fold(0.0f64, |acc, r| r.map(|n| acc + n))?;
Ok(to_json_number(sum))
}
"-" => {
require_args_range(op, &args, 1, 2)?;
let vals = eval_all(&args, data, ctx, fuel)?;
let result = if vals.len() == 1 {
js_to_number(&vals[0])
.map(|n| -n)
.ok_or_else(|| ApplyError::InvalidArguments(format!("'-' could not negate {:?}", vals[0])))?
} else {
let a = js_to_number(&vals[0]).ok_or_else(|| ApplyError::InvalidArguments(format!("'-' arg not numeric: {:?}", vals[0])))?;
let b = js_to_number(&vals[1]).ok_or_else(|| ApplyError::InvalidArguments(format!("'-' arg not numeric: {:?}", vals[1])))?;
a - b
};
Ok(to_json_number(result))
}
"*" => {
if args.is_empty() {
return Err(ApplyError::InvalidArguments("'*' requires at least 1 arg".into()));
}
let vals = eval_all(&args, data, ctx, fuel)?;
let product = vals.iter().map(|v| {
js_parse_float(v).ok_or_else(|| {
ApplyError::InvalidArguments(format!("'*' arg could not be converted to float: {:?}", v))
})
}).try_fold(1.0f64, |acc, r| r.map(|n| acc * n))?;
Ok(to_json_number(product))
}
"/" => {
require_args(op, &args, 2, 2)?;
let vals = eval_all(&args, data, ctx, fuel)?;
let a = js_to_number(&vals[0]).ok_or_else(|| ApplyError::InvalidArguments(format!("'/' arg not numeric: {:?}", vals[0])))?;
let b = js_to_number(&vals[1]).ok_or_else(|| ApplyError::InvalidArguments(format!("'/' arg not numeric: {:?}", vals[1])))?;
Ok(to_json_number(a / b))
}
"%" => {
require_args(op, &args, 2, 2)?;
let vals = eval_all(&args, data, ctx, fuel)?;
let a = js_to_number(&vals[0]).ok_or_else(|| ApplyError::InvalidArguments(format!("'%' arg not numeric: {:?}", vals[0])))?;
let b = js_to_number(&vals[1]).ok_or_else(|| ApplyError::InvalidArguments(format!("'%' arg not numeric: {:?}", vals[1])))?;
Ok(to_json_number(a % b))
}
"max" => {
if args.is_empty() {
return Err(ApplyError::InvalidArguments("'max' requires at least 1 arg".into()));
}
let vals = eval_all(&args, data, ctx, fuel)?;
let mut result = f64::NEG_INFINITY;
for v in &vals {
let n = js_to_number(v).ok_or_else(|| ApplyError::InvalidArguments(format!("'max' arg not numeric: {:?}", v)))?;
if n > result { result = n; }
}
Ok(to_json_number(result))
}
"min" => {
if args.is_empty() {
return Err(ApplyError::InvalidArguments("'min' requires at least 1 arg".into()));
}
let vals = eval_all(&args, data, ctx, fuel)?;
let mut result = f64::INFINITY;
for v in &vals {
let n = js_to_number(v).ok_or_else(|| ApplyError::InvalidArguments(format!("'min' arg not numeric: {:?}", v)))?;
if n < result { result = n; }
}
Ok(to_json_number(result))
}
"cat" => {
let vals = eval_all(&args, data, ctx, fuel)?;
let s: String = vals.iter().map(|v| js_to_string(v)).collect();
Ok(Value::String(s))
}
"substr" => {
require_args_range(op, &args, 2, 3)?;
let vals = eval_all(&args, data, ctx, fuel)?;
eval_substr(&vals)
}
"merge" => {
let vals = eval_all(&args, data, ctx, fuel)?;
let merged: Vec<Value> = vals.into_iter().flat_map(|v| match v {
Value::Array(arr) => arr,
other => vec![other],
}).collect();
Ok(Value::Array(merged))
}
"in" => {
require_args(op, &args, 2, 2)?;
let vals = eval_all(&args, data, ctx, fuel)?;
let needle = &vals[0];
let haystack = &vals[1];
match haystack {
Value::Null => Ok(Value::Bool(false)),
Value::Array(arr) => Ok(Value::Bool(arr.contains(needle))),
Value::String(s) => {
match needle {
Value::String(ns) => Ok(Value::Bool(s.contains(ns.as_str()))),
_ => Err(ApplyError::InvalidArguments(
"If 'in' haystack is a string, needle must also be a string".into()
)),
}
}
_ => Err(ApplyError::InvalidArguments(format!(
"'in' haystack must be array or string, got {:?}", haystack
))),
}
}
"map" => {
require_args(op, &args, 2, 2)?;
let items_raw = apply_value(args[0], data, ctx, fuel)?;
let items = coerce_to_array(items_raw)?;
let expr = args[1];
let result: Result<Vec<Value>, _> = items.iter()
.map(|item| apply_value(expr, item, ctx, fuel))
.collect();
Ok(Value::Array(result?))
}
"filter" => {
require_args(op, &args, 2, 2)?;
let items_raw = apply_value(args[0], data, ctx, fuel)?;
let items = coerce_to_array(items_raw)?;
let expr = args[1];
let mut out = Vec::new();
for item in &items {
let pred = apply_value(expr, item, ctx, fuel)?;
if truthy(&pred) { out.push(item.clone()); }
}
Ok(Value::Array(out))
}
"reduce" => {
require_args(op, &args, 3, 3)?;
let items_raw = apply_value(args[0], data, ctx, fuel)?;
let items = coerce_to_array(items_raw)?;
let expr = args[1];
let mut acc = apply_value(args[2], data, ctx, fuel)?;
for item in items {
let mut reduce_data = Map::with_capacity(2);
reduce_data.insert("current".into(), item);
reduce_data.insert("accumulator".into(), acc);
acc = apply_value(expr, &Value::Object(reduce_data), ctx, fuel)?;
}
Ok(acc)
}
"all" => {
require_args(op, &args, 2, 2)?;
let items_raw = args_to_iterable(args[0], data, ctx, fuel)?;
if items_raw.is_empty() { return Ok(Value::Bool(false)); }
let expr = args[1];
for item in &items_raw {
let pred = apply_value(expr, item, ctx, fuel)?;
if !truthy(&pred) { return Ok(Value::Bool(false)); }
}
Ok(Value::Bool(true))
}
"some" => {
require_args(op, &args, 2, 2)?;
let items_raw = args_to_iterable(args[0], data, ctx, fuel)?;
if items_raw.is_empty() { return Ok(Value::Bool(false)); }
let expr = args[1];
for item in &items_raw {
let pred = apply_value(expr, item, ctx, fuel)?;
if truthy(&pred) { return Ok(Value::Bool(true)); }
}
Ok(Value::Bool(false))
}
"none" => {
require_args(op, &args, 2, 2)?;
let items_raw = args_to_iterable(args[0], data, ctx, fuel)?;
if items_raw.is_empty() { return Ok(Value::Bool(true)); }
let expr = args[1];
for item in &items_raw {
let pred = apply_value(expr, item, ctx, fuel)?;
if truthy(&pred) { return Ok(Value::Bool(false)); }
}
Ok(Value::Bool(true))
}
"log" => {
require_args(op, &args, 1, 1)?;
let val = apply_value(args[0], data, ctx, fuel)?;
eprintln!("{}", val);
Ok(val)
}
unknown_op => {
if let Some(handler) = ctx.ops.get(unknown_op) {
let evaluated: Result<Vec<Value>, ApplyError> = args
.iter()
.map(|arg| apply_value(arg, data, ctx, fuel))
.collect();
let evaluated = evaluated?;
handler(&evaluated).map_err(|e| ApplyError::CustomOpFailed {
op: unknown_op.to_string(),
source: e.to_string(),
})
} else {
Err(ApplyError::UnknownOperator(unknown_op.to_string()))
}
}
}
}
fn require_args(op: &str, args: &[&Value], min: usize, max: usize) -> Result<(), ApplyError> {
if args.len() < min || args.len() > max {
Err(ApplyError::InvalidArguments(format!(
"'{}' requires {}-{} args, got {}", op, min, max, args.len()
)))
} else {
Ok(())
}
}
fn require_args_range(op: &str, args: &[&Value], min: usize, max: usize) -> Result<(), ApplyError> {
require_args(op, args, min, max)
}
fn eval_all(
args: &[&Value],
data: &Value,
ctx: &EvalCtx,
fuel: &mut usize,
) -> Result<Vec<Value>, ApplyError> {
args.iter().map(|a| apply_value(a, data, ctx, fuel)).collect()
}
fn eval_if(
args: &[&Value],
data: &Value,
ctx: &EvalCtx,
fuel: &mut usize,
) -> Result<Value, ApplyError> {
match args.len() {
0 => Ok(Value::Null),
1 => apply_value(args[0], data, ctx, fuel),
_ => {
let mut i = 0;
while i + 1 < args.len() {
let cond = apply_value(args[i], data, ctx, fuel)?;
if truthy(&cond) {
return apply_value(args[i + 1], data, ctx, fuel);
}
i += 2;
}
if i < args.len() {
apply_value(args[i], data, ctx, fuel)
} else {
Ok(Value::Null)
}
}
}
}
fn coerce_to_array(v: Value) -> Result<Vec<Value>, ApplyError> {
match v {
Value::Array(arr) => Ok(arr),
Value::Null => Ok(vec![]),
other => Err(ApplyError::InvalidArguments(format!(
"Expected array, got {:?}", other
))),
}
}
fn args_to_iterable(
first_arg: &Value,
data: &Value,
ctx: &EvalCtx,
fuel: &mut usize,
) -> Result<Vec<Value>, ApplyError> {
let evaled = apply_value(first_arg, data, ctx, fuel)?;
match evaled {
Value::Array(arr) => Ok(arr),
Value::String(s) => Ok(s.chars().map(|c| Value::String(c.to_string())).collect()),
Value::Null => Ok(vec![]),
other => Err(ApplyError::InvalidArguments(format!(
"First argument to all/some/none must be array, string, or null; got {:?}", other
))),
}
}
fn eval_substr(vals: &[Value]) -> Result<Value, ApplyError> {
let s = match &vals[0] {
Value::String(s) => s,
other => return Err(ApplyError::InvalidArguments(format!(
"'substr' first arg must be string, got {:?}", other
))),
};
let idx = match &vals[1] {
Value::Number(n) => n.as_i64().ok_or_else(|| {
ApplyError::InvalidArguments("'substr' index must be integer".into())
})?,
other => return Err(ApplyError::InvalidArguments(format!(
"'substr' index must be number, got {:?}", other
))),
};
let limit = if vals.len() > 2 {
Some(match &vals[2] {
Value::Number(n) => n.as_i64().ok_or_else(|| {
ApplyError::InvalidArguments("'substr' limit must be integer".into())
})?,
other => return Err(ApplyError::InvalidArguments(format!(
"'substr' limit must be number, got {:?}", other
))),
})
} else {
None
};
let chars: Vec<char> = s.chars().collect();
let len = chars.len();
let start = if idx < 0 {
len.saturating_sub((-idx) as usize)
} else {
(idx as usize).min(len)
};
let end = match limit {
None => len,
Some(l) if l < 0 => len.saturating_sub((-l) as usize),
Some(l) => (start + l as usize).min(len),
};
let count = end.saturating_sub(start);
Ok(Value::String(chars.iter().skip(start).take(count).collect()))
}