Skip to main content

asdf_yaml/
path.rs

1//! The YAML Pointer path syntax used to address values in the tree.
2//!
3//! This is libfyaml's native lookup syntax, which libasdf inherits and which
4//! is only informally specified. The rules, as libasdf's own documentation
5//! sets them out:
6//!
7//! - Components are separated by `/`.
8//! - A non-numeric component is always a mapping key.
9//! - A numeric component depends on context: a mapping key when the parent is
10//!   a mapping, a sequence index when the parent is a sequence. Sequence
11//!   indices may be negative, counting back from the end.
12//! - `[N]` is *always* a sequence index, and fails against a mapping.
13//! - `'a'` or `"a"` is *always* a mapping key, so `'0'` is the string key
14//!   `0` rather than an index.
15//! - Escaping uses backslashes rather than JSON Pointer's `~`. The characters
16//!   needing escape are ``/{}[].&*\``; inside quotes none of them do.
17
18use crate::document::Document;
19use crate::node::{Node, NodeData, NodeId};
20
21/// One step of a path.
22#[derive(Clone, PartialEq, Eq, Debug)]
23pub enum Component {
24    /// A mapping key, stated unambiguously by quoting.
25    Key(String),
26    /// A sequence index, stated unambiguously by brackets. Negative values
27    /// count back from the end.
28    Index(i64),
29    /// A bare component, resolved against whatever the parent turns out to be.
30    Bare(String),
31}
32
33/// The characters that must be escaped outside quotes.
34pub const ESCAPE_CHARS: &[char] = &['/', '{', '}', '[', ']', '.', '&', '*', '\\'];
35
36/// An error in a path's syntax.
37#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
38pub enum PathError {
39    /// A quoted component was never closed.
40    #[error("unterminated quote in path component {0:?}")]
41    UnterminatedQuote(String),
42    /// A bracketed component was never closed.
43    #[error("unterminated bracket in path component {0:?}")]
44    UnterminatedBracket(String),
45    /// The contents of `[...]` were not an integer.
46    #[error("bracketed path component {0:?} is not an integer")]
47    NonIntegerIndex(String),
48    /// A trailing backslash with nothing to escape.
49    #[error("path ends with a dangling escape")]
50    DanglingEscape,
51}
52
53/// A parsed path.
54#[derive(Clone, PartialEq, Eq, Debug, Default)]
55pub struct Path {
56    components: Vec<Component>,
57}
58
59impl Path {
60    /// Parse a path string.
61    ///
62    /// A leading `/` is optional and ignored, so `a/b` and `/a/b` are the
63    /// same path. An empty path addresses the root.
64    pub fn parse(path: &str) -> Result<Self, PathError> {
65        let mut components = Vec::new();
66        let mut rest = path.strip_prefix('/').unwrap_or(path);
67
68        if rest.is_empty() {
69            return Ok(Path { components });
70        }
71
72        while !rest.is_empty() {
73            let (component, remainder) = parse_component(rest)?;
74            components.push(component);
75            rest = match remainder.strip_prefix('/') {
76                Some(r) => r,
77                None => {
78                    debug_assert!(remainder.is_empty());
79                    ""
80                }
81            };
82            // A trailing slash addresses the same node, matching how a
83            // directory-style path reads.
84            if rest.is_empty() {
85                break;
86            }
87        }
88        Ok(Path { components })
89    }
90
91    /// The components, in order.
92    pub fn components(&self) -> &[Component] {
93        &self.components
94    }
95
96    /// Whether this path addresses the root.
97    pub fn is_root(&self) -> bool {
98        self.components.is_empty()
99    }
100}
101
102/// Split one component off the front of `s`, returning it and the remainder.
103fn parse_component(s: &str) -> Result<(Component, &str), PathError> {
104    let mut chars = s.char_indices().peekable();
105
106    match chars.peek() {
107        // Bracketed: always a sequence index.
108        Some((_, '[')) => {
109            let close = s.find(']').ok_or_else(|| PathError::UnterminatedBracket(s.to_string()))?;
110            let body = &s[1..close];
111            let index = body
112                .trim()
113                .parse::<i64>()
114                .map_err(|_| PathError::NonIntegerIndex(body.to_string()))?;
115            Ok((Component::Index(index), &s[close + 1..]))
116        }
117
118        // Quoted: always a mapping key, with no escaping inside.
119        Some((_, quote @ ('\'' | '"'))) => {
120            let quote = *quote;
121            let close =
122                s[1..].find(quote).ok_or_else(|| PathError::UnterminatedQuote(s.to_string()))? + 1;
123            let key = s[1..close].to_string();
124            Ok((Component::Key(key), &s[close + 1..]))
125        }
126
127        // Bare: runs to the next unescaped separator.
128        _ => {
129            let mut out = String::new();
130            let mut escaped = false;
131            let mut end = s.len();
132
133            for (idx, ch) in s.char_indices() {
134                if escaped {
135                    out.push(ch);
136                    escaped = false;
137                    continue;
138                }
139                match ch {
140                    '\\' => escaped = true,
141                    '/' => {
142                        end = idx;
143                        break;
144                    }
145                    _ => out.push(ch),
146                }
147            }
148            if escaped {
149                return Err(PathError::DanglingEscape);
150            }
151            Ok((Component::Bare(out), &s[end..]))
152        }
153    }
154}
155
156/// Escape a string so it survives a round trip through [`Path::parse`] as a
157/// single bare component.
158pub fn escape_component(s: &str) -> String {
159    let mut out = String::with_capacity(s.len());
160    for ch in s.chars() {
161        if ESCAPE_CHARS.contains(&ch) {
162            out.push('\\');
163        }
164        out.push(ch);
165    }
166    out
167}
168
169impl Document {
170    /// Resolve a path against a starting node.
171    ///
172    /// Aliases are followed at each step, so a path may descend through a
173    /// shared node.
174    pub fn lookup_from(&self, start: NodeId, path: &Path) -> Option<NodeId> {
175        let mut current = start;
176
177        for component in path.components() {
178            let resolved = self.resolve(current);
179            current = match component {
180                Component::Key(key) => self.mapping_get(resolved, key)?,
181                Component::Index(index) => self.sequence_get(resolved, *index)?,
182                Component::Bare(text) => match &self.node(resolved).data {
183                    // A bare component is a key when the parent is a mapping,
184                    // whether or not it looks numeric.
185                    NodeData::Mapping { .. } => self.mapping_get(resolved, text)?,
186                    NodeData::Sequence { .. } => {
187                        let index = text.parse::<i64>().ok()?;
188                        self.sequence_get(resolved, index)?
189                    }
190                    _ => return None,
191                },
192            };
193        }
194        Some(current)
195    }
196
197    /// Resolve a path against the document root.
198    pub fn lookup(&self, path: &Path) -> Option<NodeId> {
199        self.lookup_from(self.root()?, path)
200    }
201
202    /// Parse and resolve a path in one step.
203    pub fn lookup_str(&self, path: &str) -> Option<NodeId> {
204        self.lookup(&Path::parse(path).ok()?)
205    }
206
207    /// The container that holds `id`, searching down from the root.
208    ///
209    /// A node reachable by more than one route reports the first parent found
210    /// in document order. Aliases are *not* followed: an alias node is its
211    /// own child of whatever holds it, and the node it points at is reported
212    /// under its defining position.
213    pub fn parent_of(&self, id: NodeId) -> Option<NodeId> {
214        let root = self.root()?;
215        if root == id {
216            return None;
217        }
218        let mut stack = vec![root];
219        let mut seen = vec![false; self.node_count()];
220        while let Some(current) = stack.pop() {
221            let index = current.0 as usize;
222            if index >= seen.len() || seen[index] {
223                continue;
224            }
225            seen[index] = true;
226            match &self.node(current).data {
227                NodeData::Mapping { entries, .. } => {
228                    for entry in entries {
229                        if entry.key == id || entry.value == id {
230                            return Some(current);
231                        }
232                    }
233                    for entry in entries {
234                        stack.push(entry.value);
235                    }
236                }
237                NodeData::Sequence { items, .. } => {
238                    if items.contains(&id) {
239                        return Some(current);
240                    }
241                    stack.extend(items.iter().copied());
242                }
243                _ => {}
244            }
245        }
246        None
247    }
248
249    /// The path from the root to `id`, in the syntax [`Path::parse`] accepts.
250    ///
251    /// Absolute, so `/history/extensions/0` rather than
252    /// `history/extensions/0`, and a sequence index is written plainly
253    /// rather than bracketed -- that is the form libasdf reports and the one
254    /// its own tests expect. A bare numeric component is unambiguous on the
255    /// way back in, since it is read as an index only when the container it
256    /// addresses is a sequence.
257    ///
258    /// The root itself is `/`. Returns `None` when `id` is not reachable from
259    /// the root, which is the case for a node built but not yet attached.
260    pub fn path_of(&self, id: NodeId) -> Option<String> {
261        let root = self.root()?;
262        let mut components = Vec::new();
263        if !self.walk_to(root, id, &mut components, &mut vec![false; self.node_count()]) {
264            return None;
265        }
266        Some(format!("/{}", components.join("/")))
267    }
268
269    /// Depth-first search for `target`, recording the route taken.
270    fn walk_to(
271        &self,
272        current: NodeId,
273        target: NodeId,
274        route: &mut Vec<String>,
275        seen: &mut Vec<bool>,
276    ) -> bool {
277        if current == target {
278            return true;
279        }
280        let index = current.0 as usize;
281        if index >= seen.len() || seen[index] {
282            return false;
283        }
284        seen[index] = true;
285        match &self.node(current).data {
286            NodeData::Mapping { entries, .. } => {
287                for entry in entries {
288                    let Some(key) = self.resolved(entry.key).as_str() else {
289                        continue;
290                    };
291                    route.push(escape_component(key));
292                    if self.walk_to(entry.value, target, route, seen) {
293                        return true;
294                    }
295                    route.pop();
296                }
297            }
298            NodeData::Sequence { items, .. } => {
299                for (position, item) in items.iter().enumerate() {
300                    route.push(position.to_string());
301                    if self.walk_to(*item, target, route, seen) {
302                        return true;
303                    }
304                    route.pop();
305                }
306            }
307            _ => {}
308        }
309        false
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use crate::parse::parse_document;
317
318    fn doc() -> Document {
319        parse_document(
320            "a:\n  b: 1\n  '0': zero-as-key\nseq: [x, y, z]\nnested:\n  - k: v\n  - k: w\n",
321        )
322        .unwrap()
323    }
324
325    #[test]
326    fn nodes_report_their_path_from_the_root() {
327        let d = doc();
328        let root = d.root().unwrap();
329        assert_eq!(d.path_of(root).as_deref(), Some("/"));
330
331        let inner = d.lookup_str("a/b").unwrap();
332        assert_eq!(d.path_of(inner).as_deref(), Some("/a/b"));
333
334        // A sequence element's index is written plainly; it reads back as an
335        // index because the container it addresses is a sequence.
336        let item = d.lookup_str("seq/1").unwrap();
337        assert_eq!(d.path_of(item).as_deref(), Some("/seq/1"));
338        assert_eq!(d.lookup_str(&d.path_of(item).unwrap()), Some(item));
339
340        let nested = d.lookup_str("nested/1/k").unwrap();
341        assert_eq!(d.path_of(nested).as_deref(), Some("/nested/1/k"));
342        assert_eq!(d.lookup_str("/nested/1/k"), Some(nested));
343    }
344
345    #[test]
346    fn a_detached_node_has_no_path() {
347        let mut d = doc();
348        let orphan = d.add_scalar("nowhere");
349        assert_eq!(d.path_of(orphan), None);
350        assert_eq!(d.parent_of(orphan), None);
351    }
352
353    #[test]
354    fn nodes_report_their_parent() {
355        let d = doc();
356        let root = d.root().unwrap();
357        assert_eq!(d.parent_of(root), None, "the root has no parent");
358
359        let inner = d.lookup_str("a/b").unwrap();
360        let a = d.lookup_str("a").unwrap();
361        assert_eq!(d.parent_of(inner), Some(a));
362        assert_eq!(d.parent_of(a), Some(root));
363
364        let item = d.lookup_str("seq/2").unwrap();
365        assert_eq!(d.parent_of(item), Some(d.lookup_str("seq").unwrap()));
366    }
367
368    fn get<'a>(d: &'a Document, path: &str) -> Option<&'a str> {
369        d.lookup_str(path).map(|id| d.resolved(id).as_str().unwrap_or("<container>"))
370    }
371
372    #[test]
373    fn parses_simple_paths() {
374        let p = Path::parse("a/b").unwrap();
375        assert_eq!(p.components(), [Component::Bare("a".into()), Component::Bare("b".into())]);
376        // A leading slash is optional.
377        assert_eq!(Path::parse("/a/b").unwrap(), p);
378    }
379
380    #[test]
381    fn empty_path_is_the_root() {
382        assert!(Path::parse("").unwrap().is_root());
383        assert!(Path::parse("/").unwrap().is_root());
384    }
385
386    #[test]
387    fn looks_up_nested_mappings() {
388        let d = doc();
389        assert_eq!(get(&d, "a/b"), Some("1"));
390        assert_eq!(get(&d, "/a/b"), Some("1"));
391        assert!(get(&d, "a/missing").is_none());
392    }
393
394    #[test]
395    fn numeric_component_indexes_a_sequence() {
396        let d = doc();
397        assert_eq!(get(&d, "seq/0"), Some("x"));
398        assert_eq!(get(&d, "seq/2"), Some("z"));
399        assert!(get(&d, "seq/3").is_none());
400    }
401
402    #[test]
403    fn negative_indices_count_from_the_end() {
404        let d = doc();
405        assert_eq!(get(&d, "seq/-1"), Some("z"));
406        assert_eq!(get(&d, "seq/-3"), Some("x"));
407        assert!(get(&d, "seq/-4").is_none());
408    }
409
410    #[test]
411    fn numeric_component_is_a_key_under_a_mapping() {
412        // The context rule: '0' addresses the *key* "0", not an index.
413        let d = doc();
414        assert_eq!(get(&d, "a/0"), Some("zero-as-key"));
415    }
416
417    #[test]
418    fn brackets_force_a_sequence_index() {
419        let d = doc();
420        assert_eq!(get(&d, "seq/[1]"), Some("y"));
421        assert_eq!(get(&d, "seq/[-1]"), Some("z"));
422        // Against a mapping it must fail, even though the key "0" exists.
423        assert!(get(&d, "a/[0]").is_none());
424    }
425
426    #[test]
427    fn quotes_force_a_mapping_key() {
428        let d = doc();
429        assert_eq!(get(&d, "a/'0'"), Some("zero-as-key"));
430        assert_eq!(get(&d, "a/\"0\""), Some("zero-as-key"));
431        // Against a sequence a quoted component must fail: it is a string key.
432        assert!(get(&d, "seq/'0'").is_none());
433    }
434
435    #[test]
436    fn descends_through_sequences_of_mappings() {
437        let d = doc();
438        assert_eq!(get(&d, "nested/0/k"), Some("v"));
439        assert_eq!(get(&d, "nested/1/k"), Some("w"));
440        assert_eq!(get(&d, "nested/-1/k"), Some("w"));
441    }
442
443    #[test]
444    fn backslash_escapes_a_separator() {
445        let d = parse_document("a/b: slashed\n").unwrap();
446        assert_eq!(get(&d, "a\\/b"), Some("slashed"));
447        // Without the escape it is two components and finds nothing.
448        assert!(get(&d, "a/b").is_none());
449    }
450
451    #[test]
452    fn quotes_avoid_the_need_to_escape() {
453        let d = parse_document("a.b: dotted\n").unwrap();
454        assert_eq!(get(&d, "'a.b'"), Some("dotted"));
455        assert_eq!(get(&d, "a\\.b"), Some("dotted"));
456    }
457
458    #[test]
459    fn escape_round_trips() {
460        for raw in ["a/b", "a.b", "a[0]", "a&b", "plain"] {
461            let escaped = escape_component(raw);
462            let parsed = Path::parse(&escaped).unwrap();
463            assert_eq!(
464                parsed.components(),
465                [Component::Bare(raw.to_string())],
466                "{raw:?} escaped to {escaped:?}"
467            );
468        }
469    }
470
471    #[test]
472    fn paths_follow_aliases() {
473        let d = parse_document("target: &a {x: 1}\nalias: *a\n").unwrap();
474        assert_eq!(get(&d, "alias/x"), Some("1"));
475        assert_eq!(get(&d, "target/x"), Some("1"));
476    }
477
478    #[test]
479    fn descending_into_a_scalar_fails() {
480        let d = doc();
481        assert!(get(&d, "a/b/c").is_none());
482    }
483
484    #[test]
485    fn syntax_errors_are_reported() {
486        assert_eq!(
487            Path::parse("'unterminated"),
488            Err(PathError::UnterminatedQuote("'unterminated".into()))
489        );
490        assert_eq!(Path::parse("[1"), Err(PathError::UnterminatedBracket("[1".into())));
491        assert_eq!(Path::parse("[abc]"), Err(PathError::NonIntegerIndex("abc".into())));
492        assert_eq!(Path::parse("a\\"), Err(PathError::DanglingEscape));
493    }
494
495    #[test]
496    fn trailing_slash_is_ignored() {
497        let d = doc();
498        assert_eq!(get(&d, "a/b/"), Some("1"));
499    }
500}
501
502impl Document {
503    /// Insert `value` at `path`, creating intermediate mappings as needed.
504    ///
505    /// This mirrors libasdf's `asdf_node_insert_at` with materialisation on:
506    /// setting `powers/squares` in an empty tree creates the `powers` mapping
507    /// on the way. Returns the node that was replaced, if any.
508    ///
509    /// Intermediate steps are only ever created as mappings, since a bare
510    /// path component gives no way to say "make a sequence here". A component
511    /// that names an existing sequence still indexes into it.
512    pub fn insert_at(&mut self, path: &Path, value: NodeId) -> Result<Option<NodeId>, PathError> {
513        let Some((last, leading)) = path.components().split_last() else {
514            // An empty path replaces the root outright.
515            let previous = self.root();
516            self.set_root(value);
517            return Ok(previous);
518        };
519
520        // Walk to the parent, materialising mappings that are not there yet.
521        let mut current = match self.root() {
522            Some(root) => root,
523            None => {
524                let root = self.add(Node::mapping());
525                self.set_root(root);
526                root
527            }
528        };
529
530        for component in leading {
531            let resolved = self.resolve(current);
532            let existing = match component {
533                Component::Key(key) => self.mapping_get(resolved, key),
534                Component::Index(index) => self.sequence_get(resolved, *index),
535                Component::Bare(text) => match &self.node(resolved).data {
536                    NodeData::Sequence { .. } => {
537                        text.parse::<i64>().ok().and_then(|i| self.sequence_get(resolved, i))
538                    }
539                    _ => self.mapping_get(resolved, text),
540                },
541            };
542
543            current = match existing {
544                // Descend through anything that can hold children.
545                Some(node) if self.node(self.resolve(node)).is_mapping() => node,
546                Some(node) if self.node(self.resolve(node)).is_sequence() => node,
547                // A scalar in the way is replaced by a mapping, since the
548                // caller has asked for a path through it.
549                _ => {
550                    let fresh = self.add(Node::mapping());
551                    self.set_component(resolved, component, fresh)?;
552                    fresh
553                }
554            };
555        }
556
557        let parent = self.resolve(current);
558        self.set_component(parent, last, value)
559    }
560
561    /// Parse and insert in one step.
562    pub fn insert_at_str(
563        &mut self,
564        path: &str,
565        value: NodeId,
566    ) -> Result<Option<NodeId>, PathError> {
567        let parsed = Path::parse(path)?;
568        self.insert_at(&parsed, value)
569    }
570
571    /// Set one component of a container, replacing whatever was there.
572    fn set_component(
573        &mut self,
574        parent: NodeId,
575        component: &Component,
576        value: NodeId,
577    ) -> Result<Option<NodeId>, PathError> {
578        let is_sequence = self.node(parent).is_sequence();
579
580        match component {
581            Component::Key(key) => Ok(self.mapping_set(parent, key, value)),
582            Component::Index(index) => Ok(self.sequence_set(parent, *index, value)),
583            Component::Bare(text) => {
584                if is_sequence && let Ok(index) = text.parse::<i64>() {
585                    return Ok(self.sequence_set(parent, index, value));
586                }
587                Ok(self.mapping_set(parent, text, value))
588            }
589        }
590    }
591
592    /// Replace a sequence element, or append when the index is one past the
593    /// end. Returns the element that was replaced.
594    pub fn sequence_set(&mut self, id: NodeId, index: i64, value: NodeId) -> Option<NodeId> {
595        let target = self.resolve(id);
596        let len = self.container_len(target)? as i64;
597        let idx = if index < 0 { len + index } else { index };
598
599        let NodeData::Sequence { items, .. } = &mut self.node_mut(target).data else {
600            return None;
601        };
602        if idx == len {
603            items.push(value);
604            return None;
605        }
606        if idx < 0 || idx > len {
607            return None;
608        }
609        Some(core::mem::replace(&mut items[idx as usize], value))
610    }
611
612    /// Remove whatever is at `path`, returning it.
613    pub fn remove_at_str(&mut self, path: &str) -> Option<NodeId> {
614        let parsed = Path::parse(path).ok()?;
615        let (last, leading) = parsed.components().split_last()?;
616
617        let mut current = self.root()?;
618        for component in leading {
619            let resolved = self.resolve(current);
620            current = match component {
621                Component::Key(key) => self.mapping_get(resolved, key)?,
622                Component::Index(index) => self.sequence_get(resolved, *index)?,
623                Component::Bare(text) => match &self.node(resolved).data {
624                    NodeData::Sequence { .. } => {
625                        self.sequence_get(resolved, text.parse::<i64>().ok()?)?
626                    }
627                    _ => self.mapping_get(resolved, text)?,
628                },
629            };
630        }
631
632        let parent = self.resolve(current);
633        match last {
634            Component::Key(key) => self.mapping_remove(parent, key),
635            Component::Index(index) => self.sequence_remove(parent, *index),
636            Component::Bare(text) => {
637                if self.node(parent).is_sequence()
638                    && let Ok(index) = text.parse::<i64>()
639                {
640                    return self.sequence_remove(parent, index);
641                }
642                self.mapping_remove(parent, text)
643            }
644        }
645    }
646
647    /// Remove a sequence element, returning it.
648    pub fn sequence_remove(&mut self, id: NodeId, index: i64) -> Option<NodeId> {
649        let target = self.resolve(id);
650        let len = self.container_len(target)? as i64;
651        let idx = if index < 0 { len + index } else { index };
652        if idx < 0 || idx >= len {
653            return None;
654        }
655        let NodeData::Sequence { items, .. } = &mut self.node_mut(target).data else {
656            return None;
657        };
658        Some(items.remove(idx as usize))
659    }
660}
661
662#[cfg(test)]
663mod insert_tests {
664    use super::*;
665    use crate::node::ScalarStyle;
666    use crate::parse::parse_document;
667
668    fn get<'a>(d: &'a Document, path: &str) -> Option<&'a str> {
669        d.lookup_str(path).map(|id| d.resolved(id).as_str().unwrap_or("<container>"))
670    }
671
672    #[test]
673    fn sets_a_top_level_key() {
674        let mut doc = parse_document("a: 1\n").unwrap();
675        let v = doc.add_scalar("2");
676        let previous = doc.insert_at_str("b", v).unwrap();
677        assert!(previous.is_none());
678        assert_eq!(get(&doc, "b"), Some("2"));
679        assert_eq!(get(&doc, "a"), Some("1"));
680    }
681
682    #[test]
683    fn replaces_an_existing_value() {
684        let mut doc = parse_document("a: 1\n").unwrap();
685        let v = doc.add_scalar("9");
686        let previous = doc.insert_at_str("a", v).unwrap();
687        assert!(previous.is_some());
688        assert_eq!(get(&doc, "a"), Some("9"));
689        assert_eq!(doc.container_len(doc.root().unwrap()), Some(1));
690    }
691
692    #[test]
693    fn materialises_intermediate_mappings() {
694        // The README's case: setting `powers/squares` in a tree that has
695        // neither must create `powers` on the way.
696        let mut doc = parse_document("a: 1\n").unwrap();
697        let v = doc.add_scalar("squared");
698        doc.insert_at_str("powers/squares", v).unwrap();
699
700        assert_eq!(get(&doc, "powers/squares"), Some("squared"));
701        assert!(doc.node(doc.lookup_str("powers").unwrap()).is_mapping());
702    }
703
704    #[test]
705    fn materialises_several_levels() {
706        let mut doc = Document::new();
707        let v = doc.add_scalar("deep");
708        doc.insert_at_str("a/b/c/d", v).unwrap();
709        assert_eq!(get(&doc, "a/b/c/d"), Some("deep"));
710    }
711
712    #[test]
713    fn creates_a_root_when_there_is_none() {
714        let mut doc = Document::new();
715        let v = doc.add_scalar("hello");
716        doc.insert_at_str("greeting", v).unwrap();
717        assert!(doc.root().is_some());
718        assert_eq!(get(&doc, "greeting"), Some("hello"));
719    }
720
721    #[test]
722    fn an_empty_path_replaces_the_root() {
723        let mut doc = parse_document("a: 1\n").unwrap();
724        let v = doc.add_scalar("replaced");
725        doc.insert_at_str("", v).unwrap();
726        assert_eq!(doc.root(), Some(v));
727    }
728
729    #[test]
730    fn a_scalar_in_the_way_becomes_a_mapping() {
731        let mut doc = parse_document("a: scalar\n").unwrap();
732        let v = doc.add_scalar("1");
733        doc.insert_at_str("a/b", v).unwrap();
734        assert_eq!(get(&doc, "a/b"), Some("1"));
735        assert!(doc.node(doc.lookup_str("a").unwrap()).is_mapping());
736    }
737
738    #[test]
739    fn writes_through_a_sequence_index() {
740        let mut doc = parse_document("s: [x, y, z]\n").unwrap();
741        let v = doc.add_scalar("Y");
742        doc.insert_at_str("s/1", v).unwrap();
743        assert_eq!(get(&doc, "s/1"), Some("Y"));
744        assert_eq!(doc.container_len(doc.lookup_str("s").unwrap()), Some(3));
745    }
746
747    #[test]
748    fn appending_one_past_the_end_extends_a_sequence() {
749        let mut doc = parse_document("s: [x]\n").unwrap();
750        let v = doc.add_scalar("y");
751        doc.insert_at_str("s/1", v).unwrap();
752        assert_eq!(doc.container_len(doc.lookup_str("s").unwrap()), Some(2));
753        assert_eq!(get(&doc, "s/1"), Some("y"));
754    }
755
756    #[test]
757    fn a_quoted_component_makes_a_string_key_even_over_a_sequence() {
758        let mut doc = parse_document("a: {}\n").unwrap();
759        let v = doc.add_scalar("1");
760        doc.insert_at_str("a/'0'", v).unwrap();
761        assert_eq!(get(&doc, "a/'0'"), Some("1"));
762    }
763
764    #[test]
765    fn removes_values() {
766        let mut doc = parse_document("a: 1\nb:\n  c: 2\ns: [x, y]\n").unwrap();
767
768        assert!(doc.remove_at_str("a").is_some());
769        assert!(doc.lookup_str("a").is_none());
770
771        assert!(doc.remove_at_str("b/c").is_some());
772        assert!(doc.lookup_str("b/c").is_none());
773        // The parent survives.
774        assert!(doc.lookup_str("b").is_some());
775
776        assert!(doc.remove_at_str("s/0").is_some());
777        assert_eq!(doc.container_len(doc.lookup_str("s").unwrap()), Some(1));
778        assert_eq!(get(&doc, "s/0"), Some("y"));
779
780        assert!(doc.remove_at_str("missing").is_none());
781    }
782
783    #[test]
784    fn inserted_trees_emit_and_re_read() {
785        use crate::emit::emit;
786        use crate::parse::parse_document as reparse;
787
788        let mut doc = Document::new_asdf();
789        let name = doc.add_scalar_styled("Dennis Richie", ScalarStyle::Plain);
790        doc.insert_at_str("name", name).unwrap();
791        let foo = doc.add_scalar("42");
792        doc.insert_at_str("foo", foo).unwrap();
793        let sq = doc.add_scalar("1764");
794        doc.insert_at_str("powers/squares", sq).unwrap();
795
796        let text = emit(&doc).unwrap();
797        let back = reparse(&text).unwrap();
798        assert_eq!(
799            back.lookup_str("powers/squares").map(|id| back
800                .resolved(id)
801                .as_str()
802                .unwrap()
803                .to_string()),
804            Some("1764".to_string())
805        );
806    }
807}