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