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/// Get a reference to the value at `path`, walking the tree.
12///
13/// Path syntax:
14/// - `"user.name"` — object property
15/// - `"items.0"` — array index
16/// - `"user.addresses.0.city"` — mixed
17/// - `"data.#20"` — object key literally named `"20"` (strip one leading `#`)
18/// - `"data.##"` — object key literally named `"#"` (strip one leading `#`)
19///
20/// Returns `None` for missing keys, out-of-bounds indices, invalid index
21/// formats, or attempts to descend through a non-container.
22pub fn get_nested_value<'b>(data: &'b OwnedDataValue, path: &str) -> Option<&'b OwnedDataValue> {
23    if path.is_empty() {
24        return Some(data);
25    }
26
27    let mut current = data;
28
29    for part in path.split('.') {
30        match current {
31            OwnedDataValue::Object(pairs) => {
32                let key = strip_hash_prefix(part);
33                let slot = pairs.iter().find(|(k, _)| k == key)?;
34                current = &slot.1;
35            }
36            OwnedDataValue::Array(items) => {
37                let idx: usize = part.parse().ok()?;
38                current = items.get(idx)?;
39            }
40            _ => return None,
41        }
42    }
43
44    Some(current)
45}
46
47/// Set the value at `path`, creating intermediate containers as needed.
48///
49/// Mirrors the original `serde_json::Value` flavour:
50/// - intermediate containers are created on demand; the next path part
51///   determines whether to create an `Object` (string key) or `Array`
52///   (numeric index);
53/// - arrays grow with `OwnedDataValue::Null` padding when an index past
54///   the current end is assigned;
55/// - `#`-prefix escape applies inside object contexts only;
56/// - silently no-ops when traversing through a non-container in a non-
57///   terminal hop or when an array path part isn't a valid `usize`.
58pub fn set_nested_value(data: &mut OwnedDataValue, path: &str, value: OwnedDataValue) {
59    if path.is_empty() {
60        return;
61    }
62
63    let parts: Vec<&str> = path.split('.').collect();
64    let last = parts.len() - 1;
65    let mut current = data;
66
67    for (i, part) in parts.iter().enumerate() {
68        if i == last {
69            match current {
70                OwnedDataValue::Object(pairs) => {
71                    let key = strip_hash_prefix(part);
72                    if let Some(slot) = pairs.iter_mut().find(|(k, _)| k == key) {
73                        slot.1 = value;
74                    } else {
75                        pairs.push((key.to_string(), value));
76                    }
77                }
78                OwnedDataValue::Array(items) => {
79                    if let Ok(idx) = part.parse::<usize>() {
80                        while items.len() <= idx {
81                            items.push(OwnedDataValue::Null);
82                        }
83                        items[idx] = value;
84                    }
85                }
86                _ => {}
87            }
88            return;
89        }
90
91        // Non-terminal hop: locate-or-create the child and descend.
92        // Use the next part to decide whether the child container is an Array
93        // (next part parses as usize) or an Object (anything else).
94        let next_is_array = parts[i + 1].parse::<usize>().is_ok();
95
96        match current {
97            OwnedDataValue::Object(pairs) => {
98                let key = strip_hash_prefix(part);
99                let idx = match pairs.iter().position(|(k, _)| k == key) {
100                    Some(idx) => idx,
101                    None => {
102                        let child = if next_is_array {
103                            OwnedDataValue::Array(Vec::new())
104                        } else {
105                            OwnedDataValue::Object(Vec::new())
106                        };
107                        pairs.push((key.to_string(), child));
108                        pairs.len() - 1
109                    }
110                };
111                current = &mut pairs[idx].1;
112            }
113            OwnedDataValue::Array(items) => {
114                let Ok(idx) = part.parse::<usize>() else {
115                    return; // can't use a non-numeric key on an Array
116                };
117                while items.len() <= idx {
118                    items.push(OwnedDataValue::Null);
119                }
120                if matches!(items[idx], OwnedDataValue::Null) {
121                    items[idx] = if next_is_array {
122                        OwnedDataValue::Array(Vec::new())
123                    } else {
124                        OwnedDataValue::Object(Vec::new())
125                    };
126                }
127                current = &mut items[idx];
128            }
129            _ => return,
130        }
131    }
132}
133
134/// Clone the value at `path`, returning `None` if the path is unresolvable.
135#[inline]
136pub fn get_nested_value_cloned(data: &OwnedDataValue, path: &str) -> Option<OwnedDataValue> {
137    get_nested_value(data, path).cloned()
138}
139
140/// Same as `get_nested_value` but consumes a pre-split slice of path parts.
141/// Parts retain the original `#` prefix; `strip_hash_prefix` is applied at
142/// lookup time so the `#20` → "force object key 20" semantics still hold.
143pub fn get_nested_value_parts<'b>(
144    data: &'b OwnedDataValue,
145    parts: &[Arc<str>],
146) -> Option<&'b OwnedDataValue> {
147    if parts.is_empty() {
148        return Some(data);
149    }
150    let mut current = data;
151    for part in parts {
152        match current {
153            OwnedDataValue::Object(pairs) => {
154                let key = strip_hash_prefix(part);
155                let slot = pairs.iter().find(|(k, _)| k == key)?;
156                current = &slot.1;
157            }
158            OwnedDataValue::Array(items) => {
159                let idx: usize = part.parse().ok()?;
160                current = items.get(idx)?;
161            }
162            _ => return None,
163        }
164    }
165    Some(current)
166}
167
168/// Same as `set_nested_value` but consumes a pre-split slice of path parts.
169/// Parts retain the original `#` prefix; `strip_hash_prefix` is applied at
170/// use time. Crucially, the "is the NEXT segment an array index?" decision
171/// looks at the raw (unstripped) `parts[i+1]` — `#20` parses as non-numeric,
172/// so the child container is an Object (key "20"), not an Array.
173pub fn set_nested_value_parts(
174    data: &mut OwnedDataValue,
175    parts: &[Arc<str>],
176    value: OwnedDataValue,
177) {
178    if parts.is_empty() {
179        return;
180    }
181    let last = parts.len() - 1;
182    let mut current = data;
183
184    for (i, part) in parts.iter().enumerate() {
185        if i == last {
186            match current {
187                OwnedDataValue::Object(pairs) => {
188                    let key = strip_hash_prefix(part);
189                    if let Some(slot) = pairs.iter_mut().find(|(k, _)| k == key) {
190                        slot.1 = value;
191                    } else {
192                        pairs.push((key.to_string(), value));
193                    }
194                }
195                OwnedDataValue::Array(items) => {
196                    if let Ok(idx) = part.parse::<usize>() {
197                        while items.len() <= idx {
198                            items.push(OwnedDataValue::Null);
199                        }
200                        items[idx] = value;
201                    }
202                }
203                _ => {}
204            }
205            return;
206        }
207
208        let next_part: &str = &parts[i + 1];
209        let next_is_array = next_part.parse::<usize>().is_ok();
210
211        match current {
212            OwnedDataValue::Object(pairs) => {
213                let key = strip_hash_prefix(part);
214                let idx = match pairs.iter().position(|(k, _)| k == key) {
215                    Some(idx) => idx,
216                    None => {
217                        let child = if next_is_array {
218                            OwnedDataValue::Array(Vec::new())
219                        } else {
220                            OwnedDataValue::Object(Vec::new())
221                        };
222                        pairs.push((key.to_string(), child));
223                        pairs.len() - 1
224                    }
225                };
226                current = &mut pairs[idx].1;
227            }
228            OwnedDataValue::Array(items) => {
229                let Ok(idx) = part.parse::<usize>() else {
230                    return;
231                };
232                while items.len() <= idx {
233                    items.push(OwnedDataValue::Null);
234                }
235                if matches!(items[idx], OwnedDataValue::Null) {
236                    items[idx] = if next_is_array {
237                        OwnedDataValue::Array(Vec::new())
238                    } else {
239                        OwnedDataValue::Object(Vec::new())
240                    };
241                }
242                current = &mut items[idx];
243            }
244            _ => return,
245        }
246    }
247}
248
249/// Strip exactly one leading `#` from an object-key path component.
250/// `"#20"` → `"20"`, `"##"` → `"#"`, `"foo"` → `"foo"`.
251#[inline]
252fn strip_hash_prefix(part: &str) -> &str {
253    part.strip_prefix('#').unwrap_or(part)
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use serde_json::json;
260
261    /// Test-only helper: build OwnedDataValue from a `json!` literal.
262    fn dv(v: serde_json::Value) -> OwnedDataValue {
263        OwnedDataValue::from(&v)
264    }
265
266    #[test]
267    fn test_get_nested_value() {
268        let data = dv(json!({
269            "user": {
270                "name": "John",
271                "age": 30,
272                "addresses": [
273                    {"city": "New York", "zip": "10001"},
274                    {"city": "San Francisco", "zip": "94102"}
275                ],
276                "preferences": {
277                    "theme": "dark",
278                    "notifications": true
279                }
280            },
281            "items": [1, 2, 3]
282        }));
283
284        assert_eq!(
285            get_nested_value(&data, "user.name"),
286            Some(&dv(json!("John")))
287        );
288        assert_eq!(get_nested_value(&data, "user.age"), Some(&dv(json!(30))));
289
290        assert_eq!(
291            get_nested_value(&data, "user.preferences.theme"),
292            Some(&dv(json!("dark")))
293        );
294        assert_eq!(
295            get_nested_value(&data, "user.preferences.notifications"),
296            Some(&dv(json!(true)))
297        );
298
299        assert_eq!(get_nested_value(&data, "items.0"), Some(&dv(json!(1))));
300        assert_eq!(get_nested_value(&data, "items.2"), Some(&dv(json!(3))));
301
302        assert_eq!(
303            get_nested_value(&data, "user.addresses.0.city"),
304            Some(&dv(json!("New York")))
305        );
306        assert_eq!(
307            get_nested_value(&data, "user.addresses.1.zip"),
308            Some(&dv(json!("94102")))
309        );
310
311        assert_eq!(get_nested_value(&data, "user.missing"), None);
312        assert_eq!(get_nested_value(&data, "items.10"), None);
313        assert_eq!(get_nested_value(&data, "user.addresses.2.city"), None);
314        assert_eq!(get_nested_value(&data, "nonexistent.path"), None);
315    }
316
317    #[test]
318    fn test_set_nested_value() {
319        let mut data = dv(json!({}));
320
321        set_nested_value(&mut data, "name", dv(json!("Alice")));
322        assert_eq!(data, dv(json!({"name": "Alice"})));
323
324        set_nested_value(&mut data, "user.email", dv(json!("alice@example.com")));
325        assert_eq!(
326            data,
327            dv(json!({
328                "name": "Alice",
329                "user": {"email": "alice@example.com"}
330            }))
331        );
332
333        set_nested_value(&mut data, "name", dv(json!("Bob")));
334        assert_eq!(
335            data,
336            dv(json!({
337                "name": "Bob",
338                "user": {"email": "alice@example.com"}
339            }))
340        );
341
342        set_nested_value(&mut data, "settings.theme.mode", dv(json!("dark")));
343        assert_eq!(data["settings"]["theme"]["mode"], dv(json!("dark")));
344
345        set_nested_value(&mut data, "user.age", dv(json!(25)));
346        assert_eq!(data["user"]["age"], dv(json!(25)));
347        assert_eq!(data["user"]["email"], dv(json!("alice@example.com")));
348    }
349
350    #[test]
351    fn test_set_nested_value_with_arrays() {
352        let mut data = dv(json!({ "items": [1, 2, 3] }));
353
354        set_nested_value(&mut data, "items.0", dv(json!(10)));
355        assert_eq!(data["items"], dv(json!([10, 2, 3])));
356
357        set_nested_value(&mut data, "items.5", dv(json!(50)));
358        assert_eq!(data["items"], dv(json!([10, 2, 3, null, null, 50])));
359
360        let mut data2 = dv(json!({}));
361        set_nested_value(&mut data2, "matrix.0.0", dv(json!(1)));
362        set_nested_value(&mut data2, "matrix.0.1", dv(json!(2)));
363        set_nested_value(&mut data2, "matrix.1.0", dv(json!(3)));
364        assert_eq!(data2, dv(json!({ "matrix": [[1, 2], [3]] })));
365    }
366
367    #[test]
368    fn test_set_nested_value_array_expansion() {
369        let mut data = dv(json!({}));
370
371        set_nested_value(&mut data, "array.2", dv(json!("value")));
372        assert_eq!(data, dv(json!({ "array": [null, null, "value"] })));
373
374        let mut data2 = dv(json!({}));
375        set_nested_value(&mut data2, "deep.nested.0.field", dv(json!("test")));
376        assert_eq!(
377            data2,
378            dv(json!({ "deep": { "nested": [{ "field": "test" }] } }))
379        );
380    }
381
382    #[test]
383    fn test_get_nested_value_cloned() {
384        let data = dv(json!({
385            "user": {
386                "profile": {
387                    "name": "Alice",
388                    "settings": {"theme": "dark"}
389                }
390            }
391        }));
392
393        assert_eq!(
394            get_nested_value_cloned(&data, "user.profile.name"),
395            Some(dv(json!("Alice")))
396        );
397        assert_eq!(
398            get_nested_value_cloned(&data, "user.profile.settings"),
399            Some(dv(json!({ "theme": "dark" })))
400        );
401        assert_eq!(get_nested_value_cloned(&data, "user.missing"), None);
402    }
403
404    #[test]
405    fn test_get_nested_value_bounds_checking() {
406        let data = dv(json!({
407            "items": [1, 2, 3],
408            "nested": {
409                "array": [
410                    {"id": 1},
411                    {"id": 2}
412                ]
413            }
414        }));
415
416        assert_eq!(get_nested_value(&data, "items.0"), Some(&dv(json!(1))));
417        assert_eq!(get_nested_value(&data, "items.2"), Some(&dv(json!(3))));
418
419        assert_eq!(get_nested_value(&data, "items.10"), None);
420        assert_eq!(get_nested_value(&data, "items.999999"), None);
421
422        assert_eq!(get_nested_value(&data, "items.abc"), None);
423        assert_eq!(get_nested_value(&data, "items.-1"), None);
424        assert_eq!(get_nested_value(&data, "items.2.5"), None);
425
426        assert_eq!(
427            get_nested_value(&data, "nested.array.0.id"),
428            Some(&dv(json!(1)))
429        );
430        assert_eq!(get_nested_value(&data, "nested.array.5.id"), None);
431
432        assert_eq!(get_nested_value(&data, ""), Some(&data));
433    }
434
435    #[test]
436    fn test_set_nested_value_bounds_safety() {
437        let mut data = dv(json!({}));
438
439        set_nested_value(&mut data, "large.10", dv(json!("value")));
440        assert_eq!(data["large"].as_array().unwrap().len(), 11);
441        assert_eq!(data["large"][10], dv(json!("value")));
442        for i in 0..10usize {
443            assert_eq!(data["large"][i], dv(json!(null)));
444        }
445
446        let mut data2 = dv(json!({ "matrix": [] }));
447        set_nested_value(&mut data2, "matrix.2.1", dv(json!(5)));
448        assert_eq!(data2["matrix"][0], dv(json!(null)));
449        assert_eq!(data2["matrix"][1], dv(json!(null)));
450        assert_eq!(data2["matrix"][2][0], dv(json!(null)));
451        assert_eq!(data2["matrix"][2][1], dv(json!(5)));
452
453        let mut data3 = dv(json!({ "arr": [1, 2, 3] }));
454        set_nested_value(&mut data3, "arr.1", dv(json!("replaced")));
455        assert_eq!(data3["arr"], dv(json!([1, "replaced", 3])));
456    }
457
458    #[test]
459    fn test_hash_prefix_in_paths() {
460        let data = dv(json!({
461            "fields": {
462                "20": "numeric field name",
463                "#": "hash field",
464                "##": "double hash field",
465                "normal": "normal field"
466            }
467        }));
468
469        assert_eq!(
470            get_nested_value(&data, "fields.#20"),
471            Some(&dv(json!("numeric field name")))
472        );
473        assert_eq!(
474            get_nested_value(&data, "fields.##"),
475            Some(&dv(json!("hash field")))
476        );
477        assert_eq!(
478            get_nested_value(&data, "fields.###"),
479            Some(&dv(json!("double hash field")))
480        );
481        assert_eq!(
482            get_nested_value(&data, "fields.normal"),
483            Some(&dv(json!("normal field")))
484        );
485        assert_eq!(get_nested_value(&data, "fields.#999"), None);
486    }
487
488    #[test]
489    fn test_set_hash_prefix_in_paths() {
490        let mut data = dv(json!({}));
491
492        set_nested_value(&mut data, "fields.#20", dv(json!("value for 20")));
493        assert_eq!(data["fields"]["20"], dv(json!("value for 20")));
494
495        set_nested_value(&mut data, "fields.##", dv(json!("hash value")));
496        assert_eq!(data["fields"]["#"], dv(json!("hash value")));
497
498        set_nested_value(&mut data, "fields.###", dv(json!("double hash value")));
499        assert_eq!(data["fields"]["##"], dv(json!("double hash value")));
500
501        set_nested_value(&mut data, "fields.normal", dv(json!("normal value")));
502        assert_eq!(data["fields"]["normal"], dv(json!("normal value")));
503
504        assert_eq!(
505            data,
506            dv(json!({
507                "fields": {
508                    "20": "value for 20",
509                    "#": "hash value",
510                    "##": "double hash value",
511                    "normal": "normal value"
512                }
513            }))
514        );
515    }
516
517    #[test]
518    fn test_hash_prefix_with_arrays() {
519        let mut data = dv(json!({
520            "items": [
521                {"0": "field named zero", "id": 1},
522                {"1": "field named one", "id": 2}
523            ]
524        }));
525
526        assert_eq!(
527            get_nested_value(&data, "items.0.#0"),
528            Some(&dv(json!("field named zero")))
529        );
530        assert_eq!(
531            get_nested_value(&data, "items.1.#1"),
532            Some(&dv(json!("field named one")))
533        );
534
535        set_nested_value(&mut data, "items.0.#2", dv(json!("field named two")));
536        assert_eq!(data["items"][0]["2"], dv(json!("field named two")));
537
538        assert_eq!(get_nested_value(&data, "items.0.id"), Some(&dv(json!(1))));
539        assert_eq!(get_nested_value(&data, "items.1.id"), Some(&dv(json!(2))));
540    }
541
542    #[test]
543    fn test_hash_prefix_field_with_array_value() {
544        let data = dv(json!({
545            "data": {
546                "fields": {
547                    "72": ["first", "second", "third"],
548                    "100": ["alpha", "beta", "gamma"],
549                    "normal": ["one", "two", "three"]
550                }
551            }
552        }));
553
554        assert_eq!(
555            get_nested_value(&data, "data.fields.#72.0"),
556            Some(&dv(json!("first")))
557        );
558        assert_eq!(
559            get_nested_value(&data, "data.fields.#72.1"),
560            Some(&dv(json!("second")))
561        );
562        assert_eq!(
563            get_nested_value(&data, "data.fields.#72.2"),
564            Some(&dv(json!("third")))
565        );
566
567        assert_eq!(
568            get_nested_value(&data, "data.fields.#100.0"),
569            Some(&dv(json!("alpha")))
570        );
571        assert_eq!(
572            get_nested_value(&data, "data.fields.#100.1"),
573            Some(&dv(json!("beta")))
574        );
575
576        assert_eq!(
577            get_nested_value(&data, "data.fields.normal.0"),
578            Some(&dv(json!("one")))
579        );
580
581        let mut data_mut = data.clone();
582        set_nested_value(&mut data_mut, "data.fields.#72.0", dv(json!("modified")));
583        assert_eq!(data_mut["data"]["fields"]["72"][0], dv(json!("modified")));
584
585        set_nested_value(&mut data_mut, "data.fields.#999.0", dv(json!("new value")));
586        assert_eq!(data_mut["data"]["fields"]["999"][0], dv(json!("new value")));
587
588        let complex_data = dv(json!({
589            "fields": {
590                "42": [
591                    {"name": "item1", "value": 100},
592                    {"name": "item2", "value": 200}
593                ]
594            }
595        }));
596
597        assert_eq!(
598            get_nested_value(&complex_data, "fields.#42.0.name"),
599            Some(&dv(json!("item1")))
600        );
601        assert_eq!(
602            get_nested_value(&complex_data, "fields.#42.1.value"),
603            Some(&dv(json!(200)))
604        );
605
606        let multi_hash_data = dv(json!({
607            "data": {
608                "#fields": {
609                    "##": ["hash array"],
610                    "10": ["numeric array"]
611                }
612            }
613        }));
614
615        assert_eq!(
616            get_nested_value(&multi_hash_data, "data.##fields.###.0"),
617            Some(&dv(json!("hash array")))
618        );
619        assert_eq!(
620            get_nested_value(&multi_hash_data, "data.##fields.#10.0"),
621            Some(&dv(json!("numeric array")))
622        );
623    }
624}