Skip to main content

agent_first_data/document/
traverse.rs

1//! Core dot-path traversal for get/set operations.
2
3use crate::document::{
4    DocumentError, DocumentResult, Value,
5    keyed::{Addressing, KeyedList},
6    path::{join_path, parse_path},
7};
8
9/// Resolve one non-numeric segment against `array`, returning the index of the
10/// single element it names.
11///
12/// Shared by the read and write walks so both answer a given address the same
13/// way — and so the refusals stay in one place: an array nothing claims is
14/// [`DocumentError::UnregisteredArray`], no match is
15/// [`DocumentError::SlugNotFound`], and several matches is
16/// [`DocumentError::AmbiguousMatch`] rather than the first one.
17/// `-` is not a name but the append position, which only `set` writes to,
18/// and only as the last segment of its path. It is reserved on every array
19/// and in every verb, so an element whose slug happens to be `-` is reached by
20/// index instead: were it a name anywhere, `get a.-` would read an element
21/// that `set a.-` then appends beside. Reporting it as an unnamed element
22/// would send the caller looking for a naming rule.
23pub(crate) fn append_position_refused(prefix: &[String], last: bool) -> DocumentError {
24    let detail = if last {
25        format!(
26            "`-` names the append position after the last element of `{}`, which is only \
27             valid as the last segment of a `set` path; address an existing element by index",
28            join_path(prefix)
29        )
30    } else {
31        format!(
32            "`-` names the append position of `{}` and can only be the last segment of a \
33             path; address an existing element by index",
34            join_path(prefix)
35        )
36    };
37    DocumentError::PathSyntax { detail }
38}
39
40fn resolve_named_element(
41    array: &[Value],
42    segment: &str,
43    prefix: &[String],
44    last: bool,
45    addressing: Addressing<'_>,
46) -> DocumentResult<usize> {
47    if segment == "-" {
48        return Err(append_position_refused(prefix, last));
49    }
50    // A caller's explicit declaration outranks a format-wide rule: it names one
51    // array on purpose, so it is the more specific statement about this one.
52    if let Some(registration) = addressing
53        .keyed_lists
54        .iter()
55        .find(|list| keyed_prefix_matches(list, prefix))
56    {
57        let hits = array
58            .iter()
59            .enumerate()
60            .filter(|(_, element)| {
61                element.get(registration.slug_field).and_then(Value::as_str) == Some(segment)
62            })
63            .map(|(index, _)| index)
64            .collect();
65        return resolve_unique_hit(hits, segment, &join_path(prefix));
66    }
67
68    let Some(rule) = addressing.array_rule else {
69        return Err(DocumentError::UnregisteredArray {
70            path: join_path(prefix),
71        });
72    };
73
74    let hits: Vec<usize> = array
75        .iter()
76        .enumerate()
77        .filter(|(_, element)| rule.matches(element, segment))
78        .map(|(index, _)| index)
79        .collect();
80    resolve_unique_hit(hits, segment, &join_path(prefix))
81}
82
83fn resolve_unique_hit(hits: Vec<usize>, segment: &str, prefix: &str) -> DocumentResult<usize> {
84    match hits.as_slice() {
85        [] => Err(DocumentError::SlugNotFound {
86            prefix: prefix.to_string(),
87            slug: segment.to_string(),
88        }),
89        [only] => Ok(*only),
90        several => Err(DocumentError::AmbiguousMatch {
91            prefix: prefix.to_string(),
92            segment: segment.to_string(),
93            indices: several.to_vec(),
94        }),
95    }
96}
97
98/// Parse an array index without letting decimal overflow fall through to named
99/// lookup.
100///
101/// `str::parse::<usize>()` alone cannot distinguish "not a number" from "a
102/// number too large for this platform". The former may be a slug; the latter
103/// is still lexically an index and must fail as one rather than unexpectedly
104/// matching document content.
105fn array_index(segment: &str) -> DocumentResult<Option<usize>> {
106    if segment.is_empty() || !segment.bytes().all(|byte| byte.is_ascii_digit()) {
107        return Ok(None);
108    }
109    segment
110        .parse::<usize>()
111        .map(Some)
112        .map_err(|_| DocumentError::PathSyntax {
113            detail: "array index exceeds the platform index range".to_string(),
114        })
115}
116
117/// Get a value at the given dot-path.
118///
119/// Handles:
120/// - Object field access (any level of nesting)
121/// - Named array access (keyed-list slug, or a format's own array rule)
122/// - Greedy key matching for keys containing '.'
123pub fn get_path_ref<'a>(
124    root: &'a Value,
125    path: &str,
126    addressing: Addressing<'_>,
127) -> DocumentResult<&'a Value> {
128    if path.is_empty() {
129        return Err(DocumentError::EmptyPath);
130    }
131
132    let segments = parse_path(path)?;
133    let mut current = root;
134    let mut accumulated_prefix: Vec<String> = Vec::new();
135    let mut seg_idx = 0;
136
137    while seg_idx < segments.len() {
138        let current_seg = segments[seg_idx].as_str();
139
140        match current {
141            Value::Object(obj) => {
142                // Try exact match first
143                if let Some(next) = obj.get(current_seg) {
144                    accumulated_prefix.push(current_seg.to_string());
145                    current = next;
146                    seg_idx += 1;
147                } else {
148                    return Err(DocumentError::UnknownSegment {
149                        path: path.to_string(),
150                        segment: current_seg.to_string(),
151                    });
152                }
153            }
154            Value::Array(arr) => {
155                // Numeric index takes priority over keyed-list slug.
156                if let Some(arr_idx) = array_index(current_seg)? {
157                    let elem = arr
158                        .get(arr_idx)
159                        .ok_or_else(|| DocumentError::IndexOutOfBounds {
160                            path: join_path(&accumulated_prefix),
161                            index: arr_idx,
162                            len: arr.len(),
163                        })?;
164                    accumulated_prefix.push(current_seg.to_string());
165                    current = elem;
166                    seg_idx += 1;
167                } else {
168                    let index = resolve_named_element(
169                        arr,
170                        current_seg,
171                        &accumulated_prefix,
172                        seg_idx + 1 == segments.len(),
173                        addressing,
174                    )?;
175                    current = &arr[index];
176                    accumulated_prefix.push(current_seg.to_string());
177                    seg_idx += 1;
178                }
179            }
180            _ => {
181                return Err(DocumentError::NotTraversable {
182                    path: join_path(&accumulated_prefix),
183                    got: current.kind_name().to_string(),
184                });
185            }
186        }
187    }
188
189    Ok(current)
190}
191
192/// Get a cloned value at the given dot-path.
193pub fn get_path(root: &Value, path: &str, addressing: Addressing<'_>) -> DocumentResult<Value> {
194    Ok(get_path_ref(root, path, addressing)?.clone())
195}
196
197/// Rewrite `path` into the equivalent index-only address for `root`.
198///
199/// Content addressing is something only the parsed value can answer, and the
200/// source-preserving writers never see it: each backend re-walks the original
201/// text by path, and nothing in `identities.me` tells a TOML editor which
202/// element `me` is. So a write resolves the address once, here, where the whole
203/// document is in hand, and hands the backends the canonical `identities.0`.
204/// The alternative — teaching six backends to match on content — would give
205/// each its own chance to disagree with the read walk about what an address
206/// means.
207///
208/// Resolution stops at the first segment the document does not have and copies
209/// the rest verbatim, because `set` creates missing object parents and a node
210/// that does not exist yet cannot be addressed by its content anyway. Whatever
211/// is wrong with the tail is then the writer's error to report, unchanged.
212///
213/// Two prefixes are tracked because they answer different questions: the
214/// *semantic* one (the caller's own segments) is what a [`KeyedList`]
215/// registration is matched against, so a registration keeps meaning what it
216/// meant on the read path, while the *canonical* one (resolved indices) is what
217/// gets returned.
218pub fn resolve_path(
219    root: &Value,
220    path: &str,
221    addressing: Addressing<'_>,
222) -> DocumentResult<String> {
223    if path.is_empty() {
224        return Ok(String::new());
225    }
226
227    let segments = parse_path(path)?;
228    let mut current = root;
229    let mut semantic: Vec<String> = Vec::new();
230    let mut canonical: Vec<String> = Vec::with_capacity(segments.len());
231
232    for (idx, segment) in segments.iter().enumerate() {
233        let copy_rest = |canonical: &mut Vec<String>| {
234            canonical.extend(segments[idx..].iter().cloned());
235        };
236        match current {
237            Value::Object(object) => match object.get(segment.as_str()) {
238                Some(next) => {
239                    canonical.push(segment.clone());
240                    semantic.push(segment.clone());
241                    current = next;
242                }
243                None => {
244                    copy_rest(&mut canonical);
245                    break;
246                }
247            },
248            Value::Array(array) => {
249                if segment == "-" && idx + 1 == segments.len() {
250                    canonical.push(segment.clone());
251                    break;
252                }
253                let index = match array_index(segment)? {
254                    Some(index) => index,
255                    None => resolve_named_element(
256                        array,
257                        segment,
258                        &semantic,
259                        idx + 1 == segments.len(),
260                        addressing,
261                    )?,
262                };
263                let Some(next) = array.get(index) else {
264                    copy_rest(&mut canonical);
265                    break;
266                };
267                canonical.push(index.to_string());
268                semantic.push(segment.clone());
269                current = next;
270            }
271            _ => {
272                copy_rest(&mut canonical);
273                break;
274            }
275        }
276    }
277
278    Ok(join_path(&canonical))
279}
280
281/// Set a value at the given dot-path. `value` is inserted as-is at the leaf —
282/// no coercion happens here; callers that accept CLI strings (e.g. the
283/// `afdata` binary) construct the typed `Value` first, via
284/// [`crate::document::coerce::value_from_type`] (an explicit `--value-type`)
285/// or a bare `Value::String` (zero coercion), before calling this.
286pub fn set_path(
287    root: &mut Value,
288    path: &str,
289    value: &Value,
290    addressing: Addressing<'_>,
291) -> DocumentResult<()> {
292    if path.is_empty() {
293        return Err(DocumentError::EmptyPath);
294    }
295
296    let segments = parse_path(path)?;
297    set_path_recursive(root, &segments, 0, &mut Vec::new(), addressing, value)
298}
299
300fn set_path_recursive(
301    current: &mut Value,
302    segments: &[String],
303    idx: usize,
304    accumulated_prefix: &mut Vec<String>,
305    addressing: Addressing<'_>,
306    value: &Value,
307) -> DocumentResult<()> {
308    if idx >= segments.len() {
309        return Err(DocumentError::EmptyPath);
310    }
311
312    let current_seg = segments[idx].as_str();
313    let is_last = idx == segments.len() - 1;
314
315    match current {
316        Value::Object(obj) => {
317            // Path parsing makes dotted keys explicit via `\\.`.
318            let key_to_use = current_seg.to_string();
319
320            let segments_to_consume = 1;
321
322            if is_last {
323                // At leaf: insert the typed value directly.
324                obj.insert(key_to_use, value.clone());
325                Ok(())
326            } else {
327                // Not at leaf: ensure key exists and recurse
328                let next_idx = idx + segments_to_consume;
329                if next_idx >= segments.len() {
330                    return Err(DocumentError::EmptyPath);
331                }
332
333                accumulated_prefix.push(key_to_use.clone());
334
335                // Use entry API to avoid double borrow
336                use std::collections::btree_map::Entry;
337                match obj.entry(key_to_use) {
338                    Entry::Occupied(mut ent) => set_path_recursive(
339                        ent.get_mut(),
340                        segments,
341                        next_idx,
342                        accumulated_prefix,
343                        addressing,
344                        value,
345                    ),
346                    Entry::Vacant(ent) => {
347                        let mut new_obj = Value::Object(Default::default());
348                        let result = set_path_recursive(
349                            &mut new_obj,
350                            segments,
351                            next_idx,
352                            accumulated_prefix,
353                            addressing,
354                            value,
355                        );
356                        if result.is_ok() {
357                            ent.insert(new_obj);
358                        }
359                        result
360                    }
361                }
362            }
363        }
364        Value::Array(arr) => {
365            if current_seg == "-" && is_last {
366                arr.push(value.clone());
367                return Ok(());
368            }
369            // Numeric index takes priority over keyed-list slug.
370            if let Some(arr_idx) = array_index(current_seg)? {
371                if arr_idx >= arr.len() {
372                    return Err(DocumentError::IndexOutOfBounds {
373                        path: join_path(accumulated_prefix),
374                        index: arr_idx,
375                        len: arr.len(),
376                    });
377                }
378                if is_last {
379                    arr[arr_idx] = value.clone();
380                    Ok(())
381                } else {
382                    accumulated_prefix.push(current_seg.to_string());
383                    set_path_recursive(
384                        &mut arr[arr_idx],
385                        segments,
386                        idx + 1,
387                        accumulated_prefix,
388                        addressing,
389                        value,
390                    )
391                }
392            } else {
393                let elem_idx = resolve_named_element(
394                    arr,
395                    current_seg,
396                    accumulated_prefix,
397                    is_last,
398                    addressing,
399                )?;
400                if is_last {
401                    Err(DocumentError::UnsupportedOperation {
402                        format: "keyed list".to_string(),
403                        operation: "set".to_string(),
404                        detail:
405                            "a keyed-list slug resolves to an element; set a child field instead"
406                                .to_string(),
407                    })
408                } else {
409                    accumulated_prefix.push(current_seg.to_string());
410                    set_path_recursive(
411                        &mut arr[elem_idx],
412                        segments,
413                        idx + 1,
414                        accumulated_prefix,
415                        addressing,
416                        value,
417                    )
418                }
419            }
420        }
421        _ => Err(DocumentError::NotTraversable {
422            path: join_path(accumulated_prefix),
423            got: current.kind_name().to_string(),
424        }),
425    }
426}
427
428pub(crate) fn keyed_prefix_matches(
429    registration: &KeyedList<'_>,
430    semantic_prefix: &[String],
431) -> bool {
432    if registration.prefix.is_empty() {
433        return semantic_prefix.is_empty();
434    }
435    crate::document::parse_path(registration.prefix)
436        .ok()
437        .is_some_and(|segments| segments == semantic_prefix)
438}
439
440/// Remove the key at the given dot-path from its parent object.
441///
442/// This is the free-fn "remove a key" verb (paired with keyed-element removal
443/// via [`crate::document::remove_keyed`]).
444pub fn unset_path(root: &mut Value, path: &str) -> DocumentResult<()> {
445    if path.is_empty() {
446        return Err(DocumentError::EmptyPath);
447    }
448    let segments = parse_path(path)?;
449    unset_path_recursive(root, &segments, 0, &mut Vec::new())
450}
451
452fn unset_path_recursive(
453    current: &mut Value,
454    segments: &[String],
455    idx: usize,
456    accumulated_prefix: &mut Vec<String>,
457) -> DocumentResult<()> {
458    if idx >= segments.len() {
459        return Err(DocumentError::EmptyPath);
460    }
461    let current_seg = segments[idx].as_str();
462    let is_last = idx == segments.len() - 1;
463
464    match current {
465        Value::Object(obj) => {
466            let key_to_use = current_seg.to_string();
467            let segments_to_consume = 1;
468
469            if is_last {
470                if obj.remove(&key_to_use).is_none() {
471                    return Err(DocumentError::PathNotFound { path: key_to_use });
472                }
473                Ok(())
474            } else {
475                let next_idx = idx + segments_to_consume;
476                accumulated_prefix.push(key_to_use.clone());
477                if let Some(next) = obj.get_mut(&key_to_use) {
478                    unset_path_recursive(next, segments, next_idx, accumulated_prefix)
479                } else {
480                    Err(DocumentError::PathNotFound {
481                        path: join_path(accumulated_prefix),
482                    })
483                }
484            }
485        }
486        Value::Array(arr) => {
487            if let Some(arr_idx) = array_index(current_seg)? {
488                if arr_idx >= arr.len() {
489                    return Err(DocumentError::IndexOutOfBounds {
490                        path: join_path(accumulated_prefix),
491                        index: arr_idx,
492                        len: arr.len(),
493                    });
494                }
495                if is_last {
496                    arr.remove(arr_idx);
497                    Ok(())
498                } else {
499                    accumulated_prefix.push(current_seg.to_string());
500                    unset_path_recursive(&mut arr[arr_idx], segments, idx + 1, accumulated_prefix)
501                }
502            } else if current_seg == "-" {
503                Err(append_position_refused(accumulated_prefix, is_last))
504            } else {
505                Err(DocumentError::UnregisteredArray {
506                    path: join_path(accumulated_prefix),
507                })
508            }
509        }
510        _ => Err(DocumentError::NotTraversable {
511            path: join_path(accumulated_prefix),
512            got: current.kind_name().to_string(),
513        }),
514    }
515}
516
517#[cfg(test)]
518mod tests {
519    #![allow(
520        clippy::unwrap_used,
521        clippy::panic,
522        clippy::expect_used,
523        clippy::bool_assert_comparison
524    )]
525    use super::*;
526
527    fn make_test_object() -> Value {
528        let mut root = Value::Object(Default::default());
529        let mut imap = Value::Object(Default::default());
530        imap.as_object_mut().unwrap().insert(
531            "host".to_string(),
532            Value::String("mail.example.com".to_string()),
533        );
534        imap.as_object_mut()
535            .unwrap()
536            .insert("port".to_string(), Value::Integer(993));
537
538        root.as_object_mut()
539            .unwrap()
540            .insert("imap".to_string(), imap);
541
542        root
543    }
544
545    #[test]
546    fn test_get_path_simple() {
547        let root = make_test_object();
548        let result = get_path(&root, "imap.host", Addressing::INDEX_ONLY).unwrap();
549        assert_eq!(result.as_str().unwrap(), "mail.example.com");
550    }
551
552    #[test]
553    fn test_get_path_integer() {
554        let root = make_test_object();
555        let result = get_path(&root, "imap.port", Addressing::INDEX_ONLY).unwrap();
556        assert_eq!(result.as_integer().unwrap(), 993);
557    }
558
559    #[test]
560    fn test_set_path_new_key() {
561        let mut root = make_test_object();
562        set_path(
563            &mut root,
564            "imap.tls",
565            &Value::Bool(true),
566            Addressing::INDEX_ONLY,
567        )
568        .unwrap();
569
570        let result = get_path(&root, "imap.tls", Addressing::INDEX_ONLY).unwrap();
571        assert_eq!(result.as_bool().unwrap(), true);
572    }
573
574    #[test]
575    fn test_set_path_overwrite() {
576        let mut root = make_test_object();
577        set_path(
578            &mut root,
579            "imap.port",
580            &Value::Integer(587),
581            Addressing::INDEX_ONLY,
582        )
583        .unwrap();
584
585        let result = get_path(&root, "imap.port", Addressing::INDEX_ONLY).unwrap();
586        assert_eq!(result.as_integer().unwrap(), 587);
587    }
588
589    #[test]
590    fn test_set_path_array_value() {
591        let mut root = Value::Object(Default::default());
592        let value = Value::Array(vec![
593            Value::String("dev".to_string()),
594            Value::String("staging".to_string()),
595        ]);
596        set_path(&mut root, "tags", &value, Addressing::INDEX_ONLY).unwrap();
597
598        let result = get_path(&root, "tags", Addressing::INDEX_ONLY).unwrap();
599        let arr = result.as_array().unwrap();
600        assert_eq!(arr.len(), 2);
601    }
602
603    fn make_steps_object() -> Value {
604        // { "steps": [{"name": "a", "port": 1}, {"name": "b", "port": 2}] }
605        let mut root = Value::Object(Default::default());
606        let mut s0 = Value::Object(Default::default());
607        s0.as_object_mut()
608            .unwrap()
609            .insert("name".to_string(), Value::String("a".to_string()));
610        s0.as_object_mut()
611            .unwrap()
612            .insert("port".to_string(), Value::Integer(1));
613        let mut s1 = Value::Object(Default::default());
614        s1.as_object_mut()
615            .unwrap()
616            .insert("name".to_string(), Value::String("b".to_string()));
617        s1.as_object_mut()
618            .unwrap()
619            .insert("port".to_string(), Value::Integer(2));
620        root.as_object_mut()
621            .unwrap()
622            .insert("steps".to_string(), Value::Array(vec![s0, s1]));
623        root
624    }
625
626    #[test]
627    fn test_get_path_numeric_index() {
628        let root = make_steps_object();
629        let name = get_path(&root, "steps.0.name", Addressing::INDEX_ONLY).unwrap();
630        assert_eq!(name.as_str().unwrap(), "a");
631        let port = get_path(&root, "steps.1.port", Addressing::INDEX_ONLY).unwrap();
632        assert_eq!(port.as_integer().unwrap(), 2);
633    }
634
635    #[test]
636    fn test_set_path_numeric_index() {
637        let mut root = make_steps_object();
638        set_path(
639            &mut root,
640            "steps.0.port",
641            &Value::Integer(99),
642            Addressing::INDEX_ONLY,
643        )
644        .unwrap();
645        let result = get_path(&root, "steps.0.port", Addressing::INDEX_ONLY).unwrap();
646        assert_eq!(result.as_integer().unwrap(), 99);
647        // other element unchanged
648        let other = get_path(&root, "steps.1.port", Addressing::INDEX_ONLY).unwrap();
649        assert_eq!(other.as_integer().unwrap(), 2);
650    }
651
652    #[test]
653    fn test_get_path_index_out_of_bounds() {
654        let root = make_steps_object();
655        let err = get_path(&root, "steps.5.name", Addressing::INDEX_ONLY).unwrap_err();
656        assert!(matches!(
657            err,
658            DocumentError::IndexOutOfBounds {
659                index: 5,
660                len: 2,
661                ..
662            }
663        ));
664    }
665
666    #[test]
667    fn test_remove_path_numeric_index() {
668        let mut root = make_steps_object();
669        unset_path(&mut root, "steps.0").unwrap();
670        let arr = get_path(&root, "steps", Addressing::INDEX_ONLY).unwrap();
671        let arr = arr.as_array().unwrap();
672        assert_eq!(arr.len(), 1);
673        assert_eq!(arr[0].get("name").unwrap().as_str().unwrap(), "b");
674    }
675
676    #[test]
677    fn named_root_array_keeps_a_semantic_prefix_for_nested_keyed_lists() {
678        let mut child = Value::Object(Default::default());
679        child
680            .as_object_mut()
681            .unwrap()
682            .insert("id".to_string(), Value::String("beta".to_string()));
683        child
684            .as_object_mut()
685            .unwrap()
686            .insert("value".to_string(), Value::Integer(1));
687
688        let mut parent = Value::Object(Default::default());
689        parent
690            .as_object_mut()
691            .unwrap()
692            .insert("id".to_string(), Value::String("alpha".to_string()));
693        parent
694            .as_object_mut()
695            .unwrap()
696            .insert("children".to_string(), Value::Array(vec![child]));
697
698        let mut root = Value::Array(vec![parent]);
699        let keyed = [
700            KeyedList {
701                prefix: "",
702                slug_field: "id",
703            },
704            KeyedList {
705                prefix: "alpha.children",
706                slug_field: "id",
707            },
708        ];
709        let addressing = Addressing::keyed(&keyed);
710
711        assert_eq!(
712            get_path(&root, "alpha.children.beta.value", addressing).unwrap(),
713            Value::Integer(1)
714        );
715        set_path(
716            &mut root,
717            "alpha.children.beta.value",
718            &Value::Integer(2),
719            addressing,
720        )
721        .unwrap();
722        assert_eq!(
723            get_path(&root, "alpha.children.beta.value", addressing).unwrap(),
724            Value::Integer(2)
725        );
726    }
727
728    #[test]
729    fn oversized_decimal_array_segment_never_falls_through_to_a_slug() {
730        let oversized = format!("{}0", usize::MAX);
731        let mut item = Value::Object(Default::default());
732        item.as_object_mut()
733            .unwrap()
734            .insert("id".to_string(), Value::String(oversized.clone()));
735        let root = Value::Array(vec![item]);
736        let keyed = [KeyedList {
737            prefix: "",
738            slug_field: "id",
739        }];
740
741        let error = get_path(&root, &oversized, Addressing::keyed(&keyed)).unwrap_err();
742        assert!(matches!(error, DocumentError::PathSyntax { .. }));
743    }
744
745    #[test]
746    fn dotted_key_and_nested_path_keep_distinct_keyed_registrations() {
747        let item = |field: &str, slug: &str| {
748            let mut value = Value::Object(Default::default());
749            value
750                .as_object_mut()
751                .unwrap()
752                .insert(field.to_string(), Value::String(slug.to_string()));
753            value
754        };
755
756        let mut nested = Value::Object(Default::default());
757        nested.as_object_mut().unwrap().insert(
758            "b".to_string(),
759            Value::Array(vec![item("nested_id", "nested")]),
760        );
761
762        let mut root = Value::Object(Default::default());
763        root.as_object_mut().unwrap().insert(
764            "a.b".to_string(),
765            Value::Array(vec![item("dotted_id", "dotted")]),
766        );
767        root.as_object_mut()
768            .unwrap()
769            .insert("a".to_string(), nested);
770
771        let keyed = [
772            KeyedList {
773                prefix: r"a\.b",
774                slug_field: "dotted_id",
775            },
776            KeyedList {
777                prefix: "a.b",
778                slug_field: "nested_id",
779            },
780        ];
781        let addressing = Addressing::keyed(&keyed);
782
783        assert_eq!(
784            get_path(&root, r"a\.b.dotted.dotted_id", addressing).unwrap(),
785            Value::String("dotted".to_string())
786        );
787        assert_eq!(
788            get_path(&root, "a.b.nested.nested_id", addressing).unwrap(),
789            Value::String("nested".to_string())
790        );
791    }
792}