Skip to main content

polyspec_crudui_validator/expr/
evaluator.rs

1//! CRUDUI expression evaluator (expressions.md §5/§6/§7, JS `PathResolver` parity).
2//!
3//! The shared 4-language fixture (`tests/fixtures/expr/cases.json`) is generated
4//! from the JS reference engine and is the §9 contract all four engines pass, so
5//! this evaluator mirrors validator-ts `PathResolver` function-for-function:
6//!
7//!   - [`Evaluator::evaluate`] → bool (JS `evaluateCondition`): `Boolean` of the
8//!     resolved value for Path/Literal; logical short-circuit; comparison; in;
9//!     unary; a ternary is `Boolean(its branch value)`.
10//!   - [`Evaluator::evaluate_value`] → value (JS `evaluateExpressionValue`): a
11//!     ternary returns its branch value; EVERYTHING else returns the boolean
12//!     condition result.
13//!
14//! truthy (JS `Boolean`): null/false/0/"" falsy; "0", "false", [], {}, and every
15//! non-empty value truthy. == / != use JS loose equality (same-type ===, else
16//! numeric when both `Number()`-parse, else string). > >= < <= coerce BOTH sides
17//! to number (parseFloat, NaN→0) and compare numerically — no lexicographic
18//! branch (JS `compare` parity).
19//!
20//! No eval / no regex evaluation (GRAMMAR §10) — a pure AST walk.
21
22use serde_json::{Number, Value};
23
24use crate::json_type::js_type_of;
25use super::ast::{LiteralValue, Node, PathSegment};
26
27/// A resolved value is either a single value or a wildcard-produced list of
28/// candidate values (JS `resolveValue` returns an array for unresolved
29/// wildcards; ANY/CURRENT collapse happens in [`Evaluator::evaluate_binary`] /
30/// [`Evaluator::evaluate_in`]).
31enum Resolved {
32    Single(Value),
33    List(Vec<Value>),
34}
35
36/// AST evaluator bound to a form-data tree and the current field path.
37pub struct Evaluator<'a> {
38    form_data: &'a Value,
39    current_path: &'a [String],
40    row_keys: &'a [usize],
41}
42
43/// The path that a relative path with `levels_up` further dots starts from: the
44/// current field is one level and each further dot moves one more level up, and
45/// a row key leaves together with the name of its collection (expressions.md,
46/// "Evaluation").
47pub fn relative_base(current_path: &[String], row_keys: &[usize], levels_up: usize) -> Vec<String> {
48    let mut base = current_path.to_vec();
49    for _ in 0..=levels_up {
50        if base.is_empty() {
51            break;
52        }
53        let leaving = if row_keys.contains(&(base.len() - 1)) {
54            2
55        } else {
56            1
57        };
58        base.truncate(base.len().saturating_sub(leaving));
59    }
60    base
61}
62
63impl<'a> Evaluator<'a> {
64    /// Build an evaluator over the full form data, the path of the field
65    /// carrying the condition (including the field name itself) and the
66    /// positions of the row keys in that path.
67    pub fn new(form_data: &'a Value, current_path: &'a [String], row_keys: &'a [usize]) -> Self {
68        Evaluator {
69            form_data,
70            current_path,
71            row_keys,
72        }
73    }
74
75    /// Evaluate to a boolean condition result (JS `evaluateCondition`).
76    pub fn evaluate(&self, node: &Node) -> bool {
77        match node {
78            Node::Binary { .. } => self.evaluate_binary(node),
79            Node::Unary { .. } => self.evaluate_unary(node),
80            Node::In { .. } => self.evaluate_in(node),
81            Node::Group(expr) => self.evaluate(expr),
82            Node::Ternary { .. } => is_truthy(&self.evaluate_ternary(node)),
83            Node::Path { .. } | Node::Literal(_) => {
84                is_truthy(&self.resolve_value(node).into_single())
85            }
86        }
87    }
88
89    /// Evaluate to a value (JS `evaluateExpressionValue`). A ternary returns its
90    /// chosen branch's RAW value; every other node returns its boolean condition
91    /// result (the shared fixture's `value` for non-ternary mirrors `truthy`).
92    pub fn evaluate_value(&self, node: &Node) -> Value {
93        match node {
94            Node::Ternary { .. } => self.evaluate_ternary(node),
95            Node::Group(expr) => self.evaluate_value(expr),
96            _ => Value::Bool(self.evaluate(node)),
97        }
98    }
99
100    /// Resolve a ternary branch to its RAW value (JS `resolveTernaryBranchValue`):
101    /// nested ternary recurses; literal/path return their value; logical/in/
102    /// comparison/unary return their boolean.
103    fn resolve_ternary_branch_value(&self, node: &Node) -> Value {
104        match node {
105            Node::Ternary { .. } => self.evaluate_ternary(node),
106            Node::Group(expr) => self.resolve_ternary_branch_value(expr),
107            Node::Literal(_) => self.resolve_value(node).into_single(),
108            Node::Path { .. } => self.resolve_value(node).into_single(),
109            Node::Binary { .. } => Value::Bool(self.evaluate_binary(node)),
110            Node::Unary { .. } => Value::Bool(self.evaluate_unary(node)),
111            Node::In { .. } => Value::Bool(self.evaluate_in(node)),
112        }
113    }
114
115    fn evaluate_ternary(&self, node: &Node) -> Value {
116        if let Node::Ternary {
117            condition,
118            true_value,
119            false_value,
120        } = node
121        {
122            if self.evaluate(condition) {
123                self.resolve_ternary_branch_value(true_value)
124            } else {
125                self.resolve_ternary_branch_value(false_value)
126            }
127        } else {
128            Value::Null
129        }
130    }
131
132    fn evaluate_binary(&self, node: &Node) -> bool {
133        let (operator, left, right) = match node {
134            Node::Binary {
135                operator,
136                left,
137                right,
138            } => (operator.as_str(), left, right),
139            _ => return false,
140        };
141
142        // Short-circuit logical operators.
143        if operator == "&&" {
144            return self.evaluate(left) && self.evaluate(right);
145        }
146        if operator == "||" {
147            return self.evaluate(left) || self.evaluate(right);
148        }
149
150        let left_value = self.resolve_value(left);
151        let right_value = self.resolve_value(right).into_single();
152
153        // Wildcard path may resolve to a list of candidate values: ANY match.
154        match left_value {
155            Resolved::List(items) => items
156                .iter()
157                .any(|lv| compare_op(lv, &right_value, operator)),
158            Resolved::Single(lv) => compare_op(&lv, &right_value, operator),
159        }
160    }
161
162    fn evaluate_unary(&self, node: &Node) -> bool {
163        if let Node::Unary { operator, operand } = node {
164            if operator == "!" {
165                return !self.evaluate(operand);
166            }
167        }
168        false
169    }
170
171    fn evaluate_in(&self, node: &Node) -> bool {
172        let (negated, value, list) = match node {
173            Node::In {
174                negated,
175                value,
176                list,
177            } => (*negated, value, list),
178            _ => return false,
179        };
180
181        let probe = self.resolve_value(value);
182        let candidates: Vec<Value> = list
183            .iter()
184            .map(|n| self.resolve_value(n).into_single())
185            .collect();
186
187        // Wildcard value resolved to a list: ANY element passing membership.
188        match probe {
189            Resolved::List(items) => items.iter().any(|v| {
190                let included = in_list(v, &candidates);
191                if negated {
192                    !included
193                } else {
194                    included
195                }
196            }),
197            Resolved::Single(v) => {
198                let included = in_list(&v, &candidates);
199                if negated {
200                    !included
201                } else {
202                    included
203                }
204            }
205        }
206    }
207
208    /// Resolve a node to its raw value (JS `resolveValue`): Literal → its value;
209    /// Path → the data at the resolved path (wildcards handled); Group → the
210    /// inner condition's boolean.
211    fn resolve_value(&self, node: &Node) -> Resolved {
212        match node {
213            Node::Literal(lit) => Resolved::Single(literal_to_value(lit)),
214            Node::Path { .. } => self.resolve_path(node),
215            Node::Group(expr) => Resolved::Single(Value::Bool(self.evaluate(expr))),
216            _ => Resolved::Single(Value::Null),
217        }
218    }
219
220    // --- path resolution (JS resolvePathSegments + resolveValue parity) -------
221
222    fn resolve_path(&self, node: &Node) -> Resolved {
223        let mut resolved_path = self.resolve_path_segments(node);
224
225        if has_wildcard(&resolved_path) {
226            // CURRENT strategy: replace wildcards with current array indices.
227            resolved_path = replace_wildcard_with_index(&resolved_path, self.current_path);
228
229            if has_wildcard(&resolved_path) {
230                // Still wildcarded: resolve to the list of all matching values.
231                let resolved = self.resolve_wildcard_path(&resolved_path);
232                return Resolved::List(resolved.into_iter().map(|(_, v)| v).collect());
233            }
234        }
235
236        Resolved::Single(self.get_value_by_path(&resolved_path))
237    }
238
239    /// Compute the absolute path segments for a Path node (JS resolvePathSegments).
240    fn resolve_path_segments(&self, node: &Node) -> Vec<String> {
241        let (relative, levels_up, segments) = match node {
242            Node::Path {
243                relative,
244                levels_up,
245                segments,
246            } => (*relative, *levels_up, segments),
247            _ => return Vec::new(),
248        };
249
250        let mut base_path: Vec<String> = if relative {
251            relative_base(self.current_path, self.row_keys, levels_up.max(0) as usize)
252        } else {
253            Vec::new()
254        };
255
256        for seg in segments {
257            match seg {
258                PathSegment::Identifier(v) => base_path.push(v.clone()),
259                PathSegment::Index(i) => base_path.push(i.to_string()),
260                PathSegment::Wildcard => base_path.push("*".to_string()),
261            }
262        }
263
264        base_path
265    }
266
267    /// Read the value at a concrete path (JS getValueByPath). A missing key
268    /// yields null. Numeric segments index into array values.
269    fn get_value_by_path(&self, path: &[String]) -> Value {
270        let mut current = self.form_data;
271
272        for segment in path {
273            match current {
274                Value::Object(map) => match map.get(segment) {
275                    Some(v) => current = v,
276                    None => return Value::Null,
277                },
278                Value::Array(arr) => match segment.parse::<usize>() {
279                    Ok(idx) => match arr.get(idx) {
280                        Some(v) => current = v,
281                        None => return Value::Null,
282                    },
283                    Err(_) => return Value::Null,
284                },
285                Value::Null => return Value::Null,
286                _ => return Value::Null,
287            }
288        }
289
290        current.clone()
291    }
292
293    /// Resolve a wildcard path to all concrete (path,value) pairs (JS
294    /// resolveWildcardPath, ANY strategy across the array; object source skips
295    /// the wildcard index per multiple:'only').
296    fn resolve_wildcard_path(&self, path: &[String]) -> Vec<(Vec<String>, Value)> {
297        let wildcard_index = index_of_wildcard(path);
298        if wildcard_index.is_none() {
299            return vec![(path.to_vec(), self.get_value_by_path(path))];
300        }
301        let wildcard_index = wildcard_index.unwrap();
302
303        let array_path = &path[..wildcard_index];
304        let remaining_path = &path[wildcard_index + 1..];
305        let array_data = self.get_value_by_path(array_path);
306
307        match &array_data {
308            // Object source: skip the wildcard index, access remaining directly.
309            Value::Object(_) => {
310                let concrete: Vec<String> = array_path
311                    .iter()
312                    .cloned()
313                    .chain(remaining_path.iter().cloned())
314                    .collect();
315                self.resolve_wildcard_path(&concrete)
316            }
317            Value::Array(arr) => {
318                let mut results = Vec::new();
319                for i in 0..arr.len() {
320                    let concrete: Vec<String> = array_path
321                        .iter()
322                        .cloned()
323                        .chain(std::iter::once(i.to_string()))
324                        .chain(remaining_path.iter().cloned())
325                        .collect();
326                    results.extend(self.resolve_wildcard_path(&concrete));
327                }
328                results
329            }
330            _ => Vec::new(),
331        }
332    }
333}
334
335impl Resolved {
336    fn into_single(self) -> Value {
337        match self {
338            Resolved::Single(v) => v,
339            // A wildcard list flowing into a single-value slot becomes a JSON
340            // array (JS map → array); used only on the comparison RHS, where the
341            // fixture never produces a wildcard.
342            Resolved::List(items) => Value::Array(items),
343        }
344    }
345}
346
347/// Replace each wildcard with the corresponding array index from currentPath, in
348/// order (JS replaceWildcardWithIndex). Wildcards with no matching index stay as
349/// '*' for later list resolution.
350fn replace_wildcard_with_index(path: &[String], current_path: &[String]) -> Vec<String> {
351    let array_indices: Vec<String> = current_path
352        .iter()
353        .filter(|s| is_numeric_segment(s))
354        .cloned()
355        .collect();
356
357    let mut result = Vec::with_capacity(path.len());
358    let mut cursor = 0usize;
359    for segment in path {
360        if segment == "*" {
361            if cursor < array_indices.len() {
362                result.push(array_indices[cursor].clone());
363                cursor += 1;
364            } else {
365                result.push("*".to_string());
366            }
367        } else {
368            result.push(segment.clone());
369        }
370    }
371
372    result
373}
374
375fn has_wildcard(path: &[String]) -> bool {
376    path.iter().any(|s| s == "*")
377}
378
379fn index_of_wildcard(path: &[String]) -> Option<usize> {
380    path.iter().position(|s| s == "*")
381}
382
383fn is_numeric_segment(segment: &str) -> bool {
384    !segment.is_empty() && segment.bytes().all(|b| b.is_ascii_digit())
385}
386
387fn literal_to_value(lit: &LiteralValue) -> Value {
388    match lit {
389        LiteralValue::Str(s) => Value::String(s.clone()),
390        LiteralValue::Int(n) => Value::from(*n),
391        LiteralValue::Float(f) => Value::from(*f),
392        LiteralValue::Bool(b) => Value::Bool(*b),
393        LiteralValue::Null => Value::Null,
394    }
395}
396
397fn in_list(value: &Value, list: &[Value]) -> bool {
398    list.iter().any(|item| loose_equals(value, item))
399}
400
401fn compare_op(left: &Value, right: &Value, operator: &str) -> bool {
402    match operator {
403        "==" => loose_equals(left, right),
404        "!=" => !loose_equals(left, right),
405        ">" => coerce_number(left) > coerce_number(right),
406        ">=" => coerce_number(left) >= coerce_number(right),
407        "<" => coerce_number(left) < coerce_number(right),
408        "<=" => coerce_number(left) <= coerce_number(right),
409        _ => false,
410    }
411}
412
413// --- value semantics (JS truthy / looseEquals / coerceNumber parity) ---------
414
415/// Truthy test (JS `Boolean`). null/false/0/0.0/"" are falsy; "0", "false",
416/// empty array, empty object, and every non-empty value are truthy.
417pub fn is_truthy(value: &Value) -> bool {
418    match value {
419        Value::Null => false,
420        Value::Bool(b) => *b,
421        Value::Number(n) => {
422            if let Some(f) = n.as_f64() {
423                f != 0.0
424            } else {
425                true
426            }
427        }
428        Value::String(s) => !s.is_empty(),
429        // [] and {} are both truthy in JS (Boolean of any object/array).
430        Value::Array(_) => true,
431        Value::Object(_) => true,
432    }
433}
434
435/// Loose equality (JS `looseEquals`). Same JS type compares directly; null equals
436/// only null; otherwise numeric equality when both `Number()`-parse, else string
437/// equality.
438fn loose_equals(a: &Value, b: &Value) -> bool {
439    if js_type_of(a) == js_type_of(b) {
440        return json_equals(a, b);
441    }
442
443    if a.is_null() {
444        return b.is_null();
445    }
446    if b.is_null() {
447        return false;
448    }
449
450    if let (Some(an), Some(bn)) = (js_number(a), js_number(b)) {
451        return an == bn;
452    }
453
454    to_string(a) == to_string(b)
455}
456
457/// Number equality by value (so 1 equals 1.0); numbers without an `f64` compare as written.
458pub(crate) fn numbers_equal(x: &Number, y: &Number) -> bool {
459    match (x.as_f64(), y.as_f64()) {
460        (Some(xf), Some(yf)) => xf == yf,
461        _ => x == y,
462    }
463}
464
465/// JSON-value structural equality (docs/spec/expressions.md, Evaluation): two
466/// arrays hold equal elements in the same order, two objects the same keys with
467/// equal values in any key order; other values compare without conversion,
468/// numbers by value (so 1 equals 1.0). An array never equals an object.
469fn json_equals(a: &Value, b: &Value) -> bool {
470    match (a, b) {
471        (Value::Number(x), Value::Number(y)) => numbers_equal(x, y),
472        (Value::Array(x), Value::Array(y)) => {
473            x.len() == y.len()
474                && x.iter()
475                    .zip(y)
476                    .all(|(item, other)| json_equals(item, other))
477        }
478        (Value::Object(x), Value::Object(y)) => {
479            x.len() == y.len()
480                && x.iter()
481                    .all(|(key, value)| y.get(key).is_some_and(|other| json_equals(value, other)))
482        }
483        _ => a == b,
484    }
485}
486
487/// JS `Number(value)` for equality: numbers pass; a string is numeric only when
488/// the WHOLE string parses (JS `Number('12abc')` is NaN; `Number('')` is 0);
489/// booleans → 1/0. Returns `None` for a non-numeric string.
490#[allow(clippy::disallowed_methods, reason = "a string that does not parse is NaN")]
491fn js_number(value: &Value) -> Option<f64> {
492    match value {
493        Value::Number(n) => n.as_f64(),
494        Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
495        Value::String(s) => {
496            let trimmed = s.trim();
497            if trimmed.is_empty() {
498                return Some(0.0); // JS Number('') === 0
499            }
500            trimmed.parse::<f64>().ok()
501        }
502        _ => None,
503    }
504}
505
506/// Coerce a value to a number for comparison (JS `coerceNumber`): numbers pass
507/// through; a string is parseFloat-ed (NaN→0); booleans → 1/0; else 0.
508fn coerce_number(value: &Value) -> f64 {
509    match value {
510        Value::Number(n) => n.as_f64().unwrap_or(0.0),
511        Value::String(s) => parse_float_prefix(s).unwrap_or(0.0),
512        Value::Bool(true) => 1.0,
513        Value::Bool(false) => 0.0,
514        _ => 0.0,
515    }
516}
517
518/// parseFloat-style leading-number parse (JS `parseFloat`): reads an optional
519/// sign, digits, fraction, exponent prefix; returns `None` when no number leads
520/// the string.
521fn parse_float_prefix(value: &str) -> Option<f64> {
522    let bytes = value.as_bytes();
523    let mut i = 0usize;
524
525    // Leading JS whitespace.
526    while i < bytes.len() && matches!(bytes[i], b' ' | b'\t' | b'\n' | b'\r') {
527        i += 1;
528    }
529
530    let start = i;
531
532    if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
533        i += 1;
534    }
535
536    let mut has_int_digits = false;
537    while i < bytes.len() && bytes[i].is_ascii_digit() {
538        i += 1;
539        has_int_digits = true;
540    }
541
542    let mut has_frac_digits = false;
543    if i < bytes.len() && bytes[i] == b'.' {
544        i += 1;
545        while i < bytes.len() && bytes[i].is_ascii_digit() {
546            i += 1;
547            has_frac_digits = true;
548        }
549    }
550
551    if !has_int_digits && !has_frac_digits {
552        return None;
553    }
554
555    // Optional exponent (only kept if well-formed; JS parseFloat tolerates a bare
556    // trailing 'e' by ignoring it).
557    if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
558        let exp_marker = i;
559        let mut j = i + 1;
560        if j < bytes.len() && (bytes[j] == b'+' || bytes[j] == b'-') {
561            j += 1;
562        }
563        let mut has_exp_digits = false;
564        while j < bytes.len() && bytes[j].is_ascii_digit() {
565            j += 1;
566            has_exp_digits = true;
567        }
568        if has_exp_digits {
569            i = j;
570        } else {
571            i = exp_marker;
572        }
573    }
574
575    // A sign, digits with an optional fraction and a well-formed exponent: Rust parses
576    // every such prefix, and the check above guarantees at least one digit.
577    Some(
578        value[start..i]
579            .parse::<f64>()
580            .expect("a numeric prefix with a digit parses as f64"),
581    )
582}
583
584/// JS `String(value)`: null→"", strings pass, booleans→"true"/"false", integral
585/// floats drop the fraction (so 5.0 → "5").
586fn to_string(value: &Value) -> String {
587    match value {
588        Value::Null => String::new(),
589        Value::String(s) => s.clone(),
590        Value::Bool(b) => {
591            if *b {
592                "true".to_string()
593            } else {
594                "false".to_string()
595            }
596        }
597        Value::Number(n) => {
598            if let Some(i) = n.as_i64() {
599                i.to_string()
600            } else if let Some(u) = n.as_u64() {
601                u.to_string()
602            } else if let Some(f) = n.as_f64() {
603                if f.is_finite() && f == f.floor() {
604                    (f as i64).to_string()
605                } else {
606                    f.to_string()
607                }
608            } else {
609                String::new()
610            }
611        }
612        _ => String::new(),
613    }
614}