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