Skip to main content

dataflow_rs/engine/
utils.rs

1//! # Utility Functions Module
2//!
3//! Path-based read/write helpers for the [`datavalue::OwnedDataValue`] tree
4//! that backs `Message::context`. The same dot-path syntax that worked on
5//! `serde_json::Value` works here unchanged — including `#`-prefix escapes
6//! for numeric object keys.
7
8use datavalue::OwnedDataValue;
9use std::sync::Arc;
10
11/// `#[serde(default = ...)]` value for a `Workflow`/`Task` condition: always
12/// true, so an absent condition runs unconditionally. Shared by both —
13/// duplicating a one-line `fn` per struct is what it looks like when it isn't.
14pub(crate) fn default_condition() -> serde_json::Value {
15    serde_json::Value::Bool(true)
16}
17
18/// Get a reference to the value at `path`, walking the tree.
19///
20/// Path syntax:
21/// - `"user.name"` — object property
22/// - `"items.0"` — array index
23/// - `"user.addresses.0.city"` — mixed
24/// - `"data.#20"` — object key literally named `"20"` (strip one leading `#`)
25/// - `"data.##"` — object key literally named `"#"` (strip one leading `#`)
26///
27/// Returns `None` for missing keys, out-of-bounds indices, invalid index
28/// formats, or attempts to descend through a non-container.
29pub fn get_nested_value<'b>(data: &'b OwnedDataValue, path: &str) -> Option<&'b OwnedDataValue> {
30    if path.is_empty() {
31        return Some(data);
32    }
33    get_nested_value_impl(data, path.split('.'))
34}
35
36/// Set the value at `path`, creating intermediate containers as needed.
37///
38/// Mirrors the original `serde_json::Value` flavour:
39/// - intermediate containers are created on demand; the next path part
40///   determines whether to create an `Object` (string key) or `Array`
41///   (numeric index);
42/// - arrays grow with `OwnedDataValue::Null` padding when an index past
43///   the current end is assigned;
44/// - `#`-prefix escape applies inside object contexts only;
45/// - silently no-ops when traversing through a non-container in a non-
46///   terminal hop or when an array path part isn't a valid `usize`.
47pub fn set_nested_value(data: &mut OwnedDataValue, path: &str, value: OwnedDataValue) {
48    if path.is_empty() {
49        return;
50    }
51    let parts: Vec<&str> = path.split('.').collect();
52    set_nested_value_impl(data, &parts, value);
53}
54
55/// Clone the value at `path`, returning `None` if the path is unresolvable.
56#[inline]
57pub fn get_nested_value_cloned(data: &OwnedDataValue, path: &str) -> Option<OwnedDataValue> {
58    get_nested_value(data, path).cloned()
59}
60
61/// Same as `get_nested_value` but consumes a pre-split slice of path parts.
62/// Parts retain the original `#` prefix; `strip_hash_prefix` is applied at
63/// lookup time so the `#20` → "force object key 20" semantics still hold.
64pub fn get_nested_value_parts<'b>(
65    data: &'b OwnedDataValue,
66    parts: &[Arc<str>],
67) -> Option<&'b OwnedDataValue> {
68    get_nested_value_impl(data, parts.iter().map(Arc::as_ref))
69}
70
71/// Shared tree-walk behind [`get_nested_value`] and [`get_nested_value_parts`].
72/// `#`-prefix escape is applied at lookup time via `strip_hash_prefix`, so a
73/// caller passing raw (unstripped) parts — as `get_nested_value_parts` does —
74/// still gets `#20` → object key `"20"` semantics.
75///
76/// Takes the parts as an iterator so neither caller collects them first: a
77/// read is one heap allocation cheaper, which matters on the paths that run
78/// per message — `TaskContext::get` and the `secret` operator among them.
79fn get_nested_value_impl<'b, 'p>(
80    data: &'b OwnedDataValue,
81    parts: impl Iterator<Item = &'p str>,
82) -> Option<&'b OwnedDataValue> {
83    let mut current = data;
84    for part in parts {
85        match current {
86            OwnedDataValue::Object(pairs) => {
87                let key = strip_hash_prefix(part);
88                let slot = pairs.iter().find(|(k, _)| k == key)?;
89                current = &slot.1;
90            }
91            OwnedDataValue::Array(items) => {
92                let idx: usize = part.parse().ok()?;
93                current = items.get(idx)?;
94            }
95            _ => return None,
96        }
97    }
98    Some(current)
99}
100
101/// Same as `set_nested_value` but consumes a pre-split slice of path parts.
102/// Parts retain the original `#` prefix; `strip_hash_prefix` is applied at
103/// use time. Crucially, the "is the NEXT segment an array index?" decision
104/// looks at the raw (unstripped) `parts[i+1]` — `#20` parses as non-numeric,
105/// so the child container is an Object (key "20"), not an Array.
106pub fn set_nested_value_parts(
107    data: &mut OwnedDataValue,
108    parts: &[Arc<str>],
109    value: OwnedDataValue,
110) {
111    if parts.is_empty() {
112        return;
113    }
114    set_nested_value_impl(data, parts, value);
115}
116
117/// Shared tree-walk behind [`set_nested_value`] and [`set_nested_value_parts`]:
118/// intermediate containers are created on demand (next part decides Array vs
119/// Object), arrays grow with `Null` padding, and the `#`-prefix escape applies
120/// inside object contexts only. See [`set_nested_value`] for the full contract.
121///
122/// Generic over the part type so the pre-split `&[Arc<str>]` callers walk their
123/// slice directly: collecting it into a `Vec<&str>` first would allocate on
124/// every write, once per loop sweep and once per mapping per task, which is
125/// exactly the cost the pre-split paths exist to avoid.
126fn set_nested_value_impl<P: AsRef<str>>(
127    data: &mut OwnedDataValue,
128    parts: &[P],
129    value: OwnedDataValue,
130) {
131    let last = parts.len() - 1;
132    let mut current = data;
133
134    for (i, part) in parts.iter().enumerate() {
135        let part = part.as_ref();
136        if i == last {
137            match current {
138                OwnedDataValue::Object(pairs) => {
139                    let key = strip_hash_prefix(part);
140                    if let Some(slot) = pairs.iter_mut().find(|(k, _)| k == key) {
141                        slot.1 = value;
142                    } else {
143                        pairs.push((key.to_string(), value));
144                    }
145                }
146                OwnedDataValue::Array(items) => {
147                    if let Ok(idx) = part.parse::<usize>() {
148                        while items.len() <= idx {
149                            items.push(OwnedDataValue::Null);
150                        }
151                        items[idx] = value;
152                    }
153                }
154                _ => {}
155            }
156            return;
157        }
158
159        // Non-terminal hop: locate-or-create the child and descend.
160        // Use the next part to decide whether the child container is an Array
161        // (next part parses as usize) or an Object (anything else).
162        let next_is_array = parts[i + 1].as_ref().parse::<usize>().is_ok();
163
164        match current {
165            OwnedDataValue::Object(pairs) => {
166                let key = strip_hash_prefix(part);
167                let idx = match pairs.iter().position(|(k, _)| k == key) {
168                    Some(idx) => idx,
169                    None => {
170                        let child = if next_is_array {
171                            OwnedDataValue::Array(Vec::new())
172                        } else {
173                            OwnedDataValue::Object(Vec::new())
174                        };
175                        pairs.push((key.to_string(), child));
176                        pairs.len() - 1
177                    }
178                };
179                current = &mut pairs[idx].1;
180            }
181            OwnedDataValue::Array(items) => {
182                let Ok(idx) = part.parse::<usize>() else {
183                    return; // can't use a non-numeric key on an Array
184                };
185                while items.len() <= idx {
186                    items.push(OwnedDataValue::Null);
187                }
188                if matches!(items[idx], OwnedDataValue::Null) {
189                    items[idx] = if next_is_array {
190                        OwnedDataValue::Array(Vec::new())
191                    } else {
192                        OwnedDataValue::Object(Vec::new())
193                    };
194                }
195                current = &mut items[idx];
196            }
197            _ => return,
198        }
199    }
200}
201
202/// Remove the value at `path` and return it; `None` if the path does not resolve.
203///
204/// Completes the module's read/write pair with a removal. Note that
205/// `set_nested_value(path, OwnedDataValue::Null)` is *not* removal — it leaves an
206/// explicit `null` in the tree, which survives every serialization boundary
207/// because `Message`'s `Serialize` emits `context` whole.
208///
209/// Path syntax is identical to [`get_nested_value`] and [`set_nested_value`]:
210/// dot-separated segments, numeric segments index arrays, and one leading `#` is
211/// stripped from an object-key segment (`"data.#20"` is the object key `"20"`).
212///
213/// - Object keys are removed from the pair vec; the relative order of the
214///   surviving pairs is preserved.
215/// - Array elements are removed with a tail shift, so later indices move down.
216/// - Returns `None` — leaving `data` untouched — for a missing key, an
217///   out-of-bounds or non-numeric array index, an attempt to descend through a
218///   non-container, or an empty `path`. Never panics.
219///
220/// Note the deliberate asymmetry with the read side: `get_nested_value(d, "")`
221/// returns the whole tree, but there is no such thing as removing the root, so an
222/// empty path yields `None` rather than taking `data` apart.
223///
224/// ```
225/// use dataflow_rs::datavalue::OwnedDataValue;
226/// use dataflow_rs::engine::utils::remove_nested_value;
227/// use serde_json::json;
228///
229/// let mut ctx = OwnedDataValue::from(&json!({"data": {"order_id": 7, "_scratch": 42}}));
230///
231/// let taken = remove_nested_value(&mut ctx, "data._scratch");
232///
233/// assert_eq!(taken, Some(OwnedDataValue::from(&json!(42))));
234/// assert_eq!(
235///     serde_json::Value::from(&ctx),
236///     json!({"data": {"order_id": 7}}),
237/// );
238/// ```
239pub fn remove_nested_value(data: &mut OwnedDataValue, path: &str) -> Option<OwnedDataValue> {
240    if path.is_empty() {
241        return None;
242    }
243    let parts: Vec<&str> = path.split('.').collect();
244    remove_nested_value_parts(data, &parts)
245}
246
247/// Pre-split variant of [`remove_nested_value`], for callers already holding a
248/// resolved `&[Arc<str>]` — `map`'s `unset` takes the same parts its writes do,
249/// so a removal never re-splits the path. Same contract, `#` handling included.
250pub(crate) fn remove_nested_value_parts<P: AsRef<str>>(
251    data: &mut OwnedDataValue,
252    parts: &[P],
253) -> Option<OwnedDataValue> {
254    let (last, parents) = parts.split_last()?;
255    let last = last.as_ref();
256
257    let mut current = data;
258    for part in parents {
259        let part = part.as_ref();
260        current = match current {
261            OwnedDataValue::Object(pairs) => {
262                let key = strip_hash_prefix(part);
263                let idx = pairs.iter().position(|(k, _)| k == key)?;
264                &mut pairs[idx].1
265            }
266            OwnedDataValue::Array(items) => {
267                let idx: usize = part.parse().ok()?;
268                items.get_mut(idx)?
269            }
270            _ => return None,
271        };
272    }
273
274    match current {
275        OwnedDataValue::Object(pairs) => {
276            let key = strip_hash_prefix(last);
277            let pos = pairs.iter().position(|(k, _)| k == key)?;
278            Some(pairs.remove(pos).1)
279        }
280        OwnedDataValue::Array(items) => {
281            let idx: usize = last.parse().ok()?;
282            if idx < items.len() {
283                Some(items.remove(idx))
284            } else {
285                None
286            }
287        }
288        _ => None,
289    }
290}
291
292/// Strip exactly one leading `#` from an object-key path component.
293/// `"#20"` → `"20"`, `"##"` → `"#"`, `"foo"` → `"foo"`.
294#[inline]
295pub(crate) fn strip_hash_prefix(part: &str) -> &str {
296    part.strip_prefix('#').unwrap_or(part)
297}
298
299/// Split `"data.{target}"` into the cached `(Arc<str>, Arc<[Arc<str>]>)` shape
300/// that `ParseConfig` and `PublishConfig` each cache on their `target_path_arc`
301/// / `target_path_parts` fields, so the hot path never re-formats or re-splits
302/// the write path. Shared by both configs' `precompute_target_path` (populated
303/// by `LogicCompiler`) and `resolve_target_path` (the on-the-fly fallback for
304/// directly-constructed configs).
305pub(crate) fn compute_data_path(target: &str) -> (Arc<str>, Arc<[Arc<str>]>) {
306    (
307        Arc::from(format!("data.{target}")),
308        compute_path_parts("data", target),
309    )
310}
311
312/// Split `"{prefix}.{target}"` into the cached `Arc<[Arc<str>]>` walk order used
313/// by the `*_parts` accessors, without building the joined string.
314///
315/// Shared by [`compute_data_path`] and `LoopConfig`'s `temp_data.{counter}`
316/// pre-split — the latter only ever writes through the parts, so materializing
317/// the dotted form for it would be wasted work.
318pub(crate) fn compute_path_parts(prefix: &str, target: &str) -> Arc<[Arc<str>]> {
319    std::iter::once(Arc::from(prefix))
320        .chain(target.split('.').map(Arc::from))
321        .collect()
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use serde_json::json;
328
329    /// Test-only helper: build OwnedDataValue from a `json!` literal.
330    fn dv(v: serde_json::Value) -> OwnedDataValue {
331        OwnedDataValue::from(&v)
332    }
333
334    #[test]
335    fn test_get_nested_value() {
336        let data = dv(json!({
337            "user": {
338                "name": "John",
339                "age": 30,
340                "addresses": [
341                    {"city": "New York", "zip": "10001"},
342                    {"city": "San Francisco", "zip": "94102"}
343                ],
344                "preferences": {
345                    "theme": "dark",
346                    "notifications": true
347                }
348            },
349            "items": [1, 2, 3]
350        }));
351
352        assert_eq!(
353            get_nested_value(&data, "user.name"),
354            Some(&dv(json!("John")))
355        );
356        assert_eq!(get_nested_value(&data, "user.age"), Some(&dv(json!(30))));
357
358        assert_eq!(
359            get_nested_value(&data, "user.preferences.theme"),
360            Some(&dv(json!("dark")))
361        );
362        assert_eq!(
363            get_nested_value(&data, "user.preferences.notifications"),
364            Some(&dv(json!(true)))
365        );
366
367        assert_eq!(get_nested_value(&data, "items.0"), Some(&dv(json!(1))));
368        assert_eq!(get_nested_value(&data, "items.2"), Some(&dv(json!(3))));
369
370        assert_eq!(
371            get_nested_value(&data, "user.addresses.0.city"),
372            Some(&dv(json!("New York")))
373        );
374        assert_eq!(
375            get_nested_value(&data, "user.addresses.1.zip"),
376            Some(&dv(json!("94102")))
377        );
378
379        assert_eq!(get_nested_value(&data, "user.missing"), None);
380        assert_eq!(get_nested_value(&data, "items.10"), None);
381        assert_eq!(get_nested_value(&data, "user.addresses.2.city"), None);
382        assert_eq!(get_nested_value(&data, "nonexistent.path"), None);
383    }
384
385    #[test]
386    fn test_set_nested_value() {
387        let mut data = dv(json!({}));
388
389        set_nested_value(&mut data, "name", dv(json!("Alice")));
390        assert_eq!(data, dv(json!({"name": "Alice"})));
391
392        set_nested_value(&mut data, "user.email", dv(json!("alice@example.com")));
393        assert_eq!(
394            data,
395            dv(json!({
396                "name": "Alice",
397                "user": {"email": "alice@example.com"}
398            }))
399        );
400
401        set_nested_value(&mut data, "name", dv(json!("Bob")));
402        assert_eq!(
403            data,
404            dv(json!({
405                "name": "Bob",
406                "user": {"email": "alice@example.com"}
407            }))
408        );
409
410        set_nested_value(&mut data, "settings.theme.mode", dv(json!("dark")));
411        assert_eq!(data["settings"]["theme"]["mode"], dv(json!("dark")));
412
413        set_nested_value(&mut data, "user.age", dv(json!(25)));
414        assert_eq!(data["user"]["age"], dv(json!(25)));
415        assert_eq!(data["user"]["email"], dv(json!("alice@example.com")));
416    }
417
418    #[test]
419    fn test_set_nested_value_with_arrays() {
420        let mut data = dv(json!({ "items": [1, 2, 3] }));
421
422        set_nested_value(&mut data, "items.0", dv(json!(10)));
423        assert_eq!(data["items"], dv(json!([10, 2, 3])));
424
425        set_nested_value(&mut data, "items.5", dv(json!(50)));
426        assert_eq!(data["items"], dv(json!([10, 2, 3, null, null, 50])));
427
428        let mut data2 = dv(json!({}));
429        set_nested_value(&mut data2, "matrix.0.0", dv(json!(1)));
430        set_nested_value(&mut data2, "matrix.0.1", dv(json!(2)));
431        set_nested_value(&mut data2, "matrix.1.0", dv(json!(3)));
432        assert_eq!(data2, dv(json!({ "matrix": [[1, 2], [3]] })));
433    }
434
435    #[test]
436    fn test_set_nested_value_array_expansion() {
437        let mut data = dv(json!({}));
438
439        set_nested_value(&mut data, "array.2", dv(json!("value")));
440        assert_eq!(data, dv(json!({ "array": [null, null, "value"] })));
441
442        let mut data2 = dv(json!({}));
443        set_nested_value(&mut data2, "deep.nested.0.field", dv(json!("test")));
444        assert_eq!(
445            data2,
446            dv(json!({ "deep": { "nested": [{ "field": "test" }] } }))
447        );
448    }
449
450    #[test]
451    fn test_get_nested_value_cloned() {
452        let data = dv(json!({
453            "user": {
454                "profile": {
455                    "name": "Alice",
456                    "settings": {"theme": "dark"}
457                }
458            }
459        }));
460
461        assert_eq!(
462            get_nested_value_cloned(&data, "user.profile.name"),
463            Some(dv(json!("Alice")))
464        );
465        assert_eq!(
466            get_nested_value_cloned(&data, "user.profile.settings"),
467            Some(dv(json!({ "theme": "dark" })))
468        );
469        assert_eq!(get_nested_value_cloned(&data, "user.missing"), None);
470    }
471
472    #[test]
473    fn test_get_nested_value_bounds_checking() {
474        let data = dv(json!({
475            "items": [1, 2, 3],
476            "nested": {
477                "array": [
478                    {"id": 1},
479                    {"id": 2}
480                ]
481            }
482        }));
483
484        assert_eq!(get_nested_value(&data, "items.0"), Some(&dv(json!(1))));
485        assert_eq!(get_nested_value(&data, "items.2"), Some(&dv(json!(3))));
486
487        assert_eq!(get_nested_value(&data, "items.10"), None);
488        assert_eq!(get_nested_value(&data, "items.999999"), None);
489
490        assert_eq!(get_nested_value(&data, "items.abc"), None);
491        assert_eq!(get_nested_value(&data, "items.-1"), None);
492        assert_eq!(get_nested_value(&data, "items.2.5"), None);
493
494        assert_eq!(
495            get_nested_value(&data, "nested.array.0.id"),
496            Some(&dv(json!(1)))
497        );
498        assert_eq!(get_nested_value(&data, "nested.array.5.id"), None);
499
500        assert_eq!(get_nested_value(&data, ""), Some(&data));
501    }
502
503    #[test]
504    fn test_set_nested_value_bounds_safety() {
505        let mut data = dv(json!({}));
506
507        set_nested_value(&mut data, "large.10", dv(json!("value")));
508        assert_eq!(data["large"].as_array().unwrap().len(), 11);
509        assert_eq!(data["large"][10], dv(json!("value")));
510        for i in 0..10usize {
511            assert_eq!(data["large"][i], dv(json!(null)));
512        }
513
514        let mut data2 = dv(json!({ "matrix": [] }));
515        set_nested_value(&mut data2, "matrix.2.1", dv(json!(5)));
516        assert_eq!(data2["matrix"][0], dv(json!(null)));
517        assert_eq!(data2["matrix"][1], dv(json!(null)));
518        assert_eq!(data2["matrix"][2][0], dv(json!(null)));
519        assert_eq!(data2["matrix"][2][1], dv(json!(5)));
520
521        let mut data3 = dv(json!({ "arr": [1, 2, 3] }));
522        set_nested_value(&mut data3, "arr.1", dv(json!("replaced")));
523        assert_eq!(data3["arr"], dv(json!([1, "replaced", 3])));
524    }
525
526    #[test]
527    fn test_hash_prefix_in_paths() {
528        let data = dv(json!({
529            "fields": {
530                "20": "numeric field name",
531                "#": "hash field",
532                "##": "double hash field",
533                "normal": "normal field"
534            }
535        }));
536
537        assert_eq!(
538            get_nested_value(&data, "fields.#20"),
539            Some(&dv(json!("numeric field name")))
540        );
541        assert_eq!(
542            get_nested_value(&data, "fields.##"),
543            Some(&dv(json!("hash field")))
544        );
545        assert_eq!(
546            get_nested_value(&data, "fields.###"),
547            Some(&dv(json!("double hash field")))
548        );
549        assert_eq!(
550            get_nested_value(&data, "fields.normal"),
551            Some(&dv(json!("normal field")))
552        );
553        assert_eq!(get_nested_value(&data, "fields.#999"), None);
554    }
555
556    #[test]
557    fn test_set_hash_prefix_in_paths() {
558        let mut data = dv(json!({}));
559
560        set_nested_value(&mut data, "fields.#20", dv(json!("value for 20")));
561        assert_eq!(data["fields"]["20"], dv(json!("value for 20")));
562
563        set_nested_value(&mut data, "fields.##", dv(json!("hash value")));
564        assert_eq!(data["fields"]["#"], dv(json!("hash value")));
565
566        set_nested_value(&mut data, "fields.###", dv(json!("double hash value")));
567        assert_eq!(data["fields"]["##"], dv(json!("double hash value")));
568
569        set_nested_value(&mut data, "fields.normal", dv(json!("normal value")));
570        assert_eq!(data["fields"]["normal"], dv(json!("normal value")));
571
572        assert_eq!(
573            data,
574            dv(json!({
575                "fields": {
576                    "20": "value for 20",
577                    "#": "hash value",
578                    "##": "double hash value",
579                    "normal": "normal value"
580                }
581            }))
582        );
583    }
584
585    #[test]
586    fn test_hash_prefix_with_arrays() {
587        let mut data = dv(json!({
588            "items": [
589                {"0": "field named zero", "id": 1},
590                {"1": "field named one", "id": 2}
591            ]
592        }));
593
594        assert_eq!(
595            get_nested_value(&data, "items.0.#0"),
596            Some(&dv(json!("field named zero")))
597        );
598        assert_eq!(
599            get_nested_value(&data, "items.1.#1"),
600            Some(&dv(json!("field named one")))
601        );
602
603        set_nested_value(&mut data, "items.0.#2", dv(json!("field named two")));
604        assert_eq!(data["items"][0]["2"], dv(json!("field named two")));
605
606        assert_eq!(get_nested_value(&data, "items.0.id"), Some(&dv(json!(1))));
607        assert_eq!(get_nested_value(&data, "items.1.id"), Some(&dv(json!(2))));
608    }
609
610    #[test]
611    fn test_hash_prefix_field_with_array_value() {
612        let data = dv(json!({
613            "data": {
614                "fields": {
615                    "72": ["first", "second", "third"],
616                    "100": ["alpha", "beta", "gamma"],
617                    "normal": ["one", "two", "three"]
618                }
619            }
620        }));
621
622        assert_eq!(
623            get_nested_value(&data, "data.fields.#72.0"),
624            Some(&dv(json!("first")))
625        );
626        assert_eq!(
627            get_nested_value(&data, "data.fields.#72.1"),
628            Some(&dv(json!("second")))
629        );
630        assert_eq!(
631            get_nested_value(&data, "data.fields.#72.2"),
632            Some(&dv(json!("third")))
633        );
634
635        assert_eq!(
636            get_nested_value(&data, "data.fields.#100.0"),
637            Some(&dv(json!("alpha")))
638        );
639        assert_eq!(
640            get_nested_value(&data, "data.fields.#100.1"),
641            Some(&dv(json!("beta")))
642        );
643
644        assert_eq!(
645            get_nested_value(&data, "data.fields.normal.0"),
646            Some(&dv(json!("one")))
647        );
648
649        let mut data_mut = data.clone();
650        set_nested_value(&mut data_mut, "data.fields.#72.0", dv(json!("modified")));
651        assert_eq!(data_mut["data"]["fields"]["72"][0], dv(json!("modified")));
652
653        set_nested_value(&mut data_mut, "data.fields.#999.0", dv(json!("new value")));
654        assert_eq!(data_mut["data"]["fields"]["999"][0], dv(json!("new value")));
655
656        let complex_data = dv(json!({
657            "fields": {
658                "42": [
659                    {"name": "item1", "value": 100},
660                    {"name": "item2", "value": 200}
661                ]
662            }
663        }));
664
665        assert_eq!(
666            get_nested_value(&complex_data, "fields.#42.0.name"),
667            Some(&dv(json!("item1")))
668        );
669        assert_eq!(
670            get_nested_value(&complex_data, "fields.#42.1.value"),
671            Some(&dv(json!(200)))
672        );
673
674        let multi_hash_data = dv(json!({
675            "data": {
676                "#fields": {
677                    "##": ["hash array"],
678                    "10": ["numeric array"]
679                }
680            }
681        }));
682
683        assert_eq!(
684            get_nested_value(&multi_hash_data, "data.##fields.###.0"),
685            Some(&dv(json!("hash array")))
686        );
687        assert_eq!(
688            get_nested_value(&multi_hash_data, "data.##fields.#10.0"),
689            Some(&dv(json!("numeric array")))
690        );
691    }
692
693    // ---------------------------------------------------------------------
694    // remove_nested_value
695    // ---------------------------------------------------------------------
696
697    /// Round-trip helper: the whole tree as `serde_json::Value`, for
698    /// byte-identical assertions after a `None` return.
699    fn as_json(v: &OwnedDataValue) -> serde_json::Value {
700        serde_json::Value::from(v)
701    }
702
703    #[test]
704    fn test_remove_nested_value_object() {
705        let mut data = dv(json!({"data": {"a": 1, "_b": 2}}));
706
707        assert_eq!(
708            remove_nested_value(&mut data, "data._b"),
709            Some(dv(json!(2)))
710        );
711        // Surviving pairs keep their relative order.
712        assert_eq!(as_json(&data), json!({"data": {"a": 1}}));
713
714        // Removing the same path twice: Some, then None.
715        assert_eq!(remove_nested_value(&mut data, "data._b"), None);
716        assert_eq!(as_json(&data), json!({"data": {"a": 1}}));
717    }
718
719    #[test]
720    fn test_remove_nested_value_array_shifts_tail() {
721        let mut data = dv(json!({"items": [1, 2, 3]}));
722
723        assert_eq!(
724            remove_nested_value(&mut data, "items.1"),
725            Some(dv(json!(2)))
726        );
727        // A tail shift, not a null hole.
728        assert_eq!(as_json(&data), json!({"items": [1, 3]}));
729    }
730
731    #[test]
732    fn test_remove_nested_value_returns_subtree_intact() {
733        let mut data = dv(json!({"data": {"nested": {"x": [1, 2]}}}));
734
735        assert_eq!(
736            remove_nested_value(&mut data, "data.nested"),
737            Some(dv(json!({"x": [1, 2]})))
738        );
739        assert_eq!(as_json(&data), json!({"data": {}}));
740    }
741
742    #[test]
743    fn test_remove_nested_value_traverses_array_in_non_terminal_position() {
744        let mut data = dv(json!({"a": [{"k": 1}, {"k": 2}]}));
745
746        assert_eq!(remove_nested_value(&mut data, "a.1.k"), Some(dv(json!(2))));
747        assert_eq!(as_json(&data), json!({"a": [{"k": 1}, {}]}));
748    }
749
750    #[test]
751    fn test_remove_hash_prefix_in_paths() {
752        // Same `#`-escape mapping asserted by test_hash_prefix_in_paths and
753        // test_set_hash_prefix_in_paths.
754        let mut data = dv(json!({"fields": {"20": "x", "#": "y", "##": "z"}}));
755
756        assert_eq!(
757            remove_nested_value(&mut data, "fields.#20"),
758            Some(dv(json!("x")))
759        );
760        assert_eq!(
761            remove_nested_value(&mut data, "fields.##"),
762            Some(dv(json!("y")))
763        );
764        assert_eq!(
765            remove_nested_value(&mut data, "fields.###"),
766            Some(dv(json!("z")))
767        );
768        assert_eq!(as_json(&data), json!({"fields": {}}));
769    }
770
771    #[test]
772    fn test_remove_nested_value_negative_cases_leave_tree_untouched() {
773        // Every one of these must return None *and* leave the tree
774        // byte-identical — asserted on the whole tree, not just the return.
775        let original = json!({
776            "items": [1, 2, 3],
777            "a": [{"k": 1}],
778            "data": {"x": 1},
779            "b": 1
780        });
781
782        for path in [
783            "",          // empty path
784            "data.nope", // missing key, last segment
785            "nope.x",    // missing key, parent segment
786            "items.9",   // out-of-bounds, terminal
787            "a.5.k",     // out-of-bounds, mid-path
788            "items.abc", // non-numeric array segment
789            "items.-1",  // negative array segment
790            "b.c",       // descent through a scalar
791            "b.c.d",     // deeper descent through a scalar
792        ] {
793            let mut data = dv(original.clone());
794            assert_eq!(
795                remove_nested_value(&mut data, path),
796                None,
797                "path '{path}' should not resolve"
798            );
799            assert_eq!(
800                as_json(&data),
801                original,
802                "path '{path}' must leave the tree untouched"
803            );
804        }
805    }
806
807    #[test]
808    fn test_remove_nested_value_scalar_root() {
809        let mut scalar = dv(json!("scalar"));
810        assert_eq!(remove_nested_value(&mut scalar, "a"), None);
811        assert_eq!(as_json(&scalar), json!("scalar"));
812    }
813
814    #[test]
815    fn test_remove_nested_value_non_ascii_keys() {
816        let mut data = dv(json!({"データ": {"ключ": "значение"}}));
817
818        assert_eq!(
819            remove_nested_value(&mut data, "データ.ключ"),
820            Some(dv(json!("значение")))
821        );
822        assert_eq!(as_json(&data), json!({"データ": {}}));
823    }
824}