Skip to main content

fsm_guards/engine/
eval.rs

1//! Forked from jsonlogic-rs v0.5. Modified to thread fuel counter and EvalCtx.
2//! Original source: https://github.com/Bestowinc/jsonlogic-rs (MIT)
3
4use crate::ctx::EvalCtx;
5use crate::error::ApplyError;
6use serde_json::{Map, Number, Value};
7
8// ---------------------------------------------------------------------------
9// JS-compatible helper functions (no fuel needed — pure computation)
10// ---------------------------------------------------------------------------
11
12fn js_to_string(v: &Value) -> String {
13    match v {
14        Value::Object(_) => "[object Object]".into(),
15        Value::Bool(b) => b.to_string(),
16        Value::Null => "null".into(),
17        Value::Number(n) => n.to_string(),
18        Value::String(s) => s.clone(),
19        Value::Array(arr) => arr
20            .iter()
21            .map(|i| match i {
22                Value::Null => String::new(),
23                _ => js_to_string(i),
24            })
25            .collect::<Vec<_>>()
26            .join(","),
27    }
28}
29
30/// JS Number() coercion.
31fn js_to_number(v: &Value) -> Option<f64> {
32    match v {
33        Value::Null => Some(0.0),
34        Value::Bool(true) => Some(1.0),
35        Value::Bool(false) => Some(0.0),
36        Value::Number(n) => n.as_f64(),
37        Value::String(s) => {
38            let trimmed = s.trim();
39            if trimmed.is_empty() {
40                Some(0.0)
41            } else {
42                trimmed.parse::<f64>().ok()
43            }
44        }
45        Value::Array(_) => js_to_number(&Value::String(js_to_string(v))),
46        Value::Object(_) => None,
47    }
48}
49
50/// JS parseFloat() — strips leading numeric chars from string representation.
51/// Used by `+` and `*` operators.
52fn js_parse_float(v: &Value) -> Option<f64> {
53    match v {
54        Value::Number(n) => n.as_f64(),
55        Value::String(s) => parse_float_str(s),
56        _ => parse_float_str(&js_to_string(v)),
57    }
58}
59
60fn parse_float_str(s: &str) -> Option<f64> {
61    const NUMERICS: &[char] = &['0','1','2','3','4','5','6','7','8','9','.', '-', '+', 'e', 'E'];
62    let trimmed = s.trim();
63    let (mut chars, mut saw_decimal) = (Vec::new(), false);
64    let mut broke = false;
65    for c in trimmed.chars() {
66        if broke { break; }
67        if NUMERICS.contains(&c) {
68            if c == '.' {
69                if saw_decimal { broke = true; continue; }
70                saw_decimal = true;
71            }
72            chars.push(c);
73        } else {
74            broke = true;
75        }
76    }
77    if chars.is_empty() { return None; }
78    if matches!(chars.last(), Some('e') | Some('E')) { chars.pop(); }
79    chars.iter().collect::<String>().parse::<f64>().ok()
80}
81
82/// JSONLogic truthy: non-zero, non-empty, non-null, non-false.
83pub(crate) fn truthy(v: &Value) -> bool {
84    match v {
85        Value::Null => false,
86        Value::Bool(b) => *b,
87        Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
88        Value::String(s) => !s.is_empty(),
89        Value::Array(a) => !a.is_empty(),
90        Value::Object(_) => true,
91    }
92}
93
94/// JS abstract equality (==).
95fn abstract_eq(a: &Value, b: &Value) -> bool {
96    match (a, b) {
97        (Value::Null, Value::Null) => true,
98        (Value::Number(x), Value::Number(y)) => {
99            x.as_f64().zip(y.as_f64()).map(|(xf, yf)| xf == yf).unwrap_or(false)
100        }
101        (Value::String(x), Value::String(y)) => x == y,
102        (Value::Bool(x), Value::Bool(y)) => x == y,
103        // Bool → Number coercion
104        (Value::Bool(x), _) => {
105            let n = Value::Number(Number::from_f64(if *x { 1.0 } else { 0.0 }).unwrap());
106            abstract_eq(&n, b)
107        }
108        (_, Value::Bool(y)) => {
109            let n = Value::Number(Number::from_f64(if *y { 1.0 } else { 0.0 }).unwrap());
110            abstract_eq(a, &n)
111        }
112        // Number / String cross
113        (Value::Number(_), Value::String(_)) | (Value::String(_), Value::Number(_)) => {
114            let an = js_to_number(a);
115            let bn = js_to_number(b);
116            an.zip(bn).map(|(x, y)| x == y).unwrap_or(false)
117        }
118        // Object/Array → String
119        (Value::String(_), Value::Array(_))
120        | (Value::Number(_), Value::Array(_))
121        | (Value::String(_), Value::Object(_))
122        | (Value::Number(_), Value::Object(_)) => {
123            abstract_eq(a, &Value::String(js_to_string(b)))
124        }
125        (Value::Array(_), Value::String(_))
126        | (Value::Array(_), Value::Number(_))
127        | (Value::Object(_), Value::String(_))
128        | (Value::Object(_), Value::Number(_)) => {
129            abstract_eq(&Value::String(js_to_string(a)), b)
130        }
131        _ => false,
132    }
133}
134
135/// JS abstract less-than.
136fn abstract_lt(a: &Value, b: &Value) -> bool {
137    // Both strings → lexicographic
138    match (js_to_string_if_prim(a), js_to_string_if_prim(b)) {
139        (Some(sa), Some(sb)) => sa < sb,
140        _ => {
141            let an = js_to_number(a).unwrap_or(f64::NAN);
142            let bn = js_to_number(b).unwrap_or(f64::NAN);
143            an < bn
144        }
145    }
146}
147
148fn abstract_gt(a: &Value, b: &Value) -> bool { abstract_lt(b, a) }
149fn abstract_lte(a: &Value, b: &Value) -> bool { abstract_lt(a, b) || abstract_eq(a, b) }
150fn abstract_gte(a: &Value, b: &Value) -> bool { abstract_gt(a, b) || abstract_eq(a, b) }
151
152/// Returns `Some(String)` only when both sides are primitively strings (for lt/gt string compare).
153fn js_to_string_if_prim(v: &Value) -> Option<String> {
154    // For comparison, we only do string compare when the primitive representation is string.
155    // Objects and arrays -> None (use number path).
156    match v {
157        Value::String(s) => Some(s.clone()),
158        _ => None,
159    }
160}
161
162fn to_json_number(n: f64) -> Value {
163    if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 {
164        Value::Number(Number::from(n as i64))
165    } else {
166        Number::from_f64(n)
167            .map(Value::Number)
168            .unwrap_or(Value::Null)
169    }
170}
171
172// ---------------------------------------------------------------------------
173// Variable / data helpers
174// ---------------------------------------------------------------------------
175
176fn var_get(data: &Value, key: &str) -> Option<Value> {
177    if key.is_empty() {
178        return Some(data.clone());
179    }
180    key.split('.').fold(Some(data.clone()), |acc, segment| {
181        let cur = acc?;
182        match cur {
183            Value::Object(ref map) => map.get(segment).cloned(),
184            Value::Array(ref arr) => segment
185                .parse::<i64>()
186                .ok()
187                .and_then(|i| {
188                    let idx = if i < 0 {
189                        arr.len().checked_sub(i.unsigned_abs() as usize)?
190                    } else {
191                        i as usize
192                    };
193                    arr.get(idx).cloned()
194                }),
195            Value::String(ref s) => segment
196                .parse::<i64>()
197                .ok()
198                .and_then(|i| {
199                    let chars: Vec<char> = s.chars().collect();
200                    let idx = if i < 0 {
201                        chars.len().checked_sub(i.unsigned_abs() as usize)?
202                    } else {
203                        i as usize
204                    };
205                    chars.get(idx).map(|c| Value::String(c.to_string()))
206                }),
207            _ => None,
208        }
209    })
210}
211
212fn eval_var(args: &[&Value], data: &Value) -> Result<Value, ApplyError> {
213    if args.is_empty() {
214        return Ok(data.clone());
215    }
216    let key_str = match args[0] {
217        Value::Null => return Ok(data.clone()),
218        Value::String(s) => s.clone(),
219        Value::Number(n) => {
220            if let Some(i) = n.as_i64() {
221                i.to_string()
222            } else {
223                return Err(ApplyError::InvalidArguments("var key must be integer or string".into()));
224            }
225        }
226        other => return Err(ApplyError::InvalidArguments(
227            format!("var key must be string, number, or null; got {:?}", other)
228        )),
229    };
230    let val = var_get(data, &key_str);
231    Ok(val.unwrap_or_else(|| {
232        if args.len() >= 2 {
233            args[1].clone()
234        } else {
235            Value::Null
236        }
237    }))
238}
239
240fn eval_missing(args: &[&Value], data: &Value) -> Result<Value, ApplyError> {
241    // If first arg is an array, use that as the key list
242    let key_list: Vec<&Value> = if !args.is_empty() {
243        match args[0] {
244            Value::Array(inner) => inner.iter().collect(),
245            _ => args.to_vec(),
246        }
247    } else {
248        vec![]
249    };
250    let missing: Vec<Value> = key_list
251        .into_iter()
252        .filter(|k| {
253            let key_str = match k {
254                Value::String(s) => s.clone(),
255                Value::Number(n) => n.to_string(),
256                _ => return false,
257            };
258            var_get(data, &key_str).is_none()
259        })
260        .map(|k| (*k).clone())
261        .collect();
262    Ok(Value::Array(missing))
263}
264
265fn eval_missing_some(args: &[&Value], data: &Value) -> Result<Value, ApplyError> {
266    if args.len() < 2 {
267        return Err(ApplyError::InvalidArguments("missing_some requires 2 args".into()));
268    }
269    let threshold = match args[0] {
270        Value::Number(n) => n.as_u64().ok_or_else(|| {
271            ApplyError::InvalidArguments("missing_some threshold must be non-negative integer".into())
272        })?,
273        _ => return Err(ApplyError::InvalidArguments("missing_some threshold must be a number".into())),
274    };
275    let keys = match args[1] {
276        Value::Array(arr) => arr,
277        _ => return Err(ApplyError::InvalidArguments("missing_some keys must be an array".into())),
278    };
279    let mut missing_keys: Vec<Value> = Vec::new();
280    let mut present = 0u64;
281    for key in keys {
282        if present >= threshold { break; }
283        let key_str = match key {
284            Value::String(s) => s.clone(),
285            Value::Number(n) => n.to_string(),
286            Value::Null => continue,
287            _ => continue,
288        };
289        if var_get(data, &key_str).is_some() {
290            present += 1;
291        } else if !missing_keys.contains(key) {
292            missing_keys.push(key.clone());
293        }
294    }
295    if present >= threshold {
296        Ok(Value::Array(vec![]))
297    } else {
298        Ok(Value::Array(missing_keys))
299    }
300}
301
302// ---------------------------------------------------------------------------
303// Main recursive evaluator
304// ---------------------------------------------------------------------------
305
306pub(crate) fn apply_value(
307    logic: &Value,
308    data: &Value,
309    ctx: &EvalCtx,
310    fuel: &mut usize,
311) -> Result<Value, ApplyError> {
312    if *fuel == 0 {
313        return Err(ApplyError::FuelExceeded);
314    }
315    *fuel -= 1;
316
317    // Non-object (or multi-key object): return as-is.
318    let obj = match logic {
319        Value::Object(m) if m.len() == 1 => m,
320        _ => return Ok(logic.clone()),
321    };
322
323    let (op, args_val) = obj.iter().next().unwrap();
324    let op: &str = op.as_str();
325
326    // Normalize args: array → use as-is, scalar → single-element vec.
327    let args: Vec<&Value> = match args_val {
328        Value::Array(arr) => arr.iter().collect(),
329        _ => vec![args_val],
330    };
331
332    match op {
333        // --- Data access ---
334        "var" => eval_var(&args, data),
335        "missing" => eval_missing(&args, data),
336        "missing_some" => eval_missing_some(&args, data),
337
338        // --- Equality ---
339        "==" => {
340            require_args(op, &args, 2, 2)?;
341            let a = apply_value(args[0], data, ctx, fuel)?;
342            let b = apply_value(args[1], data, ctx, fuel)?;
343            Ok(Value::Bool(abstract_eq(&a, &b)))
344        }
345        "!=" => {
346            require_args(op, &args, 2, 2)?;
347            let a = apply_value(args[0], data, ctx, fuel)?;
348            let b = apply_value(args[1], data, ctx, fuel)?;
349            Ok(Value::Bool(!abstract_eq(&a, &b)))
350        }
351        "===" => {
352            require_args(op, &args, 2, 2)?;
353            let a = apply_value(args[0], data, ctx, fuel)?;
354            let b = apply_value(args[1], data, ctx, fuel)?;
355            Ok(Value::Bool(a == b))
356        }
357        "!==" => {
358            require_args(op, &args, 2, 2)?;
359            let a = apply_value(args[0], data, ctx, fuel)?;
360            let b = apply_value(args[1], data, ctx, fuel)?;
361            Ok(Value::Bool(a != b))
362        }
363
364        // --- Boolean ---
365        "!" => {
366            require_args(op, &args, 1, 1)?;
367            let a = apply_value(args[0], data, ctx, fuel)?;
368            Ok(Value::Bool(!truthy(&a)))
369        }
370        "!!" => {
371            require_args(op, &args, 1, 1)?;
372            let a = apply_value(args[0], data, ctx, fuel)?;
373            Ok(Value::Bool(truthy(&a)))
374        }
375
376        // --- Short-circuit logical ---
377        "and" => {
378            if args.is_empty() {
379                return Err(ApplyError::InvalidArguments("'and' requires at least 1 arg".into()));
380            }
381            let mut last = Value::Null;
382            for arg in &args {
383                last = apply_value(arg, data, ctx, fuel)?;
384                if !truthy(&last) {
385                    return Ok(last);
386                }
387            }
388            Ok(last)
389        }
390        "or" => {
391            if args.is_empty() {
392                return Err(ApplyError::InvalidArguments("'or' requires at least 1 arg".into()));
393            }
394            let mut last = Value::Null;
395            for arg in &args {
396                last = apply_value(arg, data, ctx, fuel)?;
397                if truthy(&last) {
398                    return Ok(last);
399                }
400            }
401            Ok(last)
402        }
403
404        // --- Conditional ---
405        "if" | "?:" => eval_if(&args, data, ctx, fuel),
406
407        // --- Comparison ---
408        "<" => {
409            require_args_range(op, &args, 2, 3)?;
410            let vals = eval_all(&args, data, ctx, fuel)?;
411            if vals.len() == 2 {
412                Ok(Value::Bool(abstract_lt(&vals[0], &vals[1])))
413            } else {
414                Ok(Value::Bool(abstract_lt(&vals[0], &vals[1]) && abstract_lt(&vals[1], &vals[2])))
415            }
416        }
417        "<=" => {
418            require_args_range(op, &args, 2, 3)?;
419            let vals = eval_all(&args, data, ctx, fuel)?;
420            if vals.len() == 2 {
421                Ok(Value::Bool(abstract_lte(&vals[0], &vals[1])))
422            } else {
423                Ok(Value::Bool(abstract_lte(&vals[0], &vals[1]) && abstract_lte(&vals[1], &vals[2])))
424            }
425        }
426        ">" => {
427            require_args_range(op, &args, 2, 3)?;
428            let vals = eval_all(&args, data, ctx, fuel)?;
429            if vals.len() == 2 {
430                Ok(Value::Bool(abstract_gt(&vals[0], &vals[1])))
431            } else {
432                Ok(Value::Bool(abstract_gt(&vals[0], &vals[1]) && abstract_gt(&vals[1], &vals[2])))
433            }
434        }
435        ">=" => {
436            require_args_range(op, &args, 2, 3)?;
437            let vals = eval_all(&args, data, ctx, fuel)?;
438            if vals.len() == 2 {
439                Ok(Value::Bool(abstract_gte(&vals[0], &vals[1])))
440            } else {
441                Ok(Value::Bool(abstract_gte(&vals[0], &vals[1]) && abstract_gte(&vals[1], &vals[2])))
442            }
443        }
444
445        // --- Arithmetic ---
446        "+" => {
447            let vals = eval_all(&args, data, ctx, fuel)?;
448            let sum = vals.iter().map(|v| {
449                js_parse_float(v).ok_or_else(|| {
450                    ApplyError::InvalidArguments(format!("'+' arg could not be converted to float: {:?}", v))
451                })
452            }).try_fold(0.0f64, |acc, r| r.map(|n| acc + n))?;
453            Ok(to_json_number(sum))
454        }
455        "-" => {
456            require_args_range(op, &args, 1, 2)?;
457            let vals = eval_all(&args, data, ctx, fuel)?;
458            let result = if vals.len() == 1 {
459                js_to_number(&vals[0])
460                    .map(|n| -n)
461                    .ok_or_else(|| ApplyError::InvalidArguments(format!("'-' could not negate {:?}", vals[0])))?
462            } else {
463                let a = js_to_number(&vals[0]).ok_or_else(|| ApplyError::InvalidArguments(format!("'-' arg not numeric: {:?}", vals[0])))?;
464                let b = js_to_number(&vals[1]).ok_or_else(|| ApplyError::InvalidArguments(format!("'-' arg not numeric: {:?}", vals[1])))?;
465                a - b
466            };
467            Ok(to_json_number(result))
468        }
469        "*" => {
470            if args.is_empty() {
471                return Err(ApplyError::InvalidArguments("'*' requires at least 1 arg".into()));
472            }
473            let vals = eval_all(&args, data, ctx, fuel)?;
474            let product = vals.iter().map(|v| {
475                js_parse_float(v).ok_or_else(|| {
476                    ApplyError::InvalidArguments(format!("'*' arg could not be converted to float: {:?}", v))
477                })
478            }).try_fold(1.0f64, |acc, r| r.map(|n| acc * n))?;
479            Ok(to_json_number(product))
480        }
481        "/" => {
482            require_args(op, &args, 2, 2)?;
483            let vals = eval_all(&args, data, ctx, fuel)?;
484            let a = js_to_number(&vals[0]).ok_or_else(|| ApplyError::InvalidArguments(format!("'/' arg not numeric: {:?}", vals[0])))?;
485            let b = js_to_number(&vals[1]).ok_or_else(|| ApplyError::InvalidArguments(format!("'/' arg not numeric: {:?}", vals[1])))?;
486            Ok(to_json_number(a / b))
487        }
488        "%" => {
489            require_args(op, &args, 2, 2)?;
490            let vals = eval_all(&args, data, ctx, fuel)?;
491            let a = js_to_number(&vals[0]).ok_or_else(|| ApplyError::InvalidArguments(format!("'%' arg not numeric: {:?}", vals[0])))?;
492            let b = js_to_number(&vals[1]).ok_or_else(|| ApplyError::InvalidArguments(format!("'%' arg not numeric: {:?}", vals[1])))?;
493            Ok(to_json_number(a % b))
494        }
495        "max" => {
496            if args.is_empty() {
497                return Err(ApplyError::InvalidArguments("'max' requires at least 1 arg".into()));
498            }
499            let vals = eval_all(&args, data, ctx, fuel)?;
500            let mut result = f64::NEG_INFINITY;
501            for v in &vals {
502                let n = js_to_number(v).ok_or_else(|| ApplyError::InvalidArguments(format!("'max' arg not numeric: {:?}", v)))?;
503                if n > result { result = n; }
504            }
505            Ok(to_json_number(result))
506        }
507        "min" => {
508            if args.is_empty() {
509                return Err(ApplyError::InvalidArguments("'min' requires at least 1 arg".into()));
510            }
511            let vals = eval_all(&args, data, ctx, fuel)?;
512            let mut result = f64::INFINITY;
513            for v in &vals {
514                let n = js_to_number(v).ok_or_else(|| ApplyError::InvalidArguments(format!("'min' arg not numeric: {:?}", v)))?;
515                if n < result { result = n; }
516            }
517            Ok(to_json_number(result))
518        }
519
520        // --- String ---
521        "cat" => {
522            let vals = eval_all(&args, data, ctx, fuel)?;
523            let s: String = vals.iter().map(|v| js_to_string(v)).collect();
524            Ok(Value::String(s))
525        }
526        "substr" => {
527            require_args_range(op, &args, 2, 3)?;
528            let vals = eval_all(&args, data, ctx, fuel)?;
529            eval_substr(&vals)
530        }
531
532        // --- Array ---
533        "merge" => {
534            let vals = eval_all(&args, data, ctx, fuel)?;
535            let merged: Vec<Value> = vals.into_iter().flat_map(|v| match v {
536                Value::Array(arr) => arr,
537                other => vec![other],
538            }).collect();
539            Ok(Value::Array(merged))
540        }
541        "in" => {
542            require_args(op, &args, 2, 2)?;
543            let vals = eval_all(&args, data, ctx, fuel)?;
544            let needle = &vals[0];
545            let haystack = &vals[1];
546            match haystack {
547                Value::Null => Ok(Value::Bool(false)),
548                Value::Array(arr) => Ok(Value::Bool(arr.contains(needle))),
549                Value::String(s) => {
550                    match needle {
551                        Value::String(ns) => Ok(Value::Bool(s.contains(ns.as_str()))),
552                        _ => Err(ApplyError::InvalidArguments(
553                            "If 'in' haystack is a string, needle must also be a string".into()
554                        )),
555                    }
556                }
557                _ => Err(ApplyError::InvalidArguments(format!(
558                    "'in' haystack must be array or string, got {:?}", haystack
559                ))),
560            }
561        }
562
563        // --- Array higher-order (lazy — expression evaluated per element) ---
564        "map" => {
565            require_args(op, &args, 2, 2)?;
566            let items_raw = apply_value(args[0], data, ctx, fuel)?;
567            let items = coerce_to_array(items_raw)?;
568            let expr = args[1];
569            let result: Result<Vec<Value>, _> = items.iter()
570                .map(|item| apply_value(expr, item, ctx, fuel))
571                .collect();
572            Ok(Value::Array(result?))
573        }
574        "filter" => {
575            require_args(op, &args, 2, 2)?;
576            let items_raw = apply_value(args[0], data, ctx, fuel)?;
577            let items = coerce_to_array(items_raw)?;
578            let expr = args[1];
579            let mut out = Vec::new();
580            for item in &items {
581                let pred = apply_value(expr, item, ctx, fuel)?;
582                if truthy(&pred) { out.push(item.clone()); }
583            }
584            Ok(Value::Array(out))
585        }
586        "reduce" => {
587            require_args(op, &args, 3, 3)?;
588            let items_raw = apply_value(args[0], data, ctx, fuel)?;
589            let items = coerce_to_array(items_raw)?;
590            let expr = args[1];
591            let mut acc = apply_value(args[2], data, ctx, fuel)?;
592            for item in items {
593                let mut reduce_data = Map::with_capacity(2);
594                reduce_data.insert("current".into(), item);
595                reduce_data.insert("accumulator".into(), acc);
596                acc = apply_value(expr, &Value::Object(reduce_data), ctx, fuel)?;
597            }
598            Ok(acc)
599        }
600        "all" => {
601            require_args(op, &args, 2, 2)?;
602            let items_raw = args_to_iterable(args[0], data, ctx, fuel)?;
603            if items_raw.is_empty() { return Ok(Value::Bool(false)); }
604            let expr = args[1];
605            for item in &items_raw {
606                let pred = apply_value(expr, item, ctx, fuel)?;
607                if !truthy(&pred) { return Ok(Value::Bool(false)); }
608            }
609            Ok(Value::Bool(true))
610        }
611        "some" => {
612            require_args(op, &args, 2, 2)?;
613            let items_raw = args_to_iterable(args[0], data, ctx, fuel)?;
614            if items_raw.is_empty() { return Ok(Value::Bool(false)); }
615            let expr = args[1];
616            for item in &items_raw {
617                let pred = apply_value(expr, item, ctx, fuel)?;
618                if truthy(&pred) { return Ok(Value::Bool(true)); }
619            }
620            Ok(Value::Bool(false))
621        }
622        "none" => {
623            require_args(op, &args, 2, 2)?;
624            let items_raw = args_to_iterable(args[0], data, ctx, fuel)?;
625            if items_raw.is_empty() { return Ok(Value::Bool(true)); }
626            let expr = args[1];
627            for item in &items_raw {
628                let pred = apply_value(expr, item, ctx, fuel)?;
629                if truthy(&pred) { return Ok(Value::Bool(false)); }
630            }
631            Ok(Value::Bool(true))
632        }
633
634        // --- Misc ---
635        "log" => {
636            require_args(op, &args, 1, 1)?;
637            let val = apply_value(args[0], data, ctx, fuel)?;
638            // Side-effect: print to stderr (tests check return value, not output).
639            eprintln!("{}", val);
640            Ok(val)
641        }
642
643        // --- Custom operator dispatch ---
644        unknown_op => {
645            if let Some(handler) = ctx.ops.get(unknown_op) {
646                let evaluated: Result<Vec<Value>, ApplyError> = args
647                    .iter()
648                    .map(|arg| apply_value(arg, data, ctx, fuel))
649                    .collect();
650                let evaluated = evaluated?;
651                handler(&evaluated).map_err(|e| ApplyError::CustomOpFailed {
652                    op: unknown_op.to_string(),
653                    source: e.to_string(),
654                })
655            } else {
656                Err(ApplyError::UnknownOperator(unknown_op.to_string()))
657            }
658        }
659    }
660}
661
662// ---------------------------------------------------------------------------
663// Helpers
664// ---------------------------------------------------------------------------
665
666fn require_args(op: &str, args: &[&Value], min: usize, max: usize) -> Result<(), ApplyError> {
667    if args.len() < min || args.len() > max {
668        Err(ApplyError::InvalidArguments(format!(
669            "'{}' requires {}-{} args, got {}", op, min, max, args.len()
670        )))
671    } else {
672        Ok(())
673    }
674}
675
676fn require_args_range(op: &str, args: &[&Value], min: usize, max: usize) -> Result<(), ApplyError> {
677    require_args(op, args, min, max)
678}
679
680fn eval_all(
681    args: &[&Value],
682    data: &Value,
683    ctx: &EvalCtx,
684    fuel: &mut usize,
685) -> Result<Vec<Value>, ApplyError> {
686    args.iter().map(|a| apply_value(a, data, ctx, fuel)).collect()
687}
688
689fn eval_if(
690    args: &[&Value],
691    data: &Value,
692    ctx: &EvalCtx,
693    fuel: &mut usize,
694) -> Result<Value, ApplyError> {
695    match args.len() {
696        0 => Ok(Value::Null),
697        1 => apply_value(args[0], data, ctx, fuel),
698        _ => {
699            let mut i = 0;
700            while i + 1 < args.len() {
701                let cond = apply_value(args[i], data, ctx, fuel)?;
702                if truthy(&cond) {
703                    return apply_value(args[i + 1], data, ctx, fuel);
704                }
705                i += 2;
706            }
707            // If we exhausted all conditions with no match:
708            if i < args.len() {
709                apply_value(args[i], data, ctx, fuel)
710            } else {
711                Ok(Value::Null)
712            }
713        }
714    }
715}
716
717fn coerce_to_array(v: Value) -> Result<Vec<Value>, ApplyError> {
718    match v {
719        Value::Array(arr) => Ok(arr),
720        Value::Null => Ok(vec![]),
721        other => Err(ApplyError::InvalidArguments(format!(
722            "Expected array, got {:?}", other
723        ))),
724    }
725}
726
727/// Evaluate the first arg and coerce it to an iterable (array or string-chars).
728/// Used by all/some/none.
729fn args_to_iterable(
730    first_arg: &Value,
731    data: &Value,
732    ctx: &EvalCtx,
733    fuel: &mut usize,
734) -> Result<Vec<Value>, ApplyError> {
735    let evaled = apply_value(first_arg, data, ctx, fuel)?;
736    match evaled {
737        Value::Array(arr) => Ok(arr),
738        Value::String(s) => Ok(s.chars().map(|c| Value::String(c.to_string())).collect()),
739        Value::Null => Ok(vec![]),
740        other => Err(ApplyError::InvalidArguments(format!(
741            "First argument to all/some/none must be array, string, or null; got {:?}", other
742        ))),
743    }
744}
745
746fn eval_substr(vals: &[Value]) -> Result<Value, ApplyError> {
747    let s = match &vals[0] {
748        Value::String(s) => s,
749        other => return Err(ApplyError::InvalidArguments(format!(
750            "'substr' first arg must be string, got {:?}", other
751        ))),
752    };
753    let idx = match &vals[1] {
754        Value::Number(n) => n.as_i64().ok_or_else(|| {
755            ApplyError::InvalidArguments("'substr' index must be integer".into())
756        })?,
757        other => return Err(ApplyError::InvalidArguments(format!(
758            "'substr' index must be number, got {:?}", other
759        ))),
760    };
761    let limit = if vals.len() > 2 {
762        Some(match &vals[2] {
763            Value::Number(n) => n.as_i64().ok_or_else(|| {
764                ApplyError::InvalidArguments("'substr' limit must be integer".into())
765            })?,
766            other => return Err(ApplyError::InvalidArguments(format!(
767                "'substr' limit must be number, got {:?}", other
768            ))),
769        })
770    } else {
771        None
772    };
773
774    let chars: Vec<char> = s.chars().collect();
775    let len = chars.len();
776
777    let start = if idx < 0 {
778        len.saturating_sub((-idx) as usize)
779    } else {
780        (idx as usize).min(len)
781    };
782
783    let end = match limit {
784        None => len,
785        Some(l) if l < 0 => len.saturating_sub((-l) as usize),
786        Some(l) => (start + l as usize).min(len),
787    };
788
789    let count = end.saturating_sub(start);
790    Ok(Value::String(chars.iter().skip(start).take(count).collect()))
791}