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    let (last, parents) = parts.split_last()?;
245
246    let mut current = data;
247    for part in parents {
248        current = match current {
249            OwnedDataValue::Object(pairs) => {
250                let key = strip_hash_prefix(part);
251                let idx = pairs.iter().position(|(k, _)| k == key)?;
252                &mut pairs[idx].1
253            }
254            OwnedDataValue::Array(items) => {
255                let idx: usize = part.parse().ok()?;
256                items.get_mut(idx)?
257            }
258            _ => return None,
259        };
260    }
261
262    match current {
263        OwnedDataValue::Object(pairs) => {
264            let key = strip_hash_prefix(last);
265            let pos = pairs.iter().position(|(k, _)| k == key)?;
266            Some(pairs.remove(pos).1)
267        }
268        OwnedDataValue::Array(items) => {
269            let idx: usize = last.parse().ok()?;
270            if idx < items.len() {
271                Some(items.remove(idx))
272            } else {
273                None
274            }
275        }
276        _ => None,
277    }
278}
279
280/// Strip exactly one leading `#` from an object-key path component.
281/// `"#20"` → `"20"`, `"##"` → `"#"`, `"foo"` → `"foo"`.
282#[inline]
283pub(crate) fn strip_hash_prefix(part: &str) -> &str {
284    part.strip_prefix('#').unwrap_or(part)
285}
286
287/// Split `"data.{target}"` into the cached `(Arc<str>, Arc<[Arc<str>]>)` shape
288/// that `ParseConfig` and `PublishConfig` each cache on their `target_path_arc`
289/// / `target_path_parts` fields, so the hot path never re-formats or re-splits
290/// the write path. Shared by both configs' `precompute_target_path` (populated
291/// by `LogicCompiler`) and `resolve_target_path` (the on-the-fly fallback for
292/// directly-constructed configs).
293pub(crate) fn compute_data_path(target: &str) -> (Arc<str>, Arc<[Arc<str>]>) {
294    (
295        Arc::from(format!("data.{target}")),
296        compute_path_parts("data", target),
297    )
298}
299
300/// Split `"{prefix}.{target}"` into the cached `Arc<[Arc<str>]>` walk order used
301/// by the `*_parts` accessors, without building the joined string.
302///
303/// Shared by [`compute_data_path`] and `LoopConfig`'s `temp_data.{counter}`
304/// pre-split — the latter only ever writes through the parts, so materializing
305/// the dotted form for it would be wasted work.
306pub(crate) fn compute_path_parts(prefix: &str, target: &str) -> Arc<[Arc<str>]> {
307    std::iter::once(Arc::from(prefix))
308        .chain(target.split('.').map(Arc::from))
309        .collect()
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use serde_json::json;
316
317    /// Test-only helper: build OwnedDataValue from a `json!` literal.
318    fn dv(v: serde_json::Value) -> OwnedDataValue {
319        OwnedDataValue::from(&v)
320    }
321
322    #[test]
323    fn test_get_nested_value() {
324        let data = dv(json!({
325            "user": {
326                "name": "John",
327                "age": 30,
328                "addresses": [
329                    {"city": "New York", "zip": "10001"},
330                    {"city": "San Francisco", "zip": "94102"}
331                ],
332                "preferences": {
333                    "theme": "dark",
334                    "notifications": true
335                }
336            },
337            "items": [1, 2, 3]
338        }));
339
340        assert_eq!(
341            get_nested_value(&data, "user.name"),
342            Some(&dv(json!("John")))
343        );
344        assert_eq!(get_nested_value(&data, "user.age"), Some(&dv(json!(30))));
345
346        assert_eq!(
347            get_nested_value(&data, "user.preferences.theme"),
348            Some(&dv(json!("dark")))
349        );
350        assert_eq!(
351            get_nested_value(&data, "user.preferences.notifications"),
352            Some(&dv(json!(true)))
353        );
354
355        assert_eq!(get_nested_value(&data, "items.0"), Some(&dv(json!(1))));
356        assert_eq!(get_nested_value(&data, "items.2"), Some(&dv(json!(3))));
357
358        assert_eq!(
359            get_nested_value(&data, "user.addresses.0.city"),
360            Some(&dv(json!("New York")))
361        );
362        assert_eq!(
363            get_nested_value(&data, "user.addresses.1.zip"),
364            Some(&dv(json!("94102")))
365        );
366
367        assert_eq!(get_nested_value(&data, "user.missing"), None);
368        assert_eq!(get_nested_value(&data, "items.10"), None);
369        assert_eq!(get_nested_value(&data, "user.addresses.2.city"), None);
370        assert_eq!(get_nested_value(&data, "nonexistent.path"), None);
371    }
372
373    #[test]
374    fn test_set_nested_value() {
375        let mut data = dv(json!({}));
376
377        set_nested_value(&mut data, "name", dv(json!("Alice")));
378        assert_eq!(data, dv(json!({"name": "Alice"})));
379
380        set_nested_value(&mut data, "user.email", dv(json!("alice@example.com")));
381        assert_eq!(
382            data,
383            dv(json!({
384                "name": "Alice",
385                "user": {"email": "alice@example.com"}
386            }))
387        );
388
389        set_nested_value(&mut data, "name", dv(json!("Bob")));
390        assert_eq!(
391            data,
392            dv(json!({
393                "name": "Bob",
394                "user": {"email": "alice@example.com"}
395            }))
396        );
397
398        set_nested_value(&mut data, "settings.theme.mode", dv(json!("dark")));
399        assert_eq!(data["settings"]["theme"]["mode"], dv(json!("dark")));
400
401        set_nested_value(&mut data, "user.age", dv(json!(25)));
402        assert_eq!(data["user"]["age"], dv(json!(25)));
403        assert_eq!(data["user"]["email"], dv(json!("alice@example.com")));
404    }
405
406    #[test]
407    fn test_set_nested_value_with_arrays() {
408        let mut data = dv(json!({ "items": [1, 2, 3] }));
409
410        set_nested_value(&mut data, "items.0", dv(json!(10)));
411        assert_eq!(data["items"], dv(json!([10, 2, 3])));
412
413        set_nested_value(&mut data, "items.5", dv(json!(50)));
414        assert_eq!(data["items"], dv(json!([10, 2, 3, null, null, 50])));
415
416        let mut data2 = dv(json!({}));
417        set_nested_value(&mut data2, "matrix.0.0", dv(json!(1)));
418        set_nested_value(&mut data2, "matrix.0.1", dv(json!(2)));
419        set_nested_value(&mut data2, "matrix.1.0", dv(json!(3)));
420        assert_eq!(data2, dv(json!({ "matrix": [[1, 2], [3]] })));
421    }
422
423    #[test]
424    fn test_set_nested_value_array_expansion() {
425        let mut data = dv(json!({}));
426
427        set_nested_value(&mut data, "array.2", dv(json!("value")));
428        assert_eq!(data, dv(json!({ "array": [null, null, "value"] })));
429
430        let mut data2 = dv(json!({}));
431        set_nested_value(&mut data2, "deep.nested.0.field", dv(json!("test")));
432        assert_eq!(
433            data2,
434            dv(json!({ "deep": { "nested": [{ "field": "test" }] } }))
435        );
436    }
437
438    #[test]
439    fn test_get_nested_value_cloned() {
440        let data = dv(json!({
441            "user": {
442                "profile": {
443                    "name": "Alice",
444                    "settings": {"theme": "dark"}
445                }
446            }
447        }));
448
449        assert_eq!(
450            get_nested_value_cloned(&data, "user.profile.name"),
451            Some(dv(json!("Alice")))
452        );
453        assert_eq!(
454            get_nested_value_cloned(&data, "user.profile.settings"),
455            Some(dv(json!({ "theme": "dark" })))
456        );
457        assert_eq!(get_nested_value_cloned(&data, "user.missing"), None);
458    }
459
460    #[test]
461    fn test_get_nested_value_bounds_checking() {
462        let data = dv(json!({
463            "items": [1, 2, 3],
464            "nested": {
465                "array": [
466                    {"id": 1},
467                    {"id": 2}
468                ]
469            }
470        }));
471
472        assert_eq!(get_nested_value(&data, "items.0"), Some(&dv(json!(1))));
473        assert_eq!(get_nested_value(&data, "items.2"), Some(&dv(json!(3))));
474
475        assert_eq!(get_nested_value(&data, "items.10"), None);
476        assert_eq!(get_nested_value(&data, "items.999999"), None);
477
478        assert_eq!(get_nested_value(&data, "items.abc"), None);
479        assert_eq!(get_nested_value(&data, "items.-1"), None);
480        assert_eq!(get_nested_value(&data, "items.2.5"), None);
481
482        assert_eq!(
483            get_nested_value(&data, "nested.array.0.id"),
484            Some(&dv(json!(1)))
485        );
486        assert_eq!(get_nested_value(&data, "nested.array.5.id"), None);
487
488        assert_eq!(get_nested_value(&data, ""), Some(&data));
489    }
490
491    #[test]
492    fn test_set_nested_value_bounds_safety() {
493        let mut data = dv(json!({}));
494
495        set_nested_value(&mut data, "large.10", dv(json!("value")));
496        assert_eq!(data["large"].as_array().unwrap().len(), 11);
497        assert_eq!(data["large"][10], dv(json!("value")));
498        for i in 0..10usize {
499            assert_eq!(data["large"][i], dv(json!(null)));
500        }
501
502        let mut data2 = dv(json!({ "matrix": [] }));
503        set_nested_value(&mut data2, "matrix.2.1", dv(json!(5)));
504        assert_eq!(data2["matrix"][0], dv(json!(null)));
505        assert_eq!(data2["matrix"][1], dv(json!(null)));
506        assert_eq!(data2["matrix"][2][0], dv(json!(null)));
507        assert_eq!(data2["matrix"][2][1], dv(json!(5)));
508
509        let mut data3 = dv(json!({ "arr": [1, 2, 3] }));
510        set_nested_value(&mut data3, "arr.1", dv(json!("replaced")));
511        assert_eq!(data3["arr"], dv(json!([1, "replaced", 3])));
512    }
513
514    #[test]
515    fn test_hash_prefix_in_paths() {
516        let data = dv(json!({
517            "fields": {
518                "20": "numeric field name",
519                "#": "hash field",
520                "##": "double hash field",
521                "normal": "normal field"
522            }
523        }));
524
525        assert_eq!(
526            get_nested_value(&data, "fields.#20"),
527            Some(&dv(json!("numeric field name")))
528        );
529        assert_eq!(
530            get_nested_value(&data, "fields.##"),
531            Some(&dv(json!("hash field")))
532        );
533        assert_eq!(
534            get_nested_value(&data, "fields.###"),
535            Some(&dv(json!("double hash field")))
536        );
537        assert_eq!(
538            get_nested_value(&data, "fields.normal"),
539            Some(&dv(json!("normal field")))
540        );
541        assert_eq!(get_nested_value(&data, "fields.#999"), None);
542    }
543
544    #[test]
545    fn test_set_hash_prefix_in_paths() {
546        let mut data = dv(json!({}));
547
548        set_nested_value(&mut data, "fields.#20", dv(json!("value for 20")));
549        assert_eq!(data["fields"]["20"], dv(json!("value for 20")));
550
551        set_nested_value(&mut data, "fields.##", dv(json!("hash value")));
552        assert_eq!(data["fields"]["#"], dv(json!("hash value")));
553
554        set_nested_value(&mut data, "fields.###", dv(json!("double hash value")));
555        assert_eq!(data["fields"]["##"], dv(json!("double hash value")));
556
557        set_nested_value(&mut data, "fields.normal", dv(json!("normal value")));
558        assert_eq!(data["fields"]["normal"], dv(json!("normal value")));
559
560        assert_eq!(
561            data,
562            dv(json!({
563                "fields": {
564                    "20": "value for 20",
565                    "#": "hash value",
566                    "##": "double hash value",
567                    "normal": "normal value"
568                }
569            }))
570        );
571    }
572
573    #[test]
574    fn test_hash_prefix_with_arrays() {
575        let mut data = dv(json!({
576            "items": [
577                {"0": "field named zero", "id": 1},
578                {"1": "field named one", "id": 2}
579            ]
580        }));
581
582        assert_eq!(
583            get_nested_value(&data, "items.0.#0"),
584            Some(&dv(json!("field named zero")))
585        );
586        assert_eq!(
587            get_nested_value(&data, "items.1.#1"),
588            Some(&dv(json!("field named one")))
589        );
590
591        set_nested_value(&mut data, "items.0.#2", dv(json!("field named two")));
592        assert_eq!(data["items"][0]["2"], dv(json!("field named two")));
593
594        assert_eq!(get_nested_value(&data, "items.0.id"), Some(&dv(json!(1))));
595        assert_eq!(get_nested_value(&data, "items.1.id"), Some(&dv(json!(2))));
596    }
597
598    #[test]
599    fn test_hash_prefix_field_with_array_value() {
600        let data = dv(json!({
601            "data": {
602                "fields": {
603                    "72": ["first", "second", "third"],
604                    "100": ["alpha", "beta", "gamma"],
605                    "normal": ["one", "two", "three"]
606                }
607            }
608        }));
609
610        assert_eq!(
611            get_nested_value(&data, "data.fields.#72.0"),
612            Some(&dv(json!("first")))
613        );
614        assert_eq!(
615            get_nested_value(&data, "data.fields.#72.1"),
616            Some(&dv(json!("second")))
617        );
618        assert_eq!(
619            get_nested_value(&data, "data.fields.#72.2"),
620            Some(&dv(json!("third")))
621        );
622
623        assert_eq!(
624            get_nested_value(&data, "data.fields.#100.0"),
625            Some(&dv(json!("alpha")))
626        );
627        assert_eq!(
628            get_nested_value(&data, "data.fields.#100.1"),
629            Some(&dv(json!("beta")))
630        );
631
632        assert_eq!(
633            get_nested_value(&data, "data.fields.normal.0"),
634            Some(&dv(json!("one")))
635        );
636
637        let mut data_mut = data.clone();
638        set_nested_value(&mut data_mut, "data.fields.#72.0", dv(json!("modified")));
639        assert_eq!(data_mut["data"]["fields"]["72"][0], dv(json!("modified")));
640
641        set_nested_value(&mut data_mut, "data.fields.#999.0", dv(json!("new value")));
642        assert_eq!(data_mut["data"]["fields"]["999"][0], dv(json!("new value")));
643
644        let complex_data = dv(json!({
645            "fields": {
646                "42": [
647                    {"name": "item1", "value": 100},
648                    {"name": "item2", "value": 200}
649                ]
650            }
651        }));
652
653        assert_eq!(
654            get_nested_value(&complex_data, "fields.#42.0.name"),
655            Some(&dv(json!("item1")))
656        );
657        assert_eq!(
658            get_nested_value(&complex_data, "fields.#42.1.value"),
659            Some(&dv(json!(200)))
660        );
661
662        let multi_hash_data = dv(json!({
663            "data": {
664                "#fields": {
665                    "##": ["hash array"],
666                    "10": ["numeric array"]
667                }
668            }
669        }));
670
671        assert_eq!(
672            get_nested_value(&multi_hash_data, "data.##fields.###.0"),
673            Some(&dv(json!("hash array")))
674        );
675        assert_eq!(
676            get_nested_value(&multi_hash_data, "data.##fields.#10.0"),
677            Some(&dv(json!("numeric array")))
678        );
679    }
680
681    // ---------------------------------------------------------------------
682    // remove_nested_value
683    // ---------------------------------------------------------------------
684
685    /// Round-trip helper: the whole tree as `serde_json::Value`, for
686    /// byte-identical assertions after a `None` return.
687    fn as_json(v: &OwnedDataValue) -> serde_json::Value {
688        serde_json::Value::from(v)
689    }
690
691    #[test]
692    fn test_remove_nested_value_object() {
693        let mut data = dv(json!({"data": {"a": 1, "_b": 2}}));
694
695        assert_eq!(
696            remove_nested_value(&mut data, "data._b"),
697            Some(dv(json!(2)))
698        );
699        // Surviving pairs keep their relative order.
700        assert_eq!(as_json(&data), json!({"data": {"a": 1}}));
701
702        // Removing the same path twice: Some, then None.
703        assert_eq!(remove_nested_value(&mut data, "data._b"), None);
704        assert_eq!(as_json(&data), json!({"data": {"a": 1}}));
705    }
706
707    #[test]
708    fn test_remove_nested_value_array_shifts_tail() {
709        let mut data = dv(json!({"items": [1, 2, 3]}));
710
711        assert_eq!(
712            remove_nested_value(&mut data, "items.1"),
713            Some(dv(json!(2)))
714        );
715        // A tail shift, not a null hole.
716        assert_eq!(as_json(&data), json!({"items": [1, 3]}));
717    }
718
719    #[test]
720    fn test_remove_nested_value_returns_subtree_intact() {
721        let mut data = dv(json!({"data": {"nested": {"x": [1, 2]}}}));
722
723        assert_eq!(
724            remove_nested_value(&mut data, "data.nested"),
725            Some(dv(json!({"x": [1, 2]})))
726        );
727        assert_eq!(as_json(&data), json!({"data": {}}));
728    }
729
730    #[test]
731    fn test_remove_nested_value_traverses_array_in_non_terminal_position() {
732        let mut data = dv(json!({"a": [{"k": 1}, {"k": 2}]}));
733
734        assert_eq!(remove_nested_value(&mut data, "a.1.k"), Some(dv(json!(2))));
735        assert_eq!(as_json(&data), json!({"a": [{"k": 1}, {}]}));
736    }
737
738    #[test]
739    fn test_remove_hash_prefix_in_paths() {
740        // Same `#`-escape mapping asserted by test_hash_prefix_in_paths and
741        // test_set_hash_prefix_in_paths.
742        let mut data = dv(json!({"fields": {"20": "x", "#": "y", "##": "z"}}));
743
744        assert_eq!(
745            remove_nested_value(&mut data, "fields.#20"),
746            Some(dv(json!("x")))
747        );
748        assert_eq!(
749            remove_nested_value(&mut data, "fields.##"),
750            Some(dv(json!("y")))
751        );
752        assert_eq!(
753            remove_nested_value(&mut data, "fields.###"),
754            Some(dv(json!("z")))
755        );
756        assert_eq!(as_json(&data), json!({"fields": {}}));
757    }
758
759    #[test]
760    fn test_remove_nested_value_negative_cases_leave_tree_untouched() {
761        // Every one of these must return None *and* leave the tree
762        // byte-identical — asserted on the whole tree, not just the return.
763        let original = json!({
764            "items": [1, 2, 3],
765            "a": [{"k": 1}],
766            "data": {"x": 1},
767            "b": 1
768        });
769
770        for path in [
771            "",          // empty path
772            "data.nope", // missing key, last segment
773            "nope.x",    // missing key, parent segment
774            "items.9",   // out-of-bounds, terminal
775            "a.5.k",     // out-of-bounds, mid-path
776            "items.abc", // non-numeric array segment
777            "items.-1",  // negative array segment
778            "b.c",       // descent through a scalar
779            "b.c.d",     // deeper descent through a scalar
780        ] {
781            let mut data = dv(original.clone());
782            assert_eq!(
783                remove_nested_value(&mut data, path),
784                None,
785                "path '{path}' should not resolve"
786            );
787            assert_eq!(
788                as_json(&data),
789                original,
790                "path '{path}' must leave the tree untouched"
791            );
792        }
793    }
794
795    #[test]
796    fn test_remove_nested_value_scalar_root() {
797        let mut scalar = dv(json!("scalar"));
798        assert_eq!(remove_nested_value(&mut scalar, "a"), None);
799        assert_eq!(as_json(&scalar), json!("scalar"));
800    }
801
802    #[test]
803    fn test_remove_nested_value_non_ascii_keys() {
804        let mut data = dv(json!({"データ": {"ключ": "значение"}}));
805
806        assert_eq!(
807            remove_nested_value(&mut data, "データ.ключ"),
808            Some(dv(json!("значение")))
809        );
810        assert_eq!(as_json(&data), json!({"データ": {}}));
811    }
812}