Skip to main content

voxgig_struct/
major.rs

1// Copyright (c) 2025-2026 Voxgig Ltd. MIT LICENSE.
2// VERSION: @voxgig/struct 0.1.0
3//
4// Major utilities — walk, merge, getpath, setpath, and (staged) the
5// inject / transform / validate / select machinery. See rs/PLAN.md §8-§11.
6//
7// `merge` is implemented walk-based to stay close to the canonical; the
8// `cur`/`dst` scratch vectors are shared between the before/after closures
9// via `Rc<RefCell<…>>` (Rust can't have two FnMut closures both holding a
10// `&mut` to the same Vec).
11
12use std::cell::RefCell;
13use std::rc::Rc;
14
15use crate::consts::*;
16use crate::mini::*;
17use crate::ordered_map::OrderedMap;
18use crate::value::{js_string, js_to_int32, js_to_number, Value};
19use crate::StructError;
20
21// ---------------------------------------------------------------------
22// callback / function types
23// ---------------------------------------------------------------------
24
25/// `WalkApply` — `(key, val, parent, path) -> any`. `key` is `Noval` at the
26/// root and a `Str` for every descendant (matches the canonical, where
27/// `items` always yields string keys).
28pub type WalkClosure<'a> = dyn FnMut(&Value, &Value, &Value, &[String]) -> Value + 'a;
29
30/// `Injector` — `(inj, val, ref, store) -> any`.
31pub type NativeFn = crate::value::NativeFn;
32
33/// `Modify` — `(val, key, parent, inj, store)`.
34pub type Modify = crate::value::ModifyFn;
35
36pub type Inj = Rc<RefCell<Injection>>;
37pub type SVec = Rc<RefCell<Vec<String>>>;
38
39// ---------------------------------------------------------------------
40// walk
41// ---------------------------------------------------------------------
42
43pub fn walk(
44    val: Value,
45    before: Option<&mut WalkClosure>,
46    after: Option<&mut WalkClosure>,
47    maxdepth: Option<i64>,
48) -> Value {
49    let mut path: Vec<String> = Vec::new();
50    walk_impl(
51        val,
52        before,
53        after,
54        maxdepth,
55        &Value::Noval,
56        &Value::Noval,
57        &mut path,
58    )
59}
60
61fn walk_impl(
62    val: Value,
63    mut before: Option<&mut WalkClosure>,
64    mut after: Option<&mut WalkClosure>,
65    maxdepth: Option<i64>,
66    key: &Value,
67    parent: &Value,
68    path: &mut Vec<String>,
69) -> Value {
70    let depth = path.len() as i64;
71
72    let out = match before {
73        Some(ref mut f) => f(key, &val, parent, path),
74        None => val,
75    };
76
77    let md = match maxdepth {
78        Some(m) if m >= 0 => m,
79        _ => MAXDEPTH,
80    };
81    if md == 0 || (md > 0 && md <= depth) {
82        return out;
83    }
84
85    if is_node(&out) {
86        let entries = items_vec(&out);
87        for (ckey, child) in entries {
88            path.push(ckey.clone());
89            let res = walk_impl(
90                child,
91                before.as_deref_mut(),
92                after.as_deref_mut(),
93                maxdepth,
94                &Value::str(ckey.clone()),
95                &out,
96                path,
97            );
98            path.pop();
99            set_prop(out.clone(), &Value::str(ckey), res);
100        }
101    }
102
103    match after {
104        Some(ref mut f) => f(key, &out, parent, path),
105        None => out,
106    }
107}
108
109// ---------------------------------------------------------------------
110// merge
111// ---------------------------------------------------------------------
112
113const T_INSTANCE_I: i64 = T_INSTANCE as i64;
114
115pub fn merge(val: &Value, maxdepth: Option<i64>) -> Value {
116    let md = match slice(
117        Value::Num(maxdepth.unwrap_or(MAXDEPTH) as f64),
118        Some(0),
119        None,
120        false,
121    ) {
122        Value::Num(n) => n as i64,
123        _ => MAXDEPTH,
124    };
125
126    let list: Vec<Value> = match val {
127        Value::List(l) => l.borrow().clone(),
128        other => return other.clone(),
129    };
130
131    if list.is_empty() {
132        return Value::Noval;
133    }
134    if list.len() == 1 {
135        return list[0].clone();
136    }
137
138    let mut out = match &list[0] {
139        Value::Noval => Value::empty_map(),
140        other => other.clone(),
141    };
142
143    for obj in list.iter().skip(1) {
144        let obj = obj.clone();
145        if !is_node(&obj) {
146            out = obj;
147            continue;
148        }
149
150        let cur: Rc<RefCell<Vec<Value>>> = Rc::new(RefCell::new(vec![out.clone()]));
151        let dst: Rc<RefCell<Vec<Value>>> = Rc::new(RefCell::new(vec![out.clone()]));
152
153        let cur_b = cur.clone();
154        let dst_b = dst.clone();
155        let mut before = move |key: &Value, v: &Value, _parent: &Value, path: &[String]| -> Value {
156            let pi = path.len() as i64;
157            let mut ret = v.clone();
158
159            if md <= pi {
160                let target = cur_b
161                    .borrow()
162                    .get((pi - 1) as usize)
163                    .cloned()
164                    .unwrap_or(Value::Noval);
165                set_prop(target, key, v.clone());
166            } else if !is_node(v) {
167                let mut cb = cur_b.borrow_mut();
168                grow(&mut cb, pi as usize);
169                cb[pi as usize] = v.clone();
170            } else {
171                let tval = {
172                    let d = if pi > 0 {
173                        let prev = dst_b
174                            .borrow()
175                            .get((pi - 1) as usize)
176                            .cloned()
177                            .unwrap_or(Value::Noval);
178                        get_prop(&prev, key, Value::Noval)
179                    } else {
180                        dst_b
181                            .borrow()
182                            .get(pi as usize)
183                            .cloned()
184                            .unwrap_or(Value::Noval)
185                    };
186                    let mut db = dst_b.borrow_mut();
187                    grow(&mut db, pi as usize);
188                    db[pi as usize] = d.clone();
189                    d
190                };
191                let mut cb = cur_b.borrow_mut();
192                grow(&mut cb, pi as usize);
193                if tval.is_noval() && (typify(v) & T_INSTANCE_I) == 0 {
194                    cb[pi as usize] = if is_list(v) {
195                        Value::empty_list()
196                    } else {
197                        Value::empty_map()
198                    };
199                } else if typify(v) == typify(&tval) {
200                    cb[pi as usize] = tval;
201                } else {
202                    cb[pi as usize] = v.clone();
203                    ret = Value::Noval;
204                }
205            }
206            ret
207        };
208
209        let cur_a = cur.clone();
210        let mut after = move |key: &Value, _v: &Value, _parent: &Value, path: &[String]| -> Value {
211            let ci = path.len() as i64;
212            let (target, value) = {
213                let cb = cur_a.borrow();
214                (
215                    cb.get((ci - 1) as usize).cloned().unwrap_or(Value::Noval),
216                    cb.get(ci as usize).cloned().unwrap_or(Value::Noval),
217                )
218            };
219            set_prop(target, key, value.clone());
220            value
221        };
222
223        out = walk(obj, Some(&mut before), Some(&mut after), maxdepth);
224    }
225
226    if md == 0 {
227        let last = get_elem(&Value::list(list.clone()), &Value::Num(-1.0), Value::Noval);
228        out = match &last {
229            Value::List(_) => Value::empty_list(),
230            Value::Map(_) => Value::empty_map(),
231            other => other.clone(),
232        };
233    }
234
235    out
236}
237
238fn grow(v: &mut Vec<Value>, idx: usize) {
239    while v.len() <= idx {
240        v.push(Value::Noval);
241    }
242}
243
244// ---------------------------------------------------------------------
245// Injection state
246// ---------------------------------------------------------------------
247
248pub struct Injection {
249    pub mode: i64,
250    pub full: bool,
251    pub key_i: i64,
252    pub keys: SVec,
253    pub key: String,
254    pub val: Value,
255    pub parent: Value,
256    pub path: Vec<String>,
257    pub nodes: Vec<Value>,
258    pub handler: NativeFn,
259    pub errs: Value, // Value::List
260    pub meta: Value, // Value::Map
261    pub dparent: Value,
262    pub dpath: Vec<String>,
263    pub base: Option<String>,
264    pub modify: Option<Modify>,
265    pub extra: Option<Value>,
266    pub prior: Option<Inj>,
267}
268
269/// Public `injdef` argument — the subset of `Partial<Injection>` callers set.
270#[derive(Default, Clone)]
271pub struct InjectDef {
272    pub base: Option<String>,
273    pub errs: Option<Value>,
274    pub meta: Option<Value>,
275    pub modify: Option<Modify>,
276    pub handler: Option<NativeFn>,
277    pub extra: Option<Value>,
278    pub dparent: Option<Value>,
279    pub dpath: Option<Vec<String>>,
280    pub key: Option<Value>,
281}
282
283impl Injection {
284    /// Build a (mostly-empty) injection carrying just the fields the public
285    /// `getpath` / `setpath` read from an `injdef`.
286    pub fn from_def(def: Option<&InjectDef>) -> Inj {
287        let inj = Injection {
288            mode: M_VAL,
289            full: false,
290            key_i: 0,
291            keys: Rc::new(RefCell::new(vec![S_DTOP.to_string()])),
292            key: S_DTOP.to_string(),
293            val: Value::Noval,
294            parent: Value::Noval,
295            path: vec![S_DTOP.to_string()],
296            nodes: vec![Value::Noval],
297            handler: inject_handler_fn(),
298            errs: Value::empty_list(),
299            meta: Value::empty_map(),
300            dparent: Value::Noval,
301            dpath: vec![S_DTOP.to_string()],
302            base: None,
303            modify: None,
304            extra: None,
305            prior: None,
306        };
307        let inj = Rc::new(RefCell::new(inj));
308        if let Some(d) = def {
309            let mut b = inj.borrow_mut();
310            if let Some(x) = &d.base {
311                b.base = Some(x.clone());
312            }
313            if let Some(x) = &d.errs {
314                b.errs = x.clone();
315            }
316            if let Some(x) = &d.meta {
317                b.meta = x.clone();
318            }
319            if let Some(x) = &d.modify {
320                b.modify = Some(x.clone());
321            }
322            if let Some(x) = &d.handler {
323                b.handler = x.clone();
324            }
325            if let Some(x) = &d.extra {
326                b.extra = Some(x.clone());
327            }
328            if let Some(x) = &d.dparent {
329                b.dparent = x.clone();
330            }
331            if let Some(x) = &d.dpath {
332                b.dpath = x.clone();
333            }
334            if let Some(x) = &d.key {
335                b.key = str_key(x.clone());
336            }
337        }
338        inj
339    }
340
341    fn has_handler(def: Option<&InjectDef>) -> bool {
342        def.map(|d| d.handler.is_some()).unwrap_or(false)
343    }
344
345    /// Fresh root injection: `val` wrapped in the virtual parent holder.
346    pub fn root(val: Value, parent: Value) -> Inj {
347        let parent_clone = parent.clone();
348        Rc::new(RefCell::new(Injection {
349            mode: M_VAL,
350            full: false,
351            key_i: 0,
352            keys: Rc::new(RefCell::new(vec![S_DTOP.to_string()])),
353            key: S_DTOP.to_string(),
354            val,
355            parent,
356            path: vec![S_DTOP.to_string()],
357            nodes: vec![parent_clone],
358            handler: inject_handler_fn(),
359            errs: Value::empty_list(),
360            meta: Value::empty_map(),
361            dparent: Value::Noval,
362            dpath: vec![S_DTOP.to_string()],
363            base: Some(S_DTOP.to_string()),
364            modify: None,
365            extra: None,
366            prior: None,
367        }))
368    }
369
370    pub fn descend(this: &Inj) {
371        // meta.__d++
372        {
373            let b = this.borrow();
374            if let Value::Map(m) = &b.meta {
375                let cur = m
376                    .borrow()
377                    .get("__d")
378                    .and_then(|v| v.as_num())
379                    .unwrap_or(0.0);
380                m.borrow_mut()
381                    .insert("__d".to_string(), Value::Num(cur + 1.0));
382            }
383        }
384        let (parentkey, has_dparent, dpath_len, last_part) = {
385            let b = this.borrow();
386            let parentkey = if b.path.len() >= 2 {
387                Some(b.path[b.path.len() - 2].clone())
388            } else {
389                None
390            };
391            (
392                parentkey,
393                !b.dparent.is_noval(),
394                b.dpath.len(),
395                b.dpath.last().cloned(),
396            )
397        };
398
399        if !has_dparent {
400            if dpath_len > 1 {
401                if let Some(pk) = parentkey {
402                    this.borrow_mut().dpath.push(pk);
403                }
404            }
405        } else if let Some(pk) = parentkey {
406            let newdparent = {
407                let b = this.borrow();
408                get_prop(&b.dparent, &Value::str(pk.clone()), Value::Noval)
409            };
410            let mut b = this.borrow_mut();
411            b.dparent = newdparent;
412            if last_part.as_deref() == Some(format!("$:{pk}").as_str()) {
413                let n = b.dpath.len();
414                b.dpath.truncate(n.saturating_sub(1));
415            } else {
416                b.dpath.push(pk);
417            }
418        }
419    }
420
421    pub fn child(this: &Inj, key_i: i64, keys: SVec) -> Inj {
422        let (
423            key,
424            parent_val,
425            cval,
426            path,
427            nodes,
428            mode,
429            handler,
430            modify,
431            base,
432            meta,
433            errs,
434            dpath,
435            dparent,
436        ) = {
437            let b = this.borrow();
438            let key = str_key(
439                keys.borrow()
440                    .get(key_i.max(0) as usize)
441                    .cloned()
442                    .map(Value::Str)
443                    .unwrap_or(Value::Noval),
444            );
445            let parent_val = b.val.clone();
446            let cval = get_prop(&parent_val, &Value::str(key.clone()), Value::Noval);
447            let mut path = b.path.clone();
448            path.push(key.clone());
449            let mut nodes = b.nodes.clone();
450            nodes.push(parent_val.clone());
451            (
452                key,
453                parent_val,
454                cval,
455                path,
456                nodes,
457                b.mode,
458                b.handler.clone(),
459                b.modify.clone(),
460                b.base.clone(),
461                b.meta.clone(),
462                b.errs.clone(),
463                b.dpath.clone(),
464                b.dparent.clone(),
465            )
466        };
467        Rc::new(RefCell::new(Injection {
468            mode,
469            full: false,
470            key_i,
471            keys,
472            key,
473            val: cval,
474            parent: parent_val,
475            path,
476            nodes,
477            handler,
478            errs,
479            meta,
480            dparent,
481            dpath,
482            base,
483            modify,
484            extra: None,
485            prior: Some(Rc::clone(this)),
486        }))
487    }
488
489    pub fn setval(this: &Inj, val: Value, ancestor: Option<i64>) -> Value {
490        let anc = ancestor.unwrap_or(0);
491        if anc < 2 {
492            let (parent, key) = {
493                let b = this.borrow();
494                (b.parent.clone(), b.key.clone())
495            };
496            if val.is_noval() {
497                let np = del_prop(parent, &Value::str(key));
498                this.borrow_mut().parent = np.clone();
499                np
500            } else {
501                set_prop(parent, &Value::str(key), val)
502            }
503        } else {
504            let (aval, akey) = {
505                let b = this.borrow();
506                let n = b.nodes.len() as i64;
507                let aval = if n - anc >= 0 {
508                    b.nodes[(n - anc) as usize].clone()
509                } else {
510                    Value::Noval
511                };
512                let pn = b.path.len() as i64;
513                let akey = if pn - anc >= 0 {
514                    b.path[(pn - anc) as usize].clone()
515                } else {
516                    String::new()
517                };
518                (aval, akey)
519            };
520            if val.is_noval() {
521                del_prop(aval, &Value::str(akey))
522            } else {
523                set_prop(aval, &Value::str(akey), val)
524            }
525        }
526    }
527}
528
529// ---------------------------------------------------------------------
530// getpath
531// ---------------------------------------------------------------------
532
533pub fn get_path(store: &Value, path: &Value, injdef: Option<&InjectDef>) -> Value {
534    let inj: Option<Inj> = injdef.map(|_| Injection::from_def(injdef));
535    get_path_inj(store, path, inj.as_ref())
536}
537
538/// Internal `getpath` working against a full `Inj` (used by the inject
539/// machinery and by the public `get_path` wrapper).
540pub fn get_path_inj(store: &Value, path: &Value, injdef: Option<&Inj>) -> Value {
541    let mut parts: Vec<String> = match path {
542        Value::List(l) => l
543            .borrow()
544            .iter()
545            .map(|x| match x {
546                Value::Str(s) => s.clone(),
547                other => js_string(other),
548            })
549            .collect(),
550        Value::Str(s) => s.split('.').map(|p| p.to_string()).collect(),
551        Value::Num(n) => vec![str_key(Value::Num(*n))],
552        _ => return Value::Noval,
553    };
554
555    let base = injdef.and_then(|i| i.borrow().base.clone());
556    let src = match &base {
557        Some(b) => get_prop(store, &Value::str(b.clone()), store.clone()),
558        None => store.clone(),
559    };
560    let numparts = parts.len();
561    let dparent = injdef
562        .map(|i| i.borrow().dparent.clone())
563        .unwrap_or(Value::Noval);
564
565    let mut val = store.clone();
566
567    let path_nullish = matches!(path, Value::Noval | Value::Null);
568    if path_nullish
569        || matches!(store, Value::Noval | Value::Null)
570        || (numparts == 1 && parts[0].is_empty())
571    {
572        val = src.clone();
573    } else if numparts > 0 {
574        if numparts == 1 {
575            val = get_prop(store, &Value::str(parts[0].clone()), Value::Noval);
576        }
577
578        if !is_func(&val) {
579            val = src.clone();
580
581            // meta path prefix:  "name$=..."  /  "name$~..."
582            if let Some(inj) = injdef {
583                let meta = inj.borrow().meta.clone();
584                let first = parts[0].clone();
585                if let Some(caps) = R_META_PATH.captures(&first) {
586                    if !meta.is_noval() {
587                        val = get_prop(&meta, &Value::str(caps[1].to_string()), Value::Noval);
588                        parts[0] = caps[3].to_string();
589                    }
590                }
591            }
592
593            let dpath: Vec<String> = injdef.map(|i| i.borrow().dpath.clone()).unwrap_or_default();
594
595            let mut p_i = 0usize;
596            while !val.is_noval() && p_i < numparts {
597                let mut part = parts[p_i].clone();
598
599                if let Some(inj) = injdef {
600                    if part == S_DKEY {
601                        part = inj.borrow().key.clone();
602                    } else if let Some(rest) = part.strip_prefix("$GET:") {
603                        part = js_string(&get_path_inj(&src, &Value::str(drop_last(rest)), None));
604                    } else if let Some(rest) = part.strip_prefix("$REF:") {
605                        let spec = get_prop(store, &Value::str(S_DSPEC), Value::Noval);
606                        part = js_string(&get_path_inj(&spec, &Value::str(drop_last(rest)), None));
607                    } else if let Some(rest) = part.strip_prefix("$META:") {
608                        let meta = inj.borrow().meta.clone();
609                        part = js_string(&get_path_inj(&meta, &Value::str(drop_last(rest)), None));
610                    }
611                }
612
613                // str::replace allocates a fresh String even with no match, so
614                // skip it for the common no-`$$` segment.
615                if part.contains("$$") {
616                    part = part.replace("$$", "$");
617                }
618
619                if part.is_empty() {
620                    let mut ascends = 0i64;
621                    while parts.get(1 + p_i).map(|s| s.is_empty()).unwrap_or(false) {
622                        ascends += 1;
623                        p_i += 1;
624                    }
625
626                    if injdef.is_some() && ascends > 0 {
627                        if p_i == parts.len() - 1 {
628                            ascends -= 1;
629                        }
630                        if ascends == 0 {
631                            val = dparent.clone();
632                        } else {
633                            // fullpath = slice(dpath, -ascends) ++ parts[p_i+1..]
634                            let head = slice(
635                                Value::list(dpath.iter().cloned().map(Value::Str).collect()),
636                                Some(-ascends),
637                                None,
638                                false,
639                            );
640                            let mut fullpath: Vec<String> = match &head {
641                                Value::List(l) => l
642                                    .borrow()
643                                    .iter()
644                                    .map(|x| x.as_str().map(|s| s.to_string()).unwrap_or_default())
645                                    .collect(),
646                                _ => Vec::new(),
647                            };
648                            fullpath.extend_from_slice(&parts[p_i + 1..]);
649                            if ascends <= dpath.len() as i64 {
650                                val = get_path_inj(
651                                    store,
652                                    &Value::list(fullpath.into_iter().map(Value::Str).collect()),
653                                    None,
654                                );
655                            } else {
656                                val = Value::Noval;
657                            }
658                            break;
659                        }
660                    } else {
661                        val = dparent.clone();
662                    }
663                } else {
664                    val = get_prop(&val, &Value::str(part), Value::Noval);
665                }
666
667                p_i += 1;
668            }
669        }
670    }
671
672    if let Some(inj) = injdef {
673        let handler = inj.borrow().handler.clone();
674        let r = pathify(path, None, None);
675        val = handler(inj, &val, &r, store);
676    }
677
678    val
679}
680
681fn drop_last(s: &str) -> String {
682    let chars: Vec<char> = s.chars().collect();
683    if chars.is_empty() {
684        String::new()
685    } else {
686        chars[..chars.len() - 1].iter().collect()
687    }
688}
689
690// ---------------------------------------------------------------------
691// setpath
692// ---------------------------------------------------------------------
693
694pub fn set_path(store: &Value, path: &Value, val: Value, injdef: Option<&InjectDef>) -> Value {
695    // Keep parts as Values so a numeric part (only possible when `path` is an
696    // array) makes its parent a list, while a string part makes a map.
697    let parts: Vec<Value> = match path {
698        Value::List(l) => l.borrow().clone(),
699        Value::Str(s) => s.split('.').map(Value::str).collect(),
700        Value::Num(n) => vec![Value::Num(*n)],
701        _ => return Value::Noval,
702    };
703    if parts.is_empty() {
704        return Value::Noval;
705    }
706
707    let base = injdef.and_then(|d| d.base.clone());
708    let numparts = parts.len();
709    let mut parent = match &base {
710        Some(b) => get_prop(store, &Value::str(b.clone()), store.clone()),
711        None => store.clone(),
712    };
713
714    for p_i in 0..numparts - 1 {
715        let part_key = parts[p_i].clone();
716        let next_parent = get_prop(&parent, &part_key, Value::Noval);
717        let next_parent = if !is_node(&next_parent) {
718            let next_is_num = parts
719                .get(p_i + 1)
720                .map(|p| typify(p) & (T_NUMBER as i64) != 0)
721                .unwrap_or(false);
722            let np = if next_is_num {
723                Value::empty_list()
724            } else {
725                Value::empty_map()
726            };
727            set_prop(parent.clone(), &part_key, np.clone());
728            np
729        } else {
730            next_parent
731        };
732        parent = next_parent;
733    }
734
735    let last = parts[numparts - 1].clone();
736    if val.is_delete() {
737        del_prop(parent.clone(), &last);
738    } else {
739        set_prop(parent.clone(), &last, val);
740    }
741
742    parent
743}
744
745// ---------------------------------------------------------------------
746// inject / transform / validate / select — staged (see rs/PLAN.md, NOTES.md)
747// ---------------------------------------------------------------------
748
749/// Default inject handler (`_injecthandler`): if the value is a `$NAME`
750/// command function, call it; otherwise, in `val` mode for a full-string
751/// injection, write the value back into the parent.
752pub fn inject_handler_fn() -> NativeFn {
753    Rc::new(inject_handler)
754}
755
756fn inject_handler(inj: &Inj, val: &Value, r: &str, store: &Value) -> Value {
757    let iscmd = is_func(val) && (r.is_empty() || r.starts_with('$'));
758    if iscmd {
759        if let Value::Func(f) = val {
760            return f(inj, val, r, store);
761        }
762    }
763    let (mode, full) = {
764        let b = inj.borrow();
765        (b.mode, b.full)
766    };
767    if mode == M_VAL && full {
768        Injection::setval(inj, val.clone(), None);
769    }
770    val.clone()
771}
772
773/// `_injectstr` — substitute `` `path` `` references inside a string.
774fn injectstr(val: &str, store: &Value, inj: Option<&Inj>) -> Value {
775    if val.is_empty() {
776        return Value::str("");
777    }
778
779    if let Some(caps) = R_INJECTION_FULL.captures(val) {
780        if let Some(i) = inj {
781            i.borrow_mut().full = true;
782        }
783        let mut pathref = caps[1].to_string();
784        if pathref.chars().count() > 3 {
785            pathref = pathref.replace("$BT", S_BT).replace("$DS", S_DS);
786        }
787        return get_path_inj(store, &Value::str(pathref), inj);
788    }
789
790    // partial injection: replace each `ref` occurrence
791    let out_str = R_INJECTION_PARTIAL
792        .replace_all(val, |caps: &crate::re::Captures<'_>| -> String {
793            let mut r = caps[1].to_string();
794            if r.chars().count() > 3 {
795                r = r.replace("$BT", S_BT).replace("$DS", S_DS);
796            }
797            if let Some(i) = inj {
798                i.borrow_mut().full = false;
799            }
800            let found = get_path_inj(store, &Value::str(r), inj);
801            match &found {
802                Value::Noval => String::new(),
803                Value::Str(s) => s.clone(),
804                other => jsonify(
805                    other,
806                    Some(&JsonFlags {
807                        indent: 0,
808                        offset: 0,
809                    }),
810                ),
811            }
812        })
813        .to_string();
814
815    if let Some(i) = inj {
816        let handler = {
817            i.borrow_mut().full = true;
818            i.borrow().handler.clone()
819        };
820        return handler(i, &Value::str(out_str), val, store);
821    }
822    Value::str(out_str)
823}
824
825pub fn inject(val: Value, store: &Value, injdef: Option<&InjectDef>) -> Value {
826    let inj = make_root_injection(val.clone(), store, injdef);
827    inject_inj(val, store, &inj)
828}
829
830/// Build the root injection for a top-level `inject` (mirrors the TS setup
831/// block when `injdef.mode == null`).
832fn make_root_injection(val: Value, store: &Value, injdef: Option<&InjectDef>) -> Inj {
833    let mut top = OrderedMap::new();
834    top.insert(S_DTOP.to_string(), val.clone());
835    let inj = Injection::root(val, Value::map(top));
836    {
837        let mut b = inj.borrow_mut();
838        b.dparent = store.clone();
839        let store_errs = get_prop(store, &Value::str(S_DERRS), Value::Noval);
840        if !store_errs.is_noval() {
841            b.errs = store_errs;
842        }
843        if let Value::Map(m) = &b.meta {
844            m.borrow_mut().insert("__d".to_string(), Value::Num(0.0));
845        }
846        if let Some(d) = injdef {
847            if let Some(x) = &d.modify {
848                b.modify = Some(x.clone());
849            }
850            if let Some(x) = &d.extra {
851                b.extra = Some(x.clone());
852            }
853            if let Some(x) = &d.meta {
854                b.meta = x.clone();
855            }
856            if let Some(x) = &d.handler {
857                b.handler = x.clone();
858            }
859        }
860    }
861    inj
862}
863
864/// Recursive `inject` working against an existing injection.
865// `nk_i` is re-read after every child phase to match the canonical loop
866// (an injector in the M_VAL phase, e.g. $REF, may change `key_i`).
867#[allow(unused_assignments)]
868fn inject_inj(mut val: Value, store: &Value, inj: &Inj) -> Value {
869    Injection::descend(inj);
870
871    if is_node(&val) {
872        // node keys: sorted, then `$`-bearing keys last (for maps).
873        let nodekeys: SVec = {
874            let ks = keysof_vec(&val);
875            if is_map(&val) {
876                let (mut plain, dollar): (Vec<String>, Vec<String>) =
877                    ks.into_iter().partition(|k| !k.contains(S_DS));
878                plain.extend(dollar);
879                Rc::new(RefCell::new(plain))
880            } else {
881                Rc::new(RefCell::new(ks))
882            }
883        };
884
885        let mut nk_i: i64 = 0;
886        loop {
887            if nk_i < 0 {
888                nk_i += 1;
889                continue;
890            }
891            if nk_i as usize >= nodekeys.borrow().len() {
892                break;
893            }
894
895            let childinj = Injection::child(inj, nk_i, nodekeys.clone());
896            let nodekey = childinj.borrow().key.clone();
897            childinj.borrow_mut().mode = M_KEYPRE;
898
899            let prekey = injectstr(&nodekey, store, Some(&childinj));
900            nk_i = childinj.borrow().key_i;
901            // (keys may have been replaced by an injector — `nodekeys` itself
902            // is the shared Rc, so re-reading is implicit.)
903
904            if !prekey.is_noval() {
905                let cval = get_prop(&val, &prekey, Value::Noval);
906                {
907                    let mut b = childinj.borrow_mut();
908                    b.val = cval.clone();
909                    b.mode = M_VAL;
910                }
911                inject_inj(cval, store, &childinj);
912                nk_i = childinj.borrow().key_i;
913
914                childinj.borrow_mut().mode = M_KEYPOST;
915                injectstr(&nodekey, store, Some(&childinj));
916                nk_i = childinj.borrow().key_i;
917            }
918
919            nk_i += 1;
920        }
921    } else if let Value::Str(s) = val.clone() {
922        inj.borrow_mut().mode = M_VAL;
923        let r = injectstr(&s, store, Some(inj));
924        val = r.clone();
925        if !val.is_skip() {
926            Injection::setval(inj, val.clone(), None);
927        }
928    }
929
930    // custom modification
931    let modify = inj.borrow().modify.clone();
932    if let Some(m) = modify {
933        if !val.is_skip() {
934            let (mkey, mparent) = {
935                let b = inj.borrow();
936                (b.key.clone(), b.parent.clone())
937            };
938            let mval = get_prop(&mparent, &Value::str(mkey.clone()), Value::Noval);
939            m(&mval, &Value::str(mkey), &mparent, inj, store);
940        }
941    }
942
943    inj.borrow_mut().val = val;
944    let parent = inj.borrow().parent.clone();
945    // Read back the root via raw lookup (canonical TS `_lookup(inj.parent,
946    // S_DTOP)`) so a null result is preserved, not dropped by Group A.
947    lookup(&parent, &Value::str(S_DTOP))
948}
949
950// ---- transform commands ----------------------------------------------
951
952const T_ANY_I: i64 = T_ANY as i64;
953
954fn errs_push(inj: &Inj, msg: String) {
955    let errs = inj.borrow().errs.clone();
956    if let Value::List(l) = &errs {
957        l.borrow_mut().push(Value::Str(msg));
958    }
959}
960
961fn placement_str(mode: i64) -> &'static str {
962    match mode {
963        M_VAL => "value",
964        M_KEYPRE | M_KEYPOST => "key",
965        _ => "",
966    }
967}
968
969pub fn check_placement(modes: i64, ijname: &str, parent_types: i64, inj: &Inj) -> bool {
970    let (mode, parent) = {
971        let b = inj.borrow();
972        (b.mode, b.parent.clone())
973    };
974    if modes & mode == 0 {
975        let expected: Vec<&str> = [M_KEYPRE, M_KEYPOST, M_VAL]
976            .iter()
977            .filter(|m| modes & **m != 0)
978            .map(|m| placement_str(*m))
979            .collect();
980        errs_push(
981            inj,
982            format!(
983                "${ijname}: invalid placement as {}, expected: {}.",
984                placement_str(mode),
985                expected.join(",")
986            ),
987        );
988        return false;
989    }
990    if !is_empty(&Value::Num(parent_types as f64)) {
991        let ptype = typify(&parent);
992        if parent_types & ptype == 0 {
993            errs_push(
994                inj,
995                format!(
996                    "${ijname}: invalid placement in parent {}, expected: {}.",
997                    type_name(ptype),
998                    type_name(parent_types)
999                ),
1000            );
1001            return false;
1002        }
1003    }
1004    true
1005}
1006
1007pub fn injector_args(arg_types: &[i64], args: &[Value]) -> Vec<Value> {
1008    let mut found: Vec<Value> = Vec::with_capacity(1 + arg_types.len());
1009    found.push(Value::Noval);
1010    for (i, at) in arg_types.iter().enumerate() {
1011        let arg = args.get(i).cloned().unwrap_or(Value::Noval);
1012        let argtype = typify(&arg);
1013        if at & argtype == 0 {
1014            found[0] = Value::str(format!(
1015                "invalid argument: {} ({} at position {}) is not of type: {}.",
1016                stringify(&arg, Some(22), false),
1017                type_name(argtype),
1018                1 + i,
1019                type_name(*at)
1020            ));
1021            return found;
1022        }
1023        found.push(arg);
1024    }
1025    found
1026}
1027
1028pub fn inject_child(child: Value, store: &Value, inj: &Inj) -> Inj {
1029    let prior = inj.borrow().prior.clone();
1030    let cinj: Inj = match prior {
1031        None => Rc::clone(inj),
1032        Some(p) => {
1033            let pprior = p.borrow().prior.clone();
1034            match pprior {
1035                Some(pp) => {
1036                    let (pki, pkeys, pkey) = {
1037                        let b = p.borrow();
1038                        (b.key_i, b.keys.clone(), b.key.clone())
1039                    };
1040                    let c = Injection::child(&pp, pki, pkeys);
1041                    c.borrow_mut().val = child.clone();
1042                    let cparent = c.borrow().parent.clone();
1043                    set_prop(cparent, &Value::str(pkey), child.clone());
1044                    c
1045                }
1046                None => {
1047                    let (ki, keys, key) = {
1048                        let b = inj.borrow();
1049                        (b.key_i, b.keys.clone(), b.key.clone())
1050                    };
1051                    let c = Injection::child(&p, ki, keys);
1052                    c.borrow_mut().val = child.clone();
1053                    let cparent = c.borrow().parent.clone();
1054                    set_prop(cparent, &Value::str(key), child.clone());
1055                    c
1056                }
1057            }
1058        }
1059    };
1060    let _ = inject_inj(child, store, &cinj);
1061    cinj
1062}
1063
1064const FORMATTER_NAMES: [&str; 7] = [
1065    "identity", "upper", "lower", "string", "number", "integer", "concat",
1066];
1067
1068fn apply_formatter(name: &str, k: &Value, v: &Value) -> Value {
1069    match name {
1070        "identity" => v.clone(),
1071        "upper" => {
1072            if is_node(v) {
1073                v.clone()
1074            } else {
1075                Value::str(js_string(v).to_uppercase())
1076            }
1077        }
1078        "lower" => {
1079            if is_node(v) {
1080                v.clone()
1081            } else {
1082                Value::str(js_string(v).to_lowercase())
1083            }
1084        }
1085        "string" => {
1086            if is_node(v) {
1087                v.clone()
1088            } else {
1089                Value::str(js_string(v))
1090            }
1091        }
1092        "number" => {
1093            if is_node(v) {
1094                v.clone()
1095            } else {
1096                let n = js_to_number(v);
1097                Value::Num(if n.is_nan() { 0.0 } else { n })
1098            }
1099        }
1100        "integer" => {
1101            if is_node(v) {
1102                v.clone()
1103            } else {
1104                let n = js_to_number(v);
1105                let n = if n.is_nan() { 0.0 } else { n };
1106                Value::Num(js_to_int32(n) as f64)
1107            }
1108        }
1109        "concat" => {
1110            if k.is_noval() && is_list(v) {
1111                Value::str(
1112                    items_vec(v)
1113                        .iter()
1114                        .map(|(_, n)| {
1115                            if is_node(n) {
1116                                String::new()
1117                            } else {
1118                                js_string(n)
1119                            }
1120                        })
1121                        .collect::<Vec<_>>()
1122                        .join(""),
1123                )
1124            } else {
1125                v.clone()
1126            }
1127        }
1128        _ => v.clone(),
1129    }
1130}
1131
1132// `$FORMAT` — render a templated value through a named (or supplied) formatter.
1133fn transform_format(inj: &Inj, _val: &Value, _r: &str, store: &Value) -> Value {
1134    inj.borrow().keys.borrow_mut().truncate(1);
1135    if inj.borrow().mode != M_VAL {
1136        return Value::Noval;
1137    }
1138    let (parent, path, nodes) = {
1139        let b = inj.borrow();
1140        (b.parent.clone(), b.path.clone(), b.nodes.clone())
1141    };
1142    let name = lookup(&parent, &Value::Num(1.0));
1143    let child = lookup(&parent, &Value::Num(2.0));
1144    let tkey = path
1145        .get(path.len().saturating_sub(2))
1146        .cloned()
1147        .unwrap_or_default();
1148    let nlen = nodes.len();
1149    let target = if nlen >= 2 {
1150        nodes[nlen - 2].clone()
1151    } else if nlen >= 1 {
1152        nodes[nlen - 1].clone()
1153    } else {
1154        Value::Noval
1155    };
1156
1157    let cinj = inject_child(child, store, inj);
1158    let resolved = cinj.borrow().val.clone();
1159
1160    let fname: Option<String> = name
1161        .as_str()
1162        .filter(|n| FORMATTER_NAMES.contains(n))
1163        .map(|s| s.to_string());
1164    if fname.is_none() && !is_func(&name) {
1165        errs_push(
1166            inj,
1167            format!("$FORMAT: unknown format: {}.", js_string(&name)),
1168        );
1169        return Value::Noval;
1170    }
1171
1172    let out = if let Some(fn_name) = &fname {
1173        let mut fmt = |k: &Value, v: &Value, _p: &Value, _t: &[String]| -> Value {
1174            apply_formatter(fn_name, k, v)
1175        };
1176        walk(resolved, Some(&mut fmt), None, None)
1177    } else if let Value::Func(f) = &name {
1178        let f = f.clone();
1179        let mut fmt =
1180            |_k: &Value, v: &Value, _p: &Value, _t: &[String]| -> Value { f(inj, v, "", store) };
1181        walk(resolved, Some(&mut fmt), None, None)
1182    } else {
1183        resolved
1184    };
1185
1186    set_prop(target, &Value::str(tkey), out.clone());
1187    out
1188}
1189
1190// `$APPLY` — call a function (from the spec args) on the resolved child.
1191fn transform_apply(inj: &Inj, _val: &Value, _r: &str, store: &Value) -> Value {
1192    if !check_placement(M_VAL, "APPLY", T_LIST as i64, inj) {
1193        return Value::Noval;
1194    }
1195    let (parent, path, nodes) = {
1196        let b = inj.borrow();
1197        (b.parent.clone(), b.path.clone(), b.nodes.clone())
1198    };
1199    let args: Vec<Value> = slice(parent.clone(), Some(1), None, false)
1200        .as_list()
1201        .map(|l| l.borrow().clone())
1202        .unwrap_or_default();
1203    let ia = injector_args(&[T_FUNCTION as i64, T_ANY as i64], &args);
1204    if let Value::Str(e) = &ia[0] {
1205        errs_push(inj, format!("$APPLY: {e}"));
1206        return Value::Noval;
1207    }
1208    let apply = ia.get(1).cloned().unwrap_or(Value::Noval);
1209    let child = ia.get(2).cloned().unwrap_or(Value::Noval);
1210    let tkey = path
1211        .get(path.len().saturating_sub(2))
1212        .cloned()
1213        .unwrap_or_default();
1214    let nlen = nodes.len();
1215    let target = if nlen >= 2 {
1216        nodes[nlen - 2].clone()
1217    } else if nlen >= 1 {
1218        nodes[nlen - 1].clone()
1219    } else {
1220        Value::Noval
1221    };
1222    let cinj = inject_child(child, store, inj);
1223    let resolved = cinj.borrow().val.clone();
1224    // The corpus only exercises the error paths; if `apply` is a callable, do
1225    // a best-effort call (the canonical passes (resolved, store, cinj)).
1226    let out = if let Value::Func(f) = &apply {
1227        f(&cinj, &resolved, "", store)
1228    } else {
1229        resolved
1230    };
1231    set_prop(target, &Value::str(tkey), out.clone());
1232    out
1233}
1234
1235fn transform_delete(inj: &Inj, _v: &Value, _r: &str, _store: &Value) -> Value {
1236    Injection::setval(inj, Value::Noval, None);
1237    Value::Noval
1238}
1239
1240fn transform_copy(inj: &Inj, _v: &Value, _r: &str, _store: &Value) -> Value {
1241    if !check_placement(M_VAL, "COPY", T_ANY_I, inj) {
1242        return Value::Noval;
1243    }
1244    let (dparent, key) = {
1245        let b = inj.borrow();
1246        (b.dparent.clone(), b.key.clone())
1247    };
1248    // Group B: preserve a stored null (canonical `_lookup(inj.dparent, inj.key)`).
1249    let out = lookup(&dparent, &Value::str(key));
1250    Injection::setval(inj, out.clone(), None);
1251    out
1252}
1253
1254fn transform_key(inj: &Inj, _v: &Value, _r: &str, _store: &Value) -> Value {
1255    let (mode, parent, dparent, anno_key) = {
1256        let b = inj.borrow();
1257        let anno_key = b.path.get(b.path.len().saturating_sub(2)).cloned();
1258        (b.mode, b.parent.clone(), b.dparent.clone(), anno_key)
1259    };
1260    if mode != M_VAL {
1261        return Value::Noval;
1262    }
1263    // Literal presence of the $KEY meta (canonical `_lookup(parent, S_BKEY)`):
1264    // a null keyspec still counts as declared, so use the raw lookup.
1265    let keyspec = lookup(&parent, &Value::str(S_BKEY));
1266    if !keyspec.is_noval() {
1267        del_prop(parent.clone(), &Value::str(S_BKEY));
1268        return get_prop(&dparent, &keyspec, Value::Noval);
1269    }
1270    let anno = get_prop(&parent, &Value::str(S_BANNO), Value::Noval);
1271    get_prop(
1272        &anno,
1273        &Value::str(S_KEY),
1274        anno_key.map(Value::Str).unwrap_or(Value::Noval),
1275    )
1276}
1277
1278fn transform_anno(inj: &Inj, _v: &Value, _r: &str, _store: &Value) -> Value {
1279    let parent = inj.borrow().parent.clone();
1280    del_prop(parent, &Value::str(S_BANNO));
1281    Value::Noval
1282}
1283
1284fn transform_merge(inj: &Inj, _v: &Value, _r: &str, _store: &Value) -> Value {
1285    let (mode, key, parent) = {
1286        let b = inj.borrow();
1287        (b.mode, b.key.clone(), b.parent.clone())
1288    };
1289    let mut out = Value::Noval;
1290    if mode == M_KEYPRE {
1291        out = Value::str(key);
1292    } else if mode == M_KEYPOST {
1293        out = Value::str(key.clone());
1294        let mut args = get_prop(&parent, &Value::str(key.clone()), Value::Noval);
1295        if !is_list(&args) {
1296            args = Value::list(vec![args]);
1297        }
1298        Injection::setval(inj, Value::Noval, None); // remove $MERGE key from parent
1299        let args_vec: Vec<Value> = args
1300            .as_list()
1301            .map(|l| l.borrow().clone())
1302            .unwrap_or_default();
1303        let mut mergelist: Vec<Value> = vec![parent.clone()];
1304        mergelist.extend(args_vec);
1305        mergelist.push(clone(&parent));
1306        merge(&Value::list(mergelist), Some(1));
1307    }
1308    out
1309}
1310
1311fn slice_str_vec(v: &[String], start: Option<i64>, end: Option<i64>) -> Vec<String> {
1312    match slice(path_value(v), start, end, false) {
1313        Value::List(l) => l
1314            .borrow()
1315            .iter()
1316            .map(|x| x.as_str().map(|s| s.to_string()).unwrap_or_default())
1317            .collect(),
1318        _ => Vec::new(),
1319    }
1320}
1321
1322// `$REF` — reference the original spec (enables recursive transforms).
1323fn transform_ref(inj: &Inj, val: &Value, _r: &str, store: &Value) -> Value {
1324    let (mode, parent, path, nodes) = {
1325        let b = inj.borrow();
1326        (b.mode, b.parent.clone(), b.path.clone(), b.nodes.clone())
1327    };
1328    if mode != M_VAL {
1329        return Value::Noval;
1330    }
1331    // Group B raw read of the ref arg (canonical `_lookup(inj.parent, 1)`).
1332    let refpath = lookup(&parent, &Value::Num(1.0));
1333    {
1334        let keylen = inj.borrow().keys.borrow().len() as i64;
1335        inj.borrow_mut().key_i = keylen;
1336    }
1337    // spec = ($SPEC)()
1338    let spec = {
1339        let sf = get_prop(store, &Value::str(S_DSPEC), Value::Noval);
1340        match &sf {
1341            Value::Func(f) => f(inj, &Value::Noval, "", store),
1342            _ => Value::Noval,
1343        }
1344    };
1345    let dpath = slice_str_vec(&path, Some(1), None);
1346    let dparent_for_ref = get_path_inj(&spec, &path_value(&dpath), None);
1347    let ref_def = InjectDef {
1348        dpath: Some(dpath.clone()),
1349        dparent: Some(dparent_for_ref),
1350        ..Default::default()
1351    };
1352    let refval = get_path(&spec, &refpath, Some(&ref_def));
1353
1354    let mut has_sub_ref = false;
1355    if is_node(&refval) {
1356        let mut probe = |_k: &Value, v: &Value, _p: &Value, _t: &[String]| -> Value {
1357            if matches!(v, Value::Str(s) if s == "`$REF`") {
1358                has_sub_ref = true;
1359            }
1360            v.clone()
1361        };
1362        walk(refval.clone(), Some(&mut probe), None, None);
1363    }
1364
1365    let tref = clone(&refval);
1366    let cpath = slice_str_vec(&path, Some(-3), None);
1367    let tpath = slice_str_vec(&path, Some(-1), None);
1368    let tcur = get_path_inj(store, &path_value(&cpath), None);
1369    let tval_at = get_path_inj(store, &path_value(&tpath), None);
1370
1371    let rval = if !has_sub_ref || !tval_at.is_noval() {
1372        let tinj = Injection::child(
1373            inj,
1374            0,
1375            Rc::new(RefCell::new(vec![tpath
1376                .last()
1377                .cloned()
1378                .unwrap_or_default()])),
1379        );
1380        {
1381            let mut b = tinj.borrow_mut();
1382            b.path = tpath.clone();
1383            let nlen = nodes.len();
1384            b.nodes = if nlen >= 1 {
1385                nodes[..nlen - 1].to_vec()
1386            } else {
1387                Vec::new()
1388            };
1389            b.parent = if nlen >= 2 {
1390                nodes[nlen - 2].clone()
1391            } else {
1392                Value::Noval
1393            };
1394            b.val = tref.clone();
1395            b.dpath = cpath.clone();
1396            b.dparent = tcur.clone();
1397        }
1398        let _ = inject_inj(tref.clone(), store, &tinj);
1399        let v = tinj.borrow().val.clone();
1400        v
1401    } else {
1402        Value::Noval
1403    };
1404
1405    let grandparent = Injection::setval(inj, rval, Some(2));
1406    if is_list(&grandparent) {
1407        let prior = inj.borrow().prior.clone();
1408        if let Some(p) = prior {
1409            p.borrow_mut().key_i -= 1;
1410        }
1411    }
1412    val.clone()
1413}
1414
1415fn srcpath_split(srcpath: &str) -> Vec<String> {
1416    srcpath.split('.').map(|s| s.to_string()).collect()
1417}
1418
1419// `$EACH` — apply a child template to every entry of a list or map.
1420// Spec form (a list): ['`$EACH`', 'source-path', child-template]
1421fn transform_each(inj: &Inj, _val: &Value, _r: &str, store: &Value) -> Value {
1422    if !check_placement(M_VAL, "EACH", T_LIST as i64, inj) {
1423        return Value::Noval;
1424    }
1425    // remove remaining keys to avoid spurious processing
1426    inj.borrow().keys.borrow_mut().truncate(1);
1427
1428    let (parent, path, nodes, base) = {
1429        let b = inj.borrow();
1430        (
1431            b.parent.clone(),
1432            b.path.clone(),
1433            b.nodes.clone(),
1434            b.base.clone(),
1435        )
1436    };
1437    let args: Vec<Value> = slice(parent.clone(), Some(1), None, false)
1438        .as_list()
1439        .map(|l| l.borrow().clone())
1440        .unwrap_or_default();
1441    let ia = injector_args(&[T_STRING as i64, T_ANY as i64], &args);
1442    if let Value::Str(e) = &ia[0] {
1443        errs_push(inj, format!("$EACH: {e}"));
1444        return Value::Noval;
1445    }
1446    let srcpath = ia.get(1).cloned().unwrap_or(Value::Noval);
1447    let child = ia.get(2).cloned().unwrap_or(Value::Noval);
1448    let srcpath_str = srcpath.as_str().unwrap_or("").to_string();
1449
1450    let srcstore = get_prop(
1451        store,
1452        &Value::str(base.clone().unwrap_or_default()),
1453        store.clone(),
1454    );
1455    let src = get_path_inj(&srcstore, &srcpath, Some(inj));
1456    let srctype = typify(&src);
1457
1458    let tkey = path
1459        .get(path.len().saturating_sub(2))
1460        .cloned()
1461        .unwrap_or_default();
1462    let nlen = nodes.len();
1463    let target = if nlen >= 2 {
1464        nodes[nlen - 2].clone()
1465    } else if nlen >= 1 {
1466        nodes[nlen - 1].clone()
1467    } else {
1468        Value::Noval
1469    };
1470
1471    let tval: Vec<Value> = if srctype & (T_LIST as i64) != 0 {
1472        items_vec(&src).iter().map(|_| clone(&child)).collect()
1473    } else if srctype & (T_MAP as i64) != 0 {
1474        items_vec(&src)
1475            .iter()
1476            .map(|(k, _)| {
1477                merge(
1478                    &Value::list(vec![
1479                        clone(&child),
1480                        Value::map_of([(
1481                            S_BANNO.to_string(),
1482                            Value::map_of([(S_KEY.to_string(), Value::str(k.clone()))]),
1483                        )]),
1484                    ]),
1485                    Some(1),
1486                )
1487            })
1488            .collect()
1489    } else {
1490        Vec::new()
1491    };
1492
1493    let mut rval = Value::empty_list();
1494    if !tval.is_empty() {
1495        let tcur_inner: Value = if src.is_nullish() {
1496            Value::Noval
1497        } else {
1498            Value::list(items_vec(&src).into_iter().map(|(_, v)| v).collect())
1499        };
1500        let ckey = path
1501            .get(path.len().saturating_sub(2))
1502            .cloned()
1503            .unwrap_or_default();
1504        let tpath = slice_str_vec(&path, Some(-1), None);
1505        let mut dpath: Vec<String> = vec![S_DTOP.to_string()];
1506        dpath.extend(srcpath_split(&srcpath_str));
1507        dpath.push(format!("$:{ckey}"));
1508
1509        let mut tcur = Value::map_of([(ckey.clone(), tcur_inner)]);
1510        if tpath.len() > 1 {
1511            let pkey = path
1512                .get(path.len().saturating_sub(3))
1513                .cloned()
1514                .unwrap_or_else(|| S_DTOP.to_string());
1515            tcur = Value::map_of([(pkey.clone(), tcur)]);
1516            dpath.push(format!("$:{pkey}"));
1517        }
1518
1519        let tval_v = Value::list(tval);
1520        let tinj = Injection::child(inj, 0, Rc::new(RefCell::new(vec![ckey.clone()])));
1521        {
1522            let mut b = tinj.borrow_mut();
1523            b.path = tpath;
1524            b.nodes = if nlen >= 1 {
1525                nodes[..nlen - 1].to_vec()
1526            } else {
1527                Vec::new()
1528            };
1529            b.parent = b.nodes.last().cloned().unwrap_or(Value::Noval);
1530            b.val = tval_v.clone();
1531            b.dpath = dpath;
1532            b.dparent = tcur;
1533        }
1534        let pclone = tinj.borrow().parent.clone();
1535        set_prop(pclone, &Value::str(ckey), tval_v.clone());
1536        let _ = inject_inj(tval_v.clone(), store, &tinj);
1537        rval = tinj.borrow().val.clone();
1538    }
1539
1540    set_prop(target, &Value::str(tkey), rval.clone());
1541    get_prop(&rval, &Value::Num(0.0), Value::Noval)
1542}
1543
1544// `$PACK` — repack a list/map into a map keyed by `$KEY`.
1545// Spec form (a map): { '`$PACK`': ['source-path', child-template] }
1546fn transform_pack(inj: &Inj, _val: &Value, _r: &str, store: &Value) -> Value {
1547    if !check_placement(M_KEYPRE, "EACH", T_MAP as i64, inj) {
1548        return Value::Noval;
1549    }
1550    let (key, parent, path, nodes, base) = {
1551        let b = inj.borrow();
1552        (
1553            b.key.clone(),
1554            b.parent.clone(),
1555            b.path.clone(),
1556            b.nodes.clone(),
1557            b.base.clone(),
1558        )
1559    };
1560    let args = get_prop(&parent, &Value::str(key), Value::Noval);
1561    let args_vec: Vec<Value> = args
1562        .as_list()
1563        .map(|l| l.borrow().clone())
1564        .unwrap_or_default();
1565    let ia = injector_args(&[T_STRING as i64, T_ANY as i64], &args_vec);
1566    if let Value::Str(e) = &ia[0] {
1567        errs_push(inj, format!("$EACH: {e}"));
1568        return Value::Noval;
1569    }
1570    let srcpath = ia.get(1).cloned().unwrap_or(Value::Noval);
1571    let origchildspec = ia.get(2).cloned().unwrap_or(Value::Noval);
1572    let srcpath_str = srcpath.as_str().unwrap_or("").to_string();
1573
1574    let tkey = path
1575        .get(path.len().saturating_sub(2))
1576        .cloned()
1577        .unwrap_or_default();
1578    let pathsize = path.len();
1579    let nlen = nodes.len();
1580    let target = if pathsize >= 2 {
1581        nodes.get(pathsize - 2).cloned().unwrap_or(Value::Noval)
1582    } else {
1583        nodes
1584            .get(pathsize.saturating_sub(1))
1585            .cloned()
1586            .unwrap_or(Value::Noval)
1587    };
1588    let target = if target.is_noval() {
1589        nodes
1590            .get(pathsize.saturating_sub(1))
1591            .cloned()
1592            .unwrap_or(Value::Noval)
1593    } else {
1594        target
1595    };
1596
1597    let srcstore = get_prop(
1598        store,
1599        &Value::str(base.clone().unwrap_or_default()),
1600        store.clone(),
1601    );
1602    let src_raw = get_path_inj(&srcstore, &srcpath, Some(inj));
1603    let src: Value = if is_list(&src_raw) {
1604        src_raw
1605    } else if is_map(&src_raw) {
1606        Value::list(
1607            items_vec(&src_raw)
1608                .into_iter()
1609                .map(|(k, v)| {
1610                    set_prop(
1611                        v.clone(),
1612                        &Value::str(S_BANNO),
1613                        Value::map_of([(S_KEY.to_string(), Value::str(k))]),
1614                    );
1615                    v
1616                })
1617                .collect(),
1618        )
1619    } else {
1620        return Value::Noval;
1621    };
1622    if src.is_nullish() {
1623        return Value::Noval;
1624    }
1625
1626    let keypath = get_prop(&origchildspec, &Value::str(S_BKEY), Value::Noval);
1627    let childspec = del_prop(origchildspec.clone(), &Value::str(S_BKEY));
1628    let child = get_prop(&childspec, &Value::str(S_BVAL), childspec.clone());
1629
1630    let resolve_key = |srckey: &str, srcnode: &Value| -> Value {
1631        if keypath.is_noval() {
1632            Value::str(srckey)
1633        } else if let Value::Str(kp) = &keypath {
1634            if kp.starts_with('`') {
1635                let m = merge(
1636                    &Value::list(vec![
1637                        Value::empty_map(),
1638                        store.clone(),
1639                        Value::map_of([(S_DTOP.to_string(), srcnode.clone())]),
1640                    ]),
1641                    Some(1),
1642                );
1643                inject(Value::str(kp.clone()), &m, None)
1644            } else {
1645                get_path_inj(srcnode, &Value::str(kp.clone()), Some(inj))
1646            }
1647        } else {
1648            Value::str(srckey)
1649        }
1650    };
1651
1652    let tval = Value::empty_map();
1653    for (srckey, srcnode) in items_vec(&src) {
1654        let k = resolve_key(&srckey, &srcnode);
1655        let tchild = clone(&child);
1656        set_prop(tval.clone(), &k, tchild.clone());
1657        let anno = get_prop(&srcnode, &Value::str(S_BANNO), Value::Noval);
1658        if anno.is_noval() {
1659            del_prop(tchild, &Value::str(S_BANNO));
1660        } else {
1661            set_prop(tchild, &Value::str(S_BANNO), anno);
1662        }
1663    }
1664
1665    let mut rval = Value::empty_map();
1666    if !is_empty(&tval) {
1667        let tsrc = Value::empty_map();
1668        for (i, (_, n)) in items_vec(&src).into_iter().enumerate() {
1669            let kn = if keypath.is_noval() {
1670                Value::Num(i as f64)
1671            } else {
1672                resolve_key("", &n)
1673            };
1674            set_prop(tsrc.clone(), &kn, n);
1675        }
1676        let tpath = slice_str_vec(&path, Some(-1), None);
1677        let ckey = path
1678            .get(path.len().saturating_sub(2))
1679            .cloned()
1680            .unwrap_or_default();
1681        let mut dpath: Vec<String> = vec![S_DTOP.to_string()];
1682        dpath.extend(srcpath_split(&srcpath_str));
1683        dpath.push(format!("$:{ckey}"));
1684        let mut tcur = Value::map_of([(ckey.clone(), tsrc)]);
1685        if tpath.len() > 1 {
1686            let pkey = path
1687                .get(path.len().saturating_sub(3))
1688                .cloned()
1689                .unwrap_or_else(|| S_DTOP.to_string());
1690            tcur = Value::map_of([(pkey.clone(), tcur)]);
1691            dpath.push(format!("$:{pkey}"));
1692        }
1693        let tinj = Injection::child(inj, 0, Rc::new(RefCell::new(vec![ckey.clone()])));
1694        {
1695            let mut b = tinj.borrow_mut();
1696            b.path = tpath;
1697            b.nodes = if nlen >= 1 {
1698                nodes[..nlen - 1].to_vec()
1699            } else {
1700                Vec::new()
1701            };
1702            b.parent = b.nodes.last().cloned().unwrap_or(Value::Noval);
1703            b.val = tval.clone();
1704            b.dpath = dpath;
1705            b.dparent = tcur;
1706        }
1707        let _ = inject_inj(tval.clone(), store, &tinj);
1708        rval = tinj.borrow().val.clone();
1709    }
1710
1711    set_prop(target, &Value::str(tkey), rval);
1712    Value::Noval
1713}
1714
1715pub fn transform(
1716    data: &Value,
1717    spec: &Value,
1718    injdef: Option<&InjectDef>,
1719) -> Result<Value, StructError> {
1720    let origspec = spec.clone();
1721    let spec_clone = clone(&origspec);
1722
1723    let extra = injdef.and_then(|d| d.extra.clone());
1724    let collect = injdef.map(|d| d.errs.is_some()).unwrap_or(false);
1725    let errs = injdef
1726        .and_then(|d| d.errs.clone())
1727        .unwrap_or_else(Value::empty_list);
1728
1729    // split `extra` into data-extras (non-$) and transform-extras ($)
1730    let mut extra_transforms = OrderedMap::new();
1731    let extra_data: Value = match &extra {
1732        None => Value::Noval,
1733        Some(e) => {
1734            let mut a = OrderedMap::new();
1735            for (k, v) in items_vec(e) {
1736                if k.starts_with(S_DS) {
1737                    extra_transforms.insert(k, v);
1738                } else {
1739                    a.insert(k, v);
1740                }
1741            }
1742            Value::map(a)
1743        }
1744    };
1745
1746    let data_clone = merge(
1747        &Value::list(vec![
1748            if is_empty(&extra_data) {
1749                Value::Noval
1750            } else {
1751                clone(&extra_data)
1752            },
1753            clone(data),
1754        ]),
1755        None,
1756    );
1757
1758    // build the transform store
1759    let origspec_for_thunk = origspec.clone();
1760    let mut store_base: OrderedMap<Value> = OrderedMap::new();
1761    store_base.insert(S_DTOP.to_string(), data_clone);
1762    store_base.insert(
1763        "$SPEC".to_string(),
1764        Value::func(move |_i: &Inj, _v: &Value, _r: &str, _s: &Value| origspec_for_thunk.clone()),
1765    );
1766    store_base.insert(
1767        "$BT".to_string(),
1768        Value::func(|_i: &Inj, _v: &Value, _r: &str, _s: &Value| Value::str(S_BT)),
1769    );
1770    store_base.insert(
1771        "$DS".to_string(),
1772        Value::func(|_i: &Inj, _v: &Value, _r: &str, _s: &Value| Value::str(S_DS)),
1773    );
1774    store_base.insert(
1775        "$WHEN".to_string(),
1776        Value::func(|_i: &Inj, _v: &Value, _r: &str, _s: &Value| Value::str(iso_now())),
1777    );
1778    store_base.insert("$DELETE".to_string(), Value::func(transform_delete));
1779    store_base.insert("$COPY".to_string(), Value::func(transform_copy));
1780    store_base.insert("$KEY".to_string(), Value::func(transform_key));
1781    store_base.insert("$ANNO".to_string(), Value::func(transform_anno));
1782    store_base.insert("$MERGE".to_string(), Value::func(transform_merge));
1783    store_base.insert("$EACH".to_string(), Value::func(transform_each));
1784    store_base.insert("$PACK".to_string(), Value::func(transform_pack));
1785    store_base.insert("$REF".to_string(), Value::func(transform_ref));
1786    store_base.insert("$FORMAT".to_string(), Value::func(transform_format));
1787    store_base.insert("$APPLY".to_string(), Value::func(transform_apply));
1788
1789    let store = merge(
1790        &Value::list(vec![
1791            Value::map(store_base),
1792            Value::map(extra_transforms),
1793            Value::map_of([(S_DERRS.to_string(), errs.clone())]),
1794        ]),
1795        Some(1),
1796    );
1797
1798    let out = inject(spec_clone, &store, injdef);
1799
1800    let errlen = errs.as_list().map(|l| l.borrow().len()).unwrap_or(0);
1801    if errlen > 0 && !collect {
1802        let msgs: Vec<String> = errs
1803            .as_list()
1804            .map(|l| l.borrow().iter().map(js_string).collect())
1805            .unwrap_or_default();
1806        return Err(StructError {
1807            message: msgs.join(" | "),
1808        });
1809    }
1810
1811    Ok(out)
1812}
1813
1814fn iso_now() -> String {
1815    // best-effort ISO-8601 UTC string; the corpus can't assert the exact
1816    // value (it changes), so granularity to the second is fine.
1817    use std::time::{SystemTime, UNIX_EPOCH};
1818    let dur = SystemTime::now()
1819        .duration_since(UNIX_EPOCH)
1820        .unwrap_or_default();
1821    let secs = dur.as_secs() as i64;
1822    let millis = dur.subsec_millis();
1823    // days since 1970-01-01
1824    let days = secs.div_euclid(86_400);
1825    let tod = secs.rem_euclid(86_400);
1826    let (h, m, s) = (tod / 3600, (tod % 3600) / 60, tod % 60);
1827    let (y, mo, d) = civil_from_days(days);
1828    format!("{y:04}-{mo:02}-{d:02}T{h:02}:{m:02}:{s:02}.{millis:03}Z")
1829}
1830
1831fn civil_from_days(z: i64) -> (i64, i64, i64) {
1832    // Howard Hinnant's algorithm.
1833    let z = z + 719_468;
1834    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
1835    let doe = z - era * 146_097;
1836    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
1837    let y = yoe + era * 400;
1838    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
1839    let mp = (5 * doy + 2) / 153;
1840    let d = doy - (153 * mp + 2) / 5 + 1;
1841    let m = if mp < 10 { mp + 3 } else { mp - 9 };
1842    (if m <= 2 { y + 1 } else { y }, m, d)
1843}
1844
1845// ---- validate --------------------------------------------------------
1846
1847fn path_value(path: &[String]) -> Value {
1848    Value::list(path.iter().cloned().map(Value::Str).collect())
1849}
1850
1851fn invalid_type_msg(path: &[String], needtype: &str, vt: i64, v: &Value) -> String {
1852    let vs = if v.is_nullish() {
1853        "no value".to_string()
1854    } else {
1855        stringify(v, None, false)
1856    };
1857    let field_part = if path.len() > 1 {
1858        format!("field {} to be ", pathify(&path_value(path), Some(1), None))
1859    } else {
1860        String::new()
1861    };
1862    let type_part = if !v.is_nullish() {
1863        format!("{}{}", type_name(vt), S_VIZ)
1864    } else {
1865        String::new()
1866    };
1867    format!("Expected {field_part}{needtype}, but found {type_part}{vs}.")
1868}
1869
1870fn validate_string(inj: &Inj, _v: &Value, _r: &str, _store: &Value) -> Value {
1871    let (dparent, key, path) = {
1872        let b = inj.borrow();
1873        (b.dparent.clone(), b.key.clone(), b.path.clone())
1874    };
1875    let out = lookup(&dparent, &Value::str(key));
1876    let t = typify(&out);
1877    if t & (T_STRING as i64) == 0 {
1878        errs_push(inj, invalid_type_msg(&path, "string", t, &out));
1879        return Value::Noval;
1880    }
1881    if matches!(&out, Value::Str(s) if s.is_empty()) {
1882        errs_push(
1883            inj,
1884            format!(
1885                "Empty string at {}",
1886                pathify(&path_value(&path), Some(1), None)
1887            ),
1888        );
1889        return Value::Noval;
1890    }
1891    out
1892}
1893
1894fn validate_type(inj: &Inj, _v: &Value, r: &str, _store: &Value) -> Value {
1895    let tname: String = r.chars().skip(1).collect::<String>().to_lowercase();
1896    let typev: i64 = match TYPENAME.iter().position(|x| *x == tname) {
1897        Some(idx) => 1i64 << (31 - idx as i64),
1898        None => 0,
1899    };
1900    let (dparent, key, path) = {
1901        let b = inj.borrow();
1902        (b.dparent.clone(), b.key.clone(), b.path.clone())
1903    };
1904    let out = lookup(&dparent, &Value::str(key));
1905    let t = typify(&out);
1906    if t & typev == 0 {
1907        errs_push(inj, invalid_type_msg(&path, &tname, t, &out));
1908        return Value::Noval;
1909    }
1910    out
1911}
1912
1913fn validate_any(inj: &Inj, _v: &Value, _r: &str, _store: &Value) -> Value {
1914    let (dparent, key) = {
1915        let b = inj.borrow();
1916        (b.dparent.clone(), b.key.clone())
1917    };
1918    lookup(&dparent, &Value::str(key))
1919}
1920
1921/// Render a list of tvals as `"a, b, c"`, lowering `` `$NAME` `` -> `name`.
1922fn tvals_desc(tvals: &[Value]) -> String {
1923    let joined = tvals
1924        .iter()
1925        .map(|v| stringify(v, None, false))
1926        .collect::<Vec<_>>()
1927        .join(", ");
1928    R_TRANSFORM_NAME
1929        .replace_all(&joined, |caps: &crate::re::Captures<'_>| {
1930            caps[1].to_lowercase()
1931        })
1932        .to_string()
1933}
1934
1935// Map / list `$CHILD`: apply a child template to every direct child.
1936fn validate_child(inj: &Inj, _v: &Value, _r: &str, _store: &Value) -> Value {
1937    let (mode, key, parent, path, dparent) = {
1938        let b = inj.borrow();
1939        (
1940            b.mode,
1941            b.key.clone(),
1942            b.parent.clone(),
1943            b.path.clone(),
1944            b.dparent.clone(),
1945        )
1946    };
1947
1948    if mode == M_KEYPRE {
1949        let childtm = get_prop(&parent, &Value::str(key), Value::Noval);
1950        let pkey = path
1951            .get(path.len().saturating_sub(2))
1952            .cloned()
1953            .unwrap_or_default();
1954        let mut tval = get_prop(&dparent, &Value::str(pkey), Value::Noval);
1955        if tval.is_noval() {
1956            tval = Value::empty_map();
1957        } else if !is_map(&tval) {
1958            errs_push(
1959                inj,
1960                invalid_type_msg(
1961                    &path[..path.len().saturating_sub(1)],
1962                    S_object,
1963                    typify(&tval),
1964                    &tval,
1965                ),
1966            );
1967            return Value::Noval;
1968        }
1969        let ckeys = keysof_vec(&tval);
1970        for ck in ckeys {
1971            set_prop(parent.clone(), &Value::str(ck.clone()), clone(&childtm));
1972            inj.borrow().keys.borrow_mut().push(ck);
1973        }
1974        Injection::setval(inj, Value::Noval, None);
1975        return Value::Noval;
1976    }
1977
1978    if mode == M_VAL {
1979        if !is_list(&parent) {
1980            errs_push(inj, "Invalid $CHILD as value".to_string());
1981            return Value::Noval;
1982        }
1983        // List $CHILD template (canonical `_lookup(parent, 1)`): preserve a
1984        // null template literally rather than dropping it as Group A would.
1985        let childtm = lookup(&parent, &Value::Num(1.0));
1986        if dparent.is_noval() {
1987            slice(parent.clone(), Some(0), Some(0), true); // empty default
1988            return Value::Noval;
1989        }
1990        if !is_list(&dparent) {
1991            errs_push(
1992                inj,
1993                invalid_type_msg(
1994                    &path[..path.len().saturating_sub(1)],
1995                    S_list,
1996                    typify(&dparent),
1997                    &dparent,
1998                ),
1999            );
2000            let plen = parent.as_list().map(|l| l.borrow().len()).unwrap_or(0) as i64;
2001            inj.borrow_mut().key_i = plen;
2002            return dparent;
2003        }
2004        let dlen = dparent.as_list().map(|l| l.borrow().len()).unwrap_or(0);
2005        for n in 0..dlen {
2006            set_prop(parent.clone(), &Value::Num(n as f64), clone(&childtm));
2007        }
2008        slice(parent.clone(), Some(0), Some(dlen as i64), true);
2009
2010        // NOTE: modifying inj! This extends the child value loop in inject
2011        // to cover every cloned child.
2012        {
2013            let keys = inj.borrow().keys.clone();
2014            let plen = parent.as_list().map(|l| l.borrow().len()).unwrap_or(0);
2015            let mut ks = keys.borrow_mut();
2016            for ckey_i in ks.len()..plen {
2017                ks.push(str_key(Value::Num(ckey_i as f64)));
2018            }
2019        }
2020
2021        // Restart the child value loop at the first element (the loop
2022        // increments key_i on resume) so that the first element is also
2023        // validated against the child template.
2024        inj.borrow_mut().key_i = -1;
2025
2026        // SKIP leaves the cloned child template in place at the first
2027        // element so the resumed loop can validate it.
2028        return Value::skip();
2029    }
2030
2031    Value::Noval
2032}
2033
2034// `$ONE`: value must match exactly one of a list of alternative sub-specs.
2035fn validate_one(inj: &Inj, _v: &Value, _r: &str, store: &Value) -> Value {
2036    let (mode, parent, key_i) = {
2037        let b = inj.borrow();
2038        (b.mode, b.parent.clone(), b.key_i)
2039    };
2040    if mode != M_VAL {
2041        return Value::Noval;
2042    }
2043    if !is_list(&parent) || key_i != 0 {
2044        let path = inj.borrow().path.clone();
2045        errs_push(
2046            inj,
2047            format!(
2048                "The $ONE validator at field {} must be the first element of an array.",
2049                pathify(&path_value(&path), Some(1), Some(1))
2050            ),
2051        );
2052        return Value::Noval;
2053    }
2054    let keylen = inj.borrow().keys.borrow().len() as i64;
2055    inj.borrow_mut().key_i = keylen;
2056    let dparent = inj.borrow().dparent.clone();
2057    Injection::setval(inj, dparent.clone(), Some(2));
2058    {
2059        let mut b = inj.borrow_mut();
2060        let n = b.path.len();
2061        b.path.truncate(n.saturating_sub(1));
2062        b.key = b.path.last().cloned().unwrap_or_default();
2063    }
2064    let path_after = inj.borrow().path.clone();
2065    let meta = inj.borrow().meta.clone();
2066    let tvals: Vec<Value> = slice(parent.clone(), Some(1), None, false)
2067        .as_list()
2068        .map(|l| l.borrow().clone())
2069        .unwrap_or_default();
2070    if tvals.is_empty() {
2071        errs_push(
2072            inj,
2073            format!(
2074                "The $ONE validator at field {} must have at least one argument.",
2075                pathify(&path_value(&path_after), Some(1), Some(1))
2076            ),
2077        );
2078        return Value::Noval;
2079    }
2080    for tval in &tvals {
2081        let terrs = Value::empty_list();
2082        let mut vstore = match merge(
2083            &Value::list(vec![Value::empty_map(), store.clone()]),
2084            Some(1),
2085        ) {
2086            v @ Value::Map(_) => v,
2087            _ => Value::empty_map(),
2088        };
2089        set_prop(vstore.clone(), &Value::str(S_DTOP), dparent.clone());
2090        let _ = &mut vstore;
2091        let vd = InjectDef {
2092            extra: Some(vstore.clone()),
2093            errs: Some(terrs.clone()),
2094            meta: Some(meta.clone()),
2095            ..Default::default()
2096        };
2097        let vcur = validate(&dparent, tval, Some(&vd)).unwrap_or(Value::Noval);
2098        Injection::setval(inj, vcur, Some(-2)); // hmm: ancestor -2 -> handled below
2099        let terrlen = terrs.as_list().map(|l| l.borrow().len()).unwrap_or(0);
2100        if terrlen == 0 {
2101            return Value::Noval;
2102        }
2103    }
2104    let valdesc = tvals_desc(&tvals);
2105    errs_push(
2106        inj,
2107        invalid_type_msg(
2108            &path_after,
2109            &format!(
2110                "{}{}",
2111                if tvals.len() > 1 { "one of " } else { "" },
2112                valdesc
2113            ),
2114            typify(&dparent),
2115            &dparent,
2116        ),
2117    );
2118    Value::Noval
2119}
2120
2121// `$EXACT`: value must equal a literal exactly (no shape coercion).
2122fn validate_exact(inj: &Inj, _v: &Value, _r: &str, _store: &Value) -> Value {
2123    let (mode, parent, key, key_i) = {
2124        let b = inj.borrow();
2125        (b.mode, b.parent.clone(), b.key.clone(), b.key_i)
2126    };
2127    if mode != M_VAL {
2128        del_prop(parent, &Value::str(key));
2129        return Value::Noval;
2130    }
2131    if !is_list(&parent) || key_i != 0 {
2132        let path = inj.borrow().path.clone();
2133        errs_push(
2134            inj,
2135            format!(
2136                "The $EXACT validator at field {} must be the first element of an array.",
2137                pathify(&path_value(&path), Some(1), Some(1))
2138            ),
2139        );
2140        return Value::Noval;
2141    }
2142    let keylen = inj.borrow().keys.borrow().len() as i64;
2143    inj.borrow_mut().key_i = keylen;
2144    let dparent = inj.borrow().dparent.clone();
2145    Injection::setval(inj, dparent.clone(), Some(2));
2146    {
2147        let mut b = inj.borrow_mut();
2148        let n = b.path.len();
2149        b.path.truncate(n.saturating_sub(1));
2150        b.key = b.path.last().cloned().unwrap_or_default();
2151    }
2152    let path_after = inj.borrow().path.clone();
2153    let tvals: Vec<Value> = slice(parent.clone(), Some(1), None, false)
2154        .as_list()
2155        .map(|l| l.borrow().clone())
2156        .unwrap_or_default();
2157    if tvals.is_empty() {
2158        errs_push(
2159            inj,
2160            format!(
2161                "The $EXACT validator at field {} must have at least one argument.",
2162                pathify(&path_value(&path_after), Some(1), Some(1))
2163            ),
2164        );
2165        return Value::Noval;
2166    }
2167    let mut currentstr: Option<String> = None;
2168    for tval in &tvals {
2169        let mut exactmatch = tval == &dparent;
2170        if !exactmatch && is_node(tval) {
2171            let cs = currentstr
2172                .get_or_insert_with(|| stringify(&dparent, None, false))
2173                .clone();
2174            exactmatch = stringify(tval, None, false) == cs;
2175        }
2176        if exactmatch {
2177            return Value::Noval;
2178        }
2179    }
2180    let valdesc = tvals_desc(&tvals);
2181    let need = format!(
2182        "{}exactly equal to {}{}",
2183        if path_after.len() > 1 { "" } else { "value " },
2184        if tvals.len() == 1 { "" } else { "one of " },
2185        valdesc
2186    );
2187    errs_push(
2188        inj,
2189        invalid_type_msg(&path_after, &need, typify(&dparent), &dparent),
2190    );
2191    Value::Noval
2192}
2193
2194/// `_validation` — the modify hook installed by `validate` (runs after the
2195/// per-key special commands).
2196fn validation_modify(pval: &Value, key: &Value, parent: &Value, inj: &Inj, _store: &Value) {
2197    if pval.is_skip() {
2198        return;
2199    }
2200    let (meta, dparent, path) = {
2201        let b = inj.borrow();
2202        (b.meta.clone(), b.dparent.clone(), b.path.clone())
2203    };
2204    let exact = matches!(
2205        get_prop(&meta, &Value::str(S_BEXACT), Value::Bool(false)),
2206        Value::Bool(true)
2207    );
2208    let cval = get_prop(&dparent, key, Value::Noval);
2209    if !exact && cval.is_noval() {
2210        return;
2211    }
2212    let ptype = typify(pval);
2213    if ptype & (T_STRING as i64) != 0 {
2214        if let Value::Str(s) = pval {
2215            if s.contains(S_DS) {
2216                return; // remaining special command — leave it
2217            }
2218        }
2219    }
2220    let ctype = typify(&cval);
2221    if ptype != ctype && !pval.is_noval() {
2222        errs_push(
2223            inj,
2224            invalid_type_msg(&path, &type_name(ptype), ctype, &cval),
2225        );
2226        return;
2227    }
2228
2229    if is_map(&cval) {
2230        if !is_map(pval) {
2231            errs_push(
2232                inj,
2233                invalid_type_msg(&path, &type_name(ptype), ctype, &cval),
2234            );
2235            return;
2236        }
2237        let ckeys = keysof_vec(&cval);
2238        let pkeys = keysof_vec(pval);
2239        let open = matches!(
2240            get_prop(pval, &Value::str(S_BOPEN), Value::Noval),
2241            Value::Bool(true)
2242        );
2243        if !pkeys.is_empty() && !open {
2244            // Literal presence: the shape must be checked with a raw lookup
2245            // (canonical TS `NONE === _lookup(pval, ckey)`); the Group A
2246            // has_key would treat a null-valued shape slot as absent.
2247            let badkeys: Vec<String> = ckeys
2248                .iter()
2249                .filter(|ck| lookup(pval, &Value::str((*ck).clone())).is_noval())
2250                .cloned()
2251                .collect();
2252            if !badkeys.is_empty() {
2253                errs_push(
2254                    inj,
2255                    format!(
2256                        "Unexpected keys at field {}{}{}",
2257                        pathify(&path_value(&path), Some(1), None),
2258                        S_VIZ,
2259                        badkeys.join(", ")
2260                    ),
2261                );
2262            }
2263        } else {
2264            merge(&Value::list(vec![pval.clone(), cval.clone()]), None);
2265            if is_node(pval) {
2266                del_prop(pval.clone(), &Value::str(S_BOPEN));
2267            }
2268        }
2269    } else if is_list(&cval) {
2270        if !is_list(pval) {
2271            errs_push(
2272                inj,
2273                invalid_type_msg(&path, &type_name(ptype), ctype, &cval),
2274            );
2275        }
2276    } else if exact {
2277        if &cval != pval {
2278            let pathmsg = if path.len() > 1 {
2279                format!(
2280                    "at field {}{}",
2281                    pathify(&path_value(&path), Some(1), None),
2282                    S_VIZ
2283                )
2284            } else {
2285                String::new()
2286            };
2287            errs_push(
2288                inj,
2289                format!(
2290                    "Value {}{} should equal {}{}",
2291                    pathmsg,
2292                    js_string(&cval),
2293                    js_string(pval),
2294                    S_DT
2295                ),
2296            );
2297        }
2298    } else {
2299        set_prop(parent.clone(), key, cval.clone());
2300    }
2301}
2302
2303/// `_validatehandler` — `getpath`/`_injectstr` handler installed by `validate`.
2304fn validatehandler(inj: &Inj, val: &Value, r: &str, store: &Value) -> Value {
2305    if let Some(caps) = R_META_PATH.captures(r) {
2306        if &caps[2] == "=" {
2307            Injection::setval(
2308                inj,
2309                Value::list(vec![Value::str(S_BEXACT), val.clone()]),
2310                None,
2311            );
2312        } else {
2313            Injection::setval(inj, val.clone(), None);
2314        }
2315        inj.borrow_mut().key_i = -1;
2316        return Value::skip();
2317    }
2318    inject_handler(inj, val, r, store)
2319}
2320
2321pub fn validate(
2322    data: &Value,
2323    spec: &Value,
2324    injdef: Option<&InjectDef>,
2325) -> Result<Value, StructError> {
2326    let extra = injdef.and_then(|d| d.extra.clone());
2327    let collect = injdef.map(|d| d.errs.is_some()).unwrap_or(false);
2328    let errs = injdef
2329        .and_then(|d| d.errs.clone())
2330        .unwrap_or_else(Value::empty_list);
2331
2332    // build the validator store
2333    let mut vmap: OrderedMap<Value> = OrderedMap::new();
2334    for k in [
2335        "$DELETE", "$COPY", "$KEY", "$META", "$MERGE", "$EACH", "$PACK",
2336    ] {
2337        vmap.insert(k.to_string(), Value::Null);
2338    }
2339    vmap.insert("$STRING".to_string(), Value::func(validate_string));
2340    for k in [
2341        "$NUMBER",
2342        "$INTEGER",
2343        "$DECIMAL",
2344        "$BOOLEAN",
2345        "$NULL",
2346        "$NIL",
2347        "$MAP",
2348        "$LIST",
2349        "$FUNCTION",
2350        "$INSTANCE",
2351    ] {
2352        vmap.insert(k.to_string(), Value::func(validate_type));
2353    }
2354    vmap.insert("$ANY".to_string(), Value::func(validate_any));
2355    vmap.insert("$CHILD".to_string(), Value::func(validate_child));
2356    vmap.insert("$ONE".to_string(), Value::func(validate_one));
2357    vmap.insert("$EXACT".to_string(), Value::func(validate_exact));
2358
2359    let extra_or_empty = match &extra {
2360        Some(e) => e.clone(),
2361        None => Value::empty_map(),
2362    };
2363    let store = merge(
2364        &Value::list(vec![
2365            Value::map(vmap),
2366            extra_or_empty,
2367            Value::map_of([(S_DERRS.to_string(), errs.clone())]),
2368        ]),
2369        Some(1),
2370    );
2371
2372    let meta = match injdef.and_then(|d| d.meta.clone()) {
2373        Some(m) => m,
2374        None => Value::empty_map(),
2375    };
2376    let exact_cur = get_prop(&meta, &Value::str(S_BEXACT), Value::Bool(false));
2377    set_prop(meta.clone(), &Value::str(S_BEXACT), exact_cur);
2378
2379    let td = InjectDef {
2380        meta: Some(meta),
2381        extra: Some(store),
2382        modify: Some(Rc::new(validation_modify) as Modify),
2383        handler: Some(Rc::new(validatehandler) as NativeFn),
2384        errs: Some(errs.clone()),
2385        ..Default::default()
2386    };
2387
2388    let out = transform(data, spec, Some(&td)).unwrap_or(Value::Noval);
2389
2390    let errlen = errs.as_list().map(|l| l.borrow().len()).unwrap_or(0);
2391    if errlen > 0 && !collect {
2392        let msgs: Vec<String> = errs
2393            .as_list()
2394            .map(|l| l.borrow().iter().map(js_string).collect())
2395            .unwrap_or_default();
2396        return Err(StructError {
2397            message: msgs.join(" | "),
2398        });
2399    }
2400
2401    Ok(out)
2402}
2403
2404// ---- select ----------------------------------------------------------
2405
2406fn js_lt(a: &Value, b: &Value) -> bool {
2407    match (a, b) {
2408        (Value::Str(x), Value::Str(y)) => x < y,
2409        _ => {
2410            let (x, y) = (js_to_number(a), js_to_number(b));
2411            x < y // NaN -> false, matches JS
2412        }
2413    }
2414}
2415fn js_gt(a: &Value, b: &Value) -> bool {
2416    match (a, b) {
2417        (Value::Str(x), Value::Str(y)) => x > y,
2418        _ => {
2419            let (x, y) = (js_to_number(a), js_to_number(b));
2420            x > y
2421        }
2422    }
2423}
2424
2425fn select_subvalidate(point: &Value, term: &Value, store: &Value, meta: &Value) -> bool {
2426    let vstore = match merge(
2427        &Value::list(vec![Value::empty_map(), store.clone()]),
2428        Some(1),
2429    ) {
2430        v @ Value::Map(_) => v,
2431        _ => Value::empty_map(),
2432    };
2433    set_prop(vstore.clone(), &Value::str(S_DTOP), point.clone());
2434    let terrs = Value::empty_list();
2435    let vd = InjectDef {
2436        extra: Some(vstore),
2437        errs: Some(terrs.clone()),
2438        meta: Some(meta.clone()),
2439        ..Default::default()
2440    };
2441    let _ = validate(point, term, Some(&vd));
2442    terrs
2443        .as_list()
2444        .map(|l| l.borrow().is_empty())
2445        .unwrap_or(true)
2446}
2447
2448fn select_and(inj: &Inj, _v: &Value, _r: &str, store: &Value) -> Value {
2449    if inj.borrow().mode != M_KEYPRE {
2450        return Value::Noval;
2451    }
2452    let (key, parent, path, nodes, meta) = {
2453        let b = inj.borrow();
2454        (
2455            b.key.clone(),
2456            b.parent.clone(),
2457            b.path.clone(),
2458            b.nodes.clone(),
2459            b.meta.clone(),
2460        )
2461    };
2462    let terms: Vec<Value> = lookup(&parent, &Value::str(key))
2463        .as_list()
2464        .map(|l| l.borrow().clone())
2465        .unwrap_or_default();
2466    let ppath = slice_str_vec(&path, Some(-1), None);
2467    let point = get_path_inj(store, &path_value(&ppath), None);
2468    for term in &terms {
2469        if !select_subvalidate(&point, term, store, &meta) {
2470            errs_push(
2471                inj,
2472                format!(
2473                    "AND:{}{}{} fail:{}",
2474                    pathify(&path_value(&ppath), None, None),
2475                    S_VIZ,
2476                    stringify(&point, None, false),
2477                    stringify(&Value::list(terms.clone()), None, false)
2478                ),
2479            );
2480        }
2481    }
2482    let gkey = path
2483        .get(path.len().saturating_sub(2))
2484        .cloned()
2485        .unwrap_or_default();
2486    let nlen = nodes.len();
2487    if nlen >= 2 {
2488        set_prop(nodes[nlen - 2].clone(), &Value::str(gkey), point);
2489    }
2490    Value::Noval
2491}
2492
2493fn select_or(inj: &Inj, _v: &Value, _r: &str, store: &Value) -> Value {
2494    if inj.borrow().mode != M_KEYPRE {
2495        return Value::Noval;
2496    }
2497    let (key, parent, path, nodes, meta) = {
2498        let b = inj.borrow();
2499        (
2500            b.key.clone(),
2501            b.parent.clone(),
2502            b.path.clone(),
2503            b.nodes.clone(),
2504            b.meta.clone(),
2505        )
2506    };
2507    let terms: Vec<Value> = lookup(&parent, &Value::str(key))
2508        .as_list()
2509        .map(|l| l.borrow().clone())
2510        .unwrap_or_default();
2511    let ppath = slice_str_vec(&path, Some(-1), None);
2512    let point = get_path_inj(store, &path_value(&ppath), None);
2513    for term in &terms {
2514        if select_subvalidate(&point, term, store, &meta) {
2515            let gkey = path
2516                .get(path.len().saturating_sub(2))
2517                .cloned()
2518                .unwrap_or_default();
2519            let nlen = nodes.len();
2520            if nlen >= 2 {
2521                set_prop(nodes[nlen - 2].clone(), &Value::str(gkey), point);
2522            }
2523            return Value::Noval;
2524        }
2525    }
2526    errs_push(
2527        inj,
2528        format!(
2529            "OR:{}{}{} fail:{}",
2530            pathify(&path_value(&ppath), None, None),
2531            S_VIZ,
2532            stringify(&point, None, false),
2533            stringify(&Value::list(terms.clone()), None, false)
2534        ),
2535    );
2536    Value::Noval
2537}
2538
2539fn select_not(inj: &Inj, _v: &Value, _r: &str, store: &Value) -> Value {
2540    if inj.borrow().mode != M_KEYPRE {
2541        return Value::Noval;
2542    }
2543    let (key, parent, path, nodes, meta) = {
2544        let b = inj.borrow();
2545        (
2546            b.key.clone(),
2547            b.parent.clone(),
2548            b.path.clone(),
2549            b.nodes.clone(),
2550            b.meta.clone(),
2551        )
2552    };
2553    let term = lookup(&parent, &Value::str(key));
2554    let ppath = slice_str_vec(&path, Some(-1), None);
2555    let point = get_path_inj(store, &path_value(&ppath), None);
2556    if select_subvalidate(&point, &term, store, &meta) {
2557        errs_push(
2558            inj,
2559            format!(
2560                "NOT:{}{}{} fail:{}",
2561                pathify(&path_value(&ppath), None, None),
2562                S_VIZ,
2563                stringify(&point, None, false),
2564                stringify(&term, None, false)
2565            ),
2566        );
2567    }
2568    let gkey = path
2569        .get(path.len().saturating_sub(2))
2570        .cloned()
2571        .unwrap_or_default();
2572    let nlen = nodes.len();
2573    if nlen >= 2 {
2574        set_prop(nodes[nlen - 2].clone(), &Value::str(gkey), point);
2575    }
2576    Value::Noval
2577}
2578
2579fn select_cmp(inj: &Inj, _v: &Value, r: &str, store: &Value) -> Value {
2580    if inj.borrow().mode != M_KEYPRE {
2581        return Value::Noval;
2582    }
2583    let (key, parent, path, nodes) = {
2584        let b = inj.borrow();
2585        (
2586            b.key.clone(),
2587            b.parent.clone(),
2588            b.path.clone(),
2589            b.nodes.clone(),
2590        )
2591    };
2592    let term = lookup(&parent, &Value::str(key));
2593    let gkey = path
2594        .get(path.len().saturating_sub(2))
2595        .cloned()
2596        .unwrap_or_default();
2597    let ppath = slice_str_vec(&path, Some(-1), None);
2598    let point = get_path_inj(store, &path_value(&ppath), None);
2599
2600    let pass = match r {
2601        "$GT" => js_gt(&point, &term),
2602        "$LT" => js_lt(&point, &term),
2603        "$GTE" => js_gt(&point, &term) || point == term,
2604        "$LTE" => js_lt(&point, &term) || point == term,
2605        "$LIKE" => {
2606            let pat = term.as_str().unwrap_or("").to_string();
2607            crate::re::Regex::new(&pat)
2608                .map(|re| re.is_match(&stringify(&point, None, false)))
2609                .unwrap_or(false)
2610        }
2611        _ => false,
2612    };
2613
2614    if pass {
2615        let nlen = nodes.len();
2616        if nlen >= 2 {
2617            set_prop(nodes[nlen - 2].clone(), &Value::str(gkey), point);
2618        }
2619    } else {
2620        errs_push(
2621            inj,
2622            format!(
2623                "CMP: {}{}{} fail:{} {}",
2624                pathify(&path_value(&ppath), None, None),
2625                S_VIZ,
2626                stringify(&point, None, false),
2627                r,
2628                stringify(&term, None, false)
2629            ),
2630        );
2631    }
2632    Value::Noval
2633}
2634
2635pub fn select(children: &Value, query: &Value) -> Value {
2636    if !is_node(children) {
2637        return Value::empty_list();
2638    }
2639
2640    let child_list: Vec<Value> = match children {
2641        Value::Map(_) => items_vec(children)
2642            .into_iter()
2643            .map(|(k, v)| {
2644                set_prop(v.clone(), &Value::str(S_DKEY), Value::str(k));
2645                v
2646            })
2647            .collect(),
2648        Value::List(_) => items_vec(children)
2649            .into_iter()
2650            .map(|(k, v)| {
2651                set_prop(
2652                    v.clone(),
2653                    &Value::str(S_DKEY),
2654                    Value::Num(k.parse::<f64>().unwrap_or(f64::NAN)),
2655                );
2656                v
2657            })
2658            .collect(),
2659        _ => return Value::empty_list(),
2660    };
2661
2662    let extra = Value::map_of([
2663        ("$AND".to_string(), Value::func(select_and)),
2664        ("$OR".to_string(), Value::func(select_or)),
2665        ("$NOT".to_string(), Value::func(select_not)),
2666        ("$GT".to_string(), Value::func(select_cmp)),
2667        ("$LT".to_string(), Value::func(select_cmp)),
2668        ("$GTE".to_string(), Value::func(select_cmp)),
2669        ("$LTE".to_string(), Value::func(select_cmp)),
2670        ("$LIKE".to_string(), Value::func(select_cmp)),
2671    ]);
2672    let meta = Value::map_of([(S_BEXACT.to_string(), Value::Bool(true))]);
2673
2674    let q = clone(query);
2675    let mut open = |_k: &Value, v: &Value, _p: &Value, _t: &[String]| -> Value {
2676        if is_map(v) {
2677            let cur = get_prop(v, &Value::str(S_BOPEN), Value::Bool(true));
2678            set_prop(v.clone(), &Value::str(S_BOPEN), cur);
2679        }
2680        v.clone()
2681    };
2682    walk(q.clone(), Some(&mut open), None, None);
2683
2684    let mut results: Vec<Value> = Vec::new();
2685    for child in &child_list {
2686        let errs = Value::empty_list();
2687        let vd = InjectDef {
2688            errs: Some(errs.clone()),
2689            meta: Some(meta.clone()),
2690            extra: Some(extra.clone()),
2691            ..Default::default()
2692        };
2693        let _ = validate(child, &clone(&q), Some(&vd));
2694        if errs
2695            .as_list()
2696            .map(|l| l.borrow().is_empty())
2697            .unwrap_or(true)
2698        {
2699            results.push(child.clone());
2700        }
2701    }
2702    Value::list(results)
2703}
2704
2705// keep `Injection::has_handler` referenced (used once staging is complete)
2706#[allow(dead_code)]
2707fn _keepalive() {
2708    let _ = Injection::has_handler(None);
2709    let _ = inject_child as fn(Value, &Value, &Inj) -> Inj;
2710}