1use crate::document::Document;
19use crate::node::{Node, NodeData, NodeId};
20
21#[derive(Clone, PartialEq, Eq, Debug)]
23pub enum Component {
24 Key(String),
26 Index(i64),
29 Bare(String),
31}
32
33pub const ESCAPE_CHARS: &[char] = &['/', '{', '}', '[', ']', '.', '&', '*', '\\'];
35
36#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
38pub enum PathError {
39 #[error("unterminated quote in path component {0:?}")]
41 UnterminatedQuote(String),
42 #[error("unterminated bracket in path component {0:?}")]
44 UnterminatedBracket(String),
45 #[error("bracketed path component {0:?} is not an integer")]
47 NonIntegerIndex(String),
48 #[error("path ends with a dangling escape")]
50 DanglingEscape,
51}
52
53#[derive(Clone, PartialEq, Eq, Debug, Default)]
55pub struct Path {
56 components: Vec<Component>,
57}
58
59impl Path {
60 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 if rest.is_empty() {
85 break;
86 }
87 }
88 Ok(Path { components })
89 }
90
91 pub fn components(&self) -> &[Component] {
93 &self.components
94 }
95
96 pub fn is_root(&self) -> bool {
98 self.components.is_empty()
99 }
100}
101
102fn parse_component(s: &str) -> Result<(Component, &str), PathError> {
104 let mut chars = s.char_indices().peekable();
105
106 match chars.peek() {
107 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 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 _ => {
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
156pub 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 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 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 pub fn lookup(&self, path: &Path) -> Option<NodeId> {
199 self.lookup_from(self.root()?, path)
200 }
201
202 pub fn lookup_str(&self, path: &str) -> Option<NodeId> {
204 self.lookup(&Path::parse(path).ok()?)
205 }
206
207 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 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 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 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 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 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 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 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 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 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 let previous = self.root();
516 self.set_root(value);
517 return Ok(previous);
518 };
519
520 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 Some(node) if self.node(self.resolve(node)).is_mapping() => node,
546 Some(node) if self.node(self.resolve(node)).is_sequence() => node,
547 _ => {
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 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 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 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 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 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 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 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}