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