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