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