use crate::document::Document;
use crate::node::{Node, NodeData, NodeId};
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum Component {
Key(String),
Index(i64),
Bare(String),
}
pub const ESCAPE_CHARS: &[char] = &['/', '{', '}', '[', ']', '.', '&', '*', '\\'];
#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
pub enum PathError {
#[error("unterminated quote in path component {0:?}")]
UnterminatedQuote(String),
#[error("unterminated bracket in path component {0:?}")]
UnterminatedBracket(String),
#[error("bracketed path component {0:?} is not an integer")]
NonIntegerIndex(String),
#[error("path ends with a dangling escape")]
DanglingEscape,
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct Path {
components: Vec<Component>,
}
impl Path {
pub fn parse(path: &str) -> Result<Self, PathError> {
let mut components = Vec::new();
let mut rest = path.strip_prefix('/').unwrap_or(path);
if rest.is_empty() {
return Ok(Path { components });
}
while !rest.is_empty() {
let (component, remainder) = parse_component(rest)?;
components.push(component);
rest = match remainder.strip_prefix('/') {
Some(r) => r,
None => {
debug_assert!(remainder.is_empty());
""
}
};
if rest.is_empty() {
break;
}
}
Ok(Path { components })
}
pub fn components(&self) -> &[Component] {
&self.components
}
pub fn is_root(&self) -> bool {
self.components.is_empty()
}
}
fn parse_component(s: &str) -> Result<(Component, &str), PathError> {
let mut chars = s.char_indices().peekable();
match chars.peek() {
Some((_, '[')) => {
let close = s.find(']').ok_or_else(|| PathError::UnterminatedBracket(s.to_string()))?;
let body = &s[1..close];
let index = body
.trim()
.parse::<i64>()
.map_err(|_| PathError::NonIntegerIndex(body.to_string()))?;
Ok((Component::Index(index), &s[close + 1..]))
}
Some((_, quote @ ('\'' | '"'))) => {
let quote = *quote;
let close =
s[1..].find(quote).ok_or_else(|| PathError::UnterminatedQuote(s.to_string()))? + 1;
let key = s[1..close].to_string();
Ok((Component::Key(key), &s[close + 1..]))
}
_ => {
let mut out = String::new();
let mut escaped = false;
let mut end = s.len();
for (idx, ch) in s.char_indices() {
if escaped {
out.push(ch);
escaped = false;
continue;
}
match ch {
'\\' => escaped = true,
'/' => {
end = idx;
break;
}
_ => out.push(ch),
}
}
if escaped {
return Err(PathError::DanglingEscape);
}
Ok((Component::Bare(out), &s[end..]))
}
}
}
pub fn escape_component(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
if ESCAPE_CHARS.contains(&ch) {
out.push('\\');
}
out.push(ch);
}
out
}
impl Document {
pub fn lookup_from(&self, start: NodeId, path: &Path) -> Option<NodeId> {
let mut current = start;
for component in path.components() {
let resolved = self.resolve(current);
current = match component {
Component::Key(key) => self.mapping_get(resolved, key)?,
Component::Index(index) => self.sequence_get(resolved, *index)?,
Component::Bare(text) => match &self.node(resolved).data {
NodeData::Mapping { .. } => self.mapping_get(resolved, text)?,
NodeData::Sequence { .. } => {
let index = text.parse::<i64>().ok()?;
self.sequence_get(resolved, index)?
}
_ => return None,
},
};
}
Some(current)
}
pub fn lookup(&self, path: &Path) -> Option<NodeId> {
self.lookup_from(self.root()?, path)
}
pub fn lookup_str(&self, path: &str) -> Option<NodeId> {
self.lookup(&Path::parse(path).ok()?)
}
pub fn parent_of(&self, id: NodeId) -> Option<NodeId> {
let root = self.root()?;
if root == id {
return None;
}
let mut stack = vec![root];
let mut seen = vec![false; self.node_count()];
while let Some(current) = stack.pop() {
let index = current.0 as usize;
if index >= seen.len() || seen[index] {
continue;
}
seen[index] = true;
match &self.node(current).data {
NodeData::Mapping { entries, .. } => {
for entry in entries {
if entry.key == id || entry.value == id {
return Some(current);
}
}
for entry in entries {
stack.push(entry.value);
}
}
NodeData::Sequence { items, .. } => {
if items.contains(&id) {
return Some(current);
}
stack.extend(items.iter().copied());
}
_ => {}
}
}
None
}
pub fn path_of(&self, id: NodeId) -> Option<String> {
let root = self.root()?;
let mut components = Vec::new();
if !self.walk_to(root, id, &mut components, &mut vec![false; self.node_count()]) {
return None;
}
Some(format!("/{}", components.join("/")))
}
fn walk_to(
&self,
current: NodeId,
target: NodeId,
route: &mut Vec<String>,
seen: &mut Vec<bool>,
) -> bool {
if current == target {
return true;
}
let index = current.0 as usize;
if index >= seen.len() || seen[index] {
return false;
}
seen[index] = true;
match &self.node(current).data {
NodeData::Mapping { entries, .. } => {
for entry in entries {
let Some(key) = self.resolved(entry.key).as_str() else {
continue;
};
route.push(escape_component(key));
if self.walk_to(entry.value, target, route, seen) {
return true;
}
route.pop();
}
}
NodeData::Sequence { items, .. } => {
for (position, item) in items.iter().enumerate() {
route.push(position.to_string());
if self.walk_to(*item, target, route, seen) {
return true;
}
route.pop();
}
}
_ => {}
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse::parse_document;
fn doc() -> Document {
parse_document(
"a:\n b: 1\n '0': zero-as-key\nseq: [x, y, z]\nnested:\n - k: v\n - k: w\n",
)
.unwrap()
}
#[test]
fn nodes_report_their_path_from_the_root() {
let d = doc();
let root = d.root().unwrap();
assert_eq!(d.path_of(root).as_deref(), Some("/"));
let inner = d.lookup_str("a/b").unwrap();
assert_eq!(d.path_of(inner).as_deref(), Some("/a/b"));
let item = d.lookup_str("seq/1").unwrap();
assert_eq!(d.path_of(item).as_deref(), Some("/seq/1"));
assert_eq!(d.lookup_str(&d.path_of(item).unwrap()), Some(item));
let nested = d.lookup_str("nested/1/k").unwrap();
assert_eq!(d.path_of(nested).as_deref(), Some("/nested/1/k"));
assert_eq!(d.lookup_str("/nested/1/k"), Some(nested));
}
#[test]
fn a_detached_node_has_no_path() {
let mut d = doc();
let orphan = d.add_scalar("nowhere");
assert_eq!(d.path_of(orphan), None);
assert_eq!(d.parent_of(orphan), None);
}
#[test]
fn nodes_report_their_parent() {
let d = doc();
let root = d.root().unwrap();
assert_eq!(d.parent_of(root), None, "the root has no parent");
let inner = d.lookup_str("a/b").unwrap();
let a = d.lookup_str("a").unwrap();
assert_eq!(d.parent_of(inner), Some(a));
assert_eq!(d.parent_of(a), Some(root));
let item = d.lookup_str("seq/2").unwrap();
assert_eq!(d.parent_of(item), Some(d.lookup_str("seq").unwrap()));
}
fn get<'a>(d: &'a Document, path: &str) -> Option<&'a str> {
d.lookup_str(path).map(|id| d.resolved(id).as_str().unwrap_or("<container>"))
}
#[test]
fn parses_simple_paths() {
let p = Path::parse("a/b").unwrap();
assert_eq!(p.components(), [Component::Bare("a".into()), Component::Bare("b".into())]);
assert_eq!(Path::parse("/a/b").unwrap(), p);
}
#[test]
fn empty_path_is_the_root() {
assert!(Path::parse("").unwrap().is_root());
assert!(Path::parse("/").unwrap().is_root());
}
#[test]
fn looks_up_nested_mappings() {
let d = doc();
assert_eq!(get(&d, "a/b"), Some("1"));
assert_eq!(get(&d, "/a/b"), Some("1"));
assert!(get(&d, "a/missing").is_none());
}
#[test]
fn numeric_component_indexes_a_sequence() {
let d = doc();
assert_eq!(get(&d, "seq/0"), Some("x"));
assert_eq!(get(&d, "seq/2"), Some("z"));
assert!(get(&d, "seq/3").is_none());
}
#[test]
fn negative_indices_count_from_the_end() {
let d = doc();
assert_eq!(get(&d, "seq/-1"), Some("z"));
assert_eq!(get(&d, "seq/-3"), Some("x"));
assert!(get(&d, "seq/-4").is_none());
}
#[test]
fn numeric_component_is_a_key_under_a_mapping() {
let d = doc();
assert_eq!(get(&d, "a/0"), Some("zero-as-key"));
}
#[test]
fn brackets_force_a_sequence_index() {
let d = doc();
assert_eq!(get(&d, "seq/[1]"), Some("y"));
assert_eq!(get(&d, "seq/[-1]"), Some("z"));
assert!(get(&d, "a/[0]").is_none());
}
#[test]
fn quotes_force_a_mapping_key() {
let d = doc();
assert_eq!(get(&d, "a/'0'"), Some("zero-as-key"));
assert_eq!(get(&d, "a/\"0\""), Some("zero-as-key"));
assert!(get(&d, "seq/'0'").is_none());
}
#[test]
fn descends_through_sequences_of_mappings() {
let d = doc();
assert_eq!(get(&d, "nested/0/k"), Some("v"));
assert_eq!(get(&d, "nested/1/k"), Some("w"));
assert_eq!(get(&d, "nested/-1/k"), Some("w"));
}
#[test]
fn backslash_escapes_a_separator() {
let d = parse_document("a/b: slashed\n").unwrap();
assert_eq!(get(&d, "a\\/b"), Some("slashed"));
assert!(get(&d, "a/b").is_none());
}
#[test]
fn quotes_avoid_the_need_to_escape() {
let d = parse_document("a.b: dotted\n").unwrap();
assert_eq!(get(&d, "'a.b'"), Some("dotted"));
assert_eq!(get(&d, "a\\.b"), Some("dotted"));
}
#[test]
fn escape_round_trips() {
for raw in ["a/b", "a.b", "a[0]", "a&b", "plain"] {
let escaped = escape_component(raw);
let parsed = Path::parse(&escaped).unwrap();
assert_eq!(
parsed.components(),
[Component::Bare(raw.to_string())],
"{raw:?} escaped to {escaped:?}"
);
}
}
#[test]
fn paths_follow_aliases() {
let d = parse_document("target: &a {x: 1}\nalias: *a\n").unwrap();
assert_eq!(get(&d, "alias/x"), Some("1"));
assert_eq!(get(&d, "target/x"), Some("1"));
}
#[test]
fn descending_into_a_scalar_fails() {
let d = doc();
assert!(get(&d, "a/b/c").is_none());
}
#[test]
fn syntax_errors_are_reported() {
assert_eq!(
Path::parse("'unterminated"),
Err(PathError::UnterminatedQuote("'unterminated".into()))
);
assert_eq!(Path::parse("[1"), Err(PathError::UnterminatedBracket("[1".into())));
assert_eq!(Path::parse("[abc]"), Err(PathError::NonIntegerIndex("abc".into())));
assert_eq!(Path::parse("a\\"), Err(PathError::DanglingEscape));
}
#[test]
fn trailing_slash_is_ignored() {
let d = doc();
assert_eq!(get(&d, "a/b/"), Some("1"));
}
}
impl Document {
pub fn insert_at(&mut self, path: &Path, value: NodeId) -> Result<Option<NodeId>, PathError> {
let Some((last, leading)) = path.components().split_last() else {
let previous = self.root();
self.set_root(value);
return Ok(previous);
};
let mut current = match self.root() {
Some(root) => root,
None => {
let root = self.add(Node::mapping());
self.set_root(root);
root
}
};
for component in leading {
let resolved = self.resolve(current);
let existing = match component {
Component::Key(key) => self.mapping_get(resolved, key),
Component::Index(index) => self.sequence_get(resolved, *index),
Component::Bare(text) => match &self.node(resolved).data {
NodeData::Sequence { .. } => {
text.parse::<i64>().ok().and_then(|i| self.sequence_get(resolved, i))
}
_ => self.mapping_get(resolved, text),
},
};
current = match existing {
Some(node) if self.node(self.resolve(node)).is_mapping() => node,
Some(node) if self.node(self.resolve(node)).is_sequence() => node,
_ => {
let fresh = self.add(Node::mapping());
self.set_component(resolved, component, fresh)?;
fresh
}
};
}
let parent = self.resolve(current);
self.set_component(parent, last, value)
}
pub fn insert_at_str(
&mut self,
path: &str,
value: NodeId,
) -> Result<Option<NodeId>, PathError> {
let parsed = Path::parse(path)?;
self.insert_at(&parsed, value)
}
fn set_component(
&mut self,
parent: NodeId,
component: &Component,
value: NodeId,
) -> Result<Option<NodeId>, PathError> {
let is_sequence = self.node(parent).is_sequence();
match component {
Component::Key(key) => Ok(self.mapping_set(parent, key, value)),
Component::Index(index) => Ok(self.sequence_set(parent, *index, value)),
Component::Bare(text) => {
if is_sequence && let Ok(index) = text.parse::<i64>() {
return Ok(self.sequence_set(parent, index, value));
}
Ok(self.mapping_set(parent, text, value))
}
}
}
pub fn sequence_set(&mut self, id: NodeId, index: i64, value: NodeId) -> Option<NodeId> {
let target = self.resolve(id);
let len = self.container_len(target)? as i64;
let idx = if index < 0 { len + index } else { index };
let NodeData::Sequence { items, .. } = &mut self.node_mut(target).data else {
return None;
};
if idx == len {
items.push(value);
return None;
}
if idx < 0 || idx > len {
return None;
}
Some(core::mem::replace(&mut items[idx as usize], value))
}
pub fn remove_at_str(&mut self, path: &str) -> Option<NodeId> {
let parsed = Path::parse(path).ok()?;
let (last, leading) = parsed.components().split_last()?;
let mut current = self.root()?;
for component in leading {
let resolved = self.resolve(current);
current = match component {
Component::Key(key) => self.mapping_get(resolved, key)?,
Component::Index(index) => self.sequence_get(resolved, *index)?,
Component::Bare(text) => match &self.node(resolved).data {
NodeData::Sequence { .. } => {
self.sequence_get(resolved, text.parse::<i64>().ok()?)?
}
_ => self.mapping_get(resolved, text)?,
},
};
}
let parent = self.resolve(current);
match last {
Component::Key(key) => self.mapping_remove(parent, key),
Component::Index(index) => self.sequence_remove(parent, *index),
Component::Bare(text) => {
if self.node(parent).is_sequence()
&& let Ok(index) = text.parse::<i64>()
{
return self.sequence_remove(parent, index);
}
self.mapping_remove(parent, text)
}
}
}
pub fn sequence_remove(&mut self, id: NodeId, index: i64) -> Option<NodeId> {
let target = self.resolve(id);
let len = self.container_len(target)? as i64;
let idx = if index < 0 { len + index } else { index };
if idx < 0 || idx >= len {
return None;
}
let NodeData::Sequence { items, .. } = &mut self.node_mut(target).data else {
return None;
};
Some(items.remove(idx as usize))
}
}
#[cfg(test)]
mod insert_tests {
use super::*;
use crate::node::ScalarStyle;
use crate::parse::parse_document;
fn get<'a>(d: &'a Document, path: &str) -> Option<&'a str> {
d.lookup_str(path).map(|id| d.resolved(id).as_str().unwrap_or("<container>"))
}
#[test]
fn sets_a_top_level_key() {
let mut doc = parse_document("a: 1\n").unwrap();
let v = doc.add_scalar("2");
let previous = doc.insert_at_str("b", v).unwrap();
assert!(previous.is_none());
assert_eq!(get(&doc, "b"), Some("2"));
assert_eq!(get(&doc, "a"), Some("1"));
}
#[test]
fn replaces_an_existing_value() {
let mut doc = parse_document("a: 1\n").unwrap();
let v = doc.add_scalar("9");
let previous = doc.insert_at_str("a", v).unwrap();
assert!(previous.is_some());
assert_eq!(get(&doc, "a"), Some("9"));
assert_eq!(doc.container_len(doc.root().unwrap()), Some(1));
}
#[test]
fn materialises_intermediate_mappings() {
let mut doc = parse_document("a: 1\n").unwrap();
let v = doc.add_scalar("squared");
doc.insert_at_str("powers/squares", v).unwrap();
assert_eq!(get(&doc, "powers/squares"), Some("squared"));
assert!(doc.node(doc.lookup_str("powers").unwrap()).is_mapping());
}
#[test]
fn materialises_several_levels() {
let mut doc = Document::new();
let v = doc.add_scalar("deep");
doc.insert_at_str("a/b/c/d", v).unwrap();
assert_eq!(get(&doc, "a/b/c/d"), Some("deep"));
}
#[test]
fn creates_a_root_when_there_is_none() {
let mut doc = Document::new();
let v = doc.add_scalar("hello");
doc.insert_at_str("greeting", v).unwrap();
assert!(doc.root().is_some());
assert_eq!(get(&doc, "greeting"), Some("hello"));
}
#[test]
fn an_empty_path_replaces_the_root() {
let mut doc = parse_document("a: 1\n").unwrap();
let v = doc.add_scalar("replaced");
doc.insert_at_str("", v).unwrap();
assert_eq!(doc.root(), Some(v));
}
#[test]
fn a_scalar_in_the_way_becomes_a_mapping() {
let mut doc = parse_document("a: scalar\n").unwrap();
let v = doc.add_scalar("1");
doc.insert_at_str("a/b", v).unwrap();
assert_eq!(get(&doc, "a/b"), Some("1"));
assert!(doc.node(doc.lookup_str("a").unwrap()).is_mapping());
}
#[test]
fn writes_through_a_sequence_index() {
let mut doc = parse_document("s: [x, y, z]\n").unwrap();
let v = doc.add_scalar("Y");
doc.insert_at_str("s/1", v).unwrap();
assert_eq!(get(&doc, "s/1"), Some("Y"));
assert_eq!(doc.container_len(doc.lookup_str("s").unwrap()), Some(3));
}
#[test]
fn appending_one_past_the_end_extends_a_sequence() {
let mut doc = parse_document("s: [x]\n").unwrap();
let v = doc.add_scalar("y");
doc.insert_at_str("s/1", v).unwrap();
assert_eq!(doc.container_len(doc.lookup_str("s").unwrap()), Some(2));
assert_eq!(get(&doc, "s/1"), Some("y"));
}
#[test]
fn a_quoted_component_makes_a_string_key_even_over_a_sequence() {
let mut doc = parse_document("a: {}\n").unwrap();
let v = doc.add_scalar("1");
doc.insert_at_str("a/'0'", v).unwrap();
assert_eq!(get(&doc, "a/'0'"), Some("1"));
}
#[test]
fn removes_values() {
let mut doc = parse_document("a: 1\nb:\n c: 2\ns: [x, y]\n").unwrap();
assert!(doc.remove_at_str("a").is_some());
assert!(doc.lookup_str("a").is_none());
assert!(doc.remove_at_str("b/c").is_some());
assert!(doc.lookup_str("b/c").is_none());
assert!(doc.lookup_str("b").is_some());
assert!(doc.remove_at_str("s/0").is_some());
assert_eq!(doc.container_len(doc.lookup_str("s").unwrap()), Some(1));
assert_eq!(get(&doc, "s/0"), Some("y"));
assert!(doc.remove_at_str("missing").is_none());
}
#[test]
fn inserted_trees_emit_and_re_read() {
use crate::emit::emit;
use crate::parse::parse_document as reparse;
let mut doc = Document::new_asdf();
let name = doc.add_scalar_styled("Dennis Richie", ScalarStyle::Plain);
doc.insert_at_str("name", name).unwrap();
let foo = doc.add_scalar("42");
doc.insert_at_str("foo", foo).unwrap();
let sq = doc.add_scalar("1764");
doc.insert_at_str("powers/squares", sq).unwrap();
let text = emit(&doc).unwrap();
let back = reparse(&text).unwrap();
assert_eq!(
back.lookup_str("powers/squares").map(|id| back
.resolved(id)
.as_str()
.unwrap()
.to_string()),
Some("1764".to_string())
);
}
}