use crate::yaml::Mapping;
pub trait YamlPath {
fn try_get_path(&self, path: &str) -> Result<crate::as_yaml::YamlNode, PathError> {
self.try_get_path_with(path, AliasPolicy::Follow)
}
fn try_get_path_with(
&self,
path: &str,
policy: AliasPolicy,
) -> Result<crate::as_yaml::YamlNode, PathError>;
fn try_set_path(&self, path: &str, value: impl crate::AsYaml) -> Result<(), PathError> {
self.try_set_path_with(path, value, AliasPolicy::Refuse)
}
fn try_set_path_with(
&self,
path: &str,
value: impl crate::AsYaml,
policy: AliasPolicy,
) -> Result<(), PathError>;
fn try_remove_path(&self, path: &str) -> Result<crate::as_yaml::YamlNode, PathError> {
self.try_remove_path_with(path, AliasPolicy::Refuse)
}
fn try_remove_path_with(
&self,
path: &str,
policy: AliasPolicy,
) -> Result<crate::as_yaml::YamlNode, PathError>;
#[deprecated(
since = "0.4.0",
note = "use try_get_path; get_path swallows PathError as None"
)]
fn get_path(&self, path: &str) -> Option<crate::as_yaml::YamlNode> {
self.try_get_path(path).ok()
}
#[deprecated(
since = "0.4.0",
note = "use try_set_path; set_path silently ignores PathError"
)]
fn set_path(&self, path: &str, value: impl crate::AsYaml) {
let _ = self.try_set_path(path, value);
}
#[deprecated(
since = "0.4.0",
note = "use try_remove_path; remove_path swallows PathError as false"
)]
fn remove_path(&self, path: &str) -> bool {
self.try_remove_path(path).is_ok()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum PathSegment {
Key(String),
Index(usize),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathParseError {
UnclosedIndex,
InvalidIndex(String),
}
impl std::fmt::Display for PathParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PathParseError::UnclosedIndex => {
write!(f, "unclosed '[' in path")
}
PathParseError::InvalidIndex(text) => {
write!(f, "invalid path index [{text}]")
}
}
}
}
impl std::error::Error for PathParseError {}
pub const MAX_INDEX_GROWTH: usize = 256;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathError {
Parse(PathParseError),
EmptyPath,
NoRoot,
TypeMismatch {
at: String,
},
NotFound {
at: String,
},
AliasRefused {
at: String,
alias: String,
},
IndexTooFar {
at: String,
len: usize,
index: usize,
},
UndefinedAlias {
at: String,
alias: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AliasPolicy {
#[default]
Refuse,
Follow,
Expand,
}
impl std::fmt::Display for PathError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PathError::Parse(e) => write!(f, "path parse error: {e}"),
PathError::EmptyPath => write!(f, "path is empty (no segments)"),
PathError::NoRoot => write!(f, "document has no root to descend into"),
PathError::TypeMismatch { at } => {
write!(f, "path segment {at:?} is not a container")
}
PathError::NotFound { at } => write!(f, "path segment {at:?} not found"),
PathError::AliasRefused { at, alias } => write!(
f,
"path segment {at:?} is an alias to {alias:?}; refusing to descend through it"
),
PathError::IndexTooFar { at, len, index } => write!(
f,
"path segment {at:?} would grow a sequence of {len} by {} null entries to reach index {index}",
index + 1 - len
),
PathError::UndefinedAlias { at, alias } => write!(
f,
"path segment {at:?} is an alias to undefined anchor {alias:?}"
),
}
}
}
impl std::error::Error for PathError {}
impl From<PathParseError> for PathError {
fn from(e: PathParseError) -> Self {
PathError::Parse(e)
}
}
pub fn try_parse_path(path: &str) -> Result<Vec<PathSegment>, PathParseError> {
if path.is_empty() {
return Ok(vec![]);
}
let mut segments = Vec::new();
let mut current = String::new();
let mut chars = path.chars().peekable();
let mut escaped = false;
while let Some(ch) = chars.next() {
if escaped {
current.push(ch);
escaped = false;
continue;
}
match ch {
'\\' => {
escaped = true;
}
'.' => {
if !current.is_empty() {
if let Ok(index) = current.parse::<usize>() {
segments.push(PathSegment::Index(index));
} else {
segments.push(PathSegment::Key(current.clone()));
}
current.clear();
}
}
'[' => {
if !current.is_empty() {
segments.push(PathSegment::Key(current.clone()));
current.clear();
}
let mut index_str = String::new();
let mut closed = false;
while let Some(&next_ch) = chars.peek() {
if next_ch == ']' {
chars.next(); closed = true;
break;
}
index_str.push(chars.next().unwrap());
}
if !closed {
return Err(PathParseError::UnclosedIndex);
}
match index_str.parse::<usize>() {
Ok(index) => segments.push(PathSegment::Index(index)),
Err(_) => return Err(PathParseError::InvalidIndex(index_str)),
}
}
_ => {
current.push(ch);
}
}
}
if !current.is_empty() {
if let Ok(index) = current.parse::<usize>() {
segments.push(PathSegment::Index(index));
} else {
segments.push(PathSegment::Key(current));
}
}
Ok(segments)
}
#[deprecated(note = "use try_parse_path; a bad [index] used to return an empty Vec")]
pub fn parse_path(path: &str) -> Vec<PathSegment> {
try_parse_path(path).unwrap_or_default()
}
fn path_segments_required(path: &str) -> Result<Vec<PathSegment>, PathError> {
let segments = try_parse_path(path)?;
if segments.is_empty() {
Err(PathError::EmptyPath)
} else {
Ok(segments)
}
}
fn segment_display(segment: &PathSegment) -> String {
match segment {
PathSegment::Key(key) => key.clone(),
PathSegment::Index(index) => format!("[{index}]"),
}
}
fn node_as_mapping(node: &crate::as_yaml::YamlNode) -> Option<Mapping> {
match node {
crate::as_yaml::YamlNode::Mapping(m) => Some(m.clone()),
crate::as_yaml::YamlNode::TaggedNode(t) => t.as_mapping(),
_ => None,
}
}
fn node_as_sequence(node: &crate::as_yaml::YamlNode) -> Option<crate::yaml::Sequence> {
match node {
crate::as_yaml::YamlNode::Sequence(s) => Some(s.clone()),
crate::as_yaml::YamlNode::TaggedNode(t) => t.as_sequence(),
_ => None,
}
}
fn is_replaceable_scalar(node: &crate::as_yaml::YamlNode) -> bool {
matches!(node, crate::as_yaml::YamlNode::Scalar(_))
}
fn navigate_path(
mut current: crate::as_yaml::YamlNode,
segments: &[PathSegment],
policy: AliasPolicy,
) -> Result<crate::as_yaml::YamlNode, PathError> {
for segment in segments {
current = descend_one(current, segment, policy)?;
}
Ok(current)
}
fn registry_for(node: &crate::as_yaml::YamlNode) -> crate::anchor_resolution::AnchorRegistry {
let root = node
.syntax()
.ancestors()
.last()
.unwrap_or_else(|| node.syntax().clone());
crate::anchor_resolution::AnchorRegistry::from_tree(&root)
}
fn resolve_for_read(
current: crate::as_yaml::YamlNode,
segment: &PathSegment,
policy: AliasPolicy,
) -> Result<crate::as_yaml::YamlNode, PathError> {
let crate::as_yaml::YamlNode::Alias(alias) = ¤t else {
return Ok(current);
};
let name = alias.name();
if policy == AliasPolicy::Refuse {
return Err(PathError::AliasRefused {
at: segment_display(segment),
alias: name,
});
}
let registry = registry_for(¤t);
let target = registry
.resolve(&name)
.cloned()
.ok_or_else(|| PathError::UndefinedAlias {
at: segment_display(segment),
alias: name.clone(),
})?;
crate::as_yaml::YamlNode::from_syntax_peeled(target).ok_or(PathError::UndefinedAlias {
at: segment_display(segment),
alias: name,
})
}
fn lookup_merged(mapping: &Mapping, key: &str) -> Option<crate::as_yaml::YamlNode> {
if let Some(found) = mapping.get(key) {
return Some(found);
}
use crate::anchor_resolution::MappingMergedExt;
let registry = registry_for(&crate::as_yaml::YamlNode::Mapping(mapping.clone()));
mapping.merged(®istry).get(key)
}
fn descend_one(
current: crate::as_yaml::YamlNode,
segment: &PathSegment,
policy: AliasPolicy,
) -> Result<crate::as_yaml::YamlNode, PathError> {
let current = resolve_for_read(current, segment, policy)?;
match segment {
PathSegment::Key(key) => {
let mapping = node_as_mapping(¤t).ok_or_else(|| PathError::TypeMismatch {
at: segment_display(segment),
})?;
lookup_merged(&mapping, key).ok_or_else(|| PathError::NotFound {
at: segment_display(segment),
})
}
PathSegment::Index(index) => {
if let Some(seq) = node_as_sequence(¤t) {
seq.get(*index).ok_or_else(|| PathError::NotFound {
at: segment_display(segment),
})
} else if let Some(map) = node_as_mapping(¤t) {
lookup_merged(&map, &index.to_string()).ok_or_else(|| PathError::NotFound {
at: segment_display(segment),
})
} else {
Err(PathError::TypeMismatch {
at: segment_display(segment),
})
}
}
}
}
fn segment_key(segment: &PathSegment) -> String {
match segment {
PathSegment::Key(key) => key.clone(),
PathSegment::Index(index) => index.to_string(),
}
}
impl YamlPath for crate::yaml::Document {
fn try_get_path_with(
&self,
path: &str,
policy: AliasPolicy,
) -> Result<crate::as_yaml::YamlNode, PathError> {
let segments = path_segments_required(path)?;
let root = if let Some(m) = self.as_mapping() {
crate::as_yaml::YamlNode::Mapping(m)
} else if let Some(s) = self.as_sequence() {
crate::as_yaml::YamlNode::Sequence(s)
} else if let Some(sc) = self.as_scalar() {
crate::as_yaml::YamlNode::Scalar(sc)
} else {
return Err(PathError::NoRoot);
};
navigate_path(root, &segments, policy)
}
fn try_set_path_with(
&self,
path: &str,
value: impl crate::AsYaml,
policy: AliasPolicy,
) -> Result<(), PathError> {
let segments = path_segments_required(path)?;
let mapping = self.as_mapping().ok_or(PathError::NoRoot)?;
set_path_on_mapping(&mapping, &segments, value, policy)
}
fn try_remove_path_with(
&self,
path: &str,
policy: AliasPolicy,
) -> Result<crate::as_yaml::YamlNode, PathError> {
let segments = path_segments_required(path)?;
let root = if let Some(m) = self.as_mapping() {
crate::as_yaml::YamlNode::Mapping(m)
} else if let Some(s) = self.as_sequence() {
crate::as_yaml::YamlNode::Sequence(s)
} else {
return Err(PathError::NoRoot);
};
remove_path_impl(root, &segments, policy)
}
}
fn remove_path_impl(
root: crate::as_yaml::YamlNode,
segments: &[PathSegment],
policy: AliasPolicy,
) -> Result<crate::as_yaml::YamlNode, PathError> {
debug_assert!(!segments.is_empty(), "caller must reject empty paths");
let root = resolve_for_read(root, &segments[0], policy)?;
if segments.len() == 1 {
let seg = &segments[0];
let key = match seg {
PathSegment::Key(key) => key.clone(),
PathSegment::Index(index) => {
if node_as_mapping(&root).is_none() {
return Err(PathError::TypeMismatch {
at: segment_display(seg),
});
}
index.to_string()
}
};
let mapping = node_as_mapping(&root).ok_or_else(|| PathError::TypeMismatch {
at: segment_display(seg),
})?;
let value = mapping
.get(key.as_str())
.ok_or_else(|| PathError::NotFound {
at: segment_display(seg),
})?;
mapping.remove(key.as_str());
return Ok(value);
}
let nested = descend_one(root, &segments[0], policy)?;
remove_path_impl(nested, &segments[1..], policy)
}
impl YamlPath for Mapping {
fn try_get_path_with(
&self,
path: &str,
policy: AliasPolicy,
) -> Result<crate::as_yaml::YamlNode, PathError> {
let segments = path_segments_required(path)?;
navigate_path(
crate::as_yaml::YamlNode::Mapping(self.clone()),
&segments,
policy,
)
}
fn try_set_path_with(
&self,
path: &str,
value: impl crate::AsYaml,
policy: AliasPolicy,
) -> Result<(), PathError> {
let segments = path_segments_required(path)?;
set_path_on_mapping(self, &segments, value, policy)
}
fn try_remove_path_with(
&self,
path: &str,
policy: AliasPolicy,
) -> Result<crate::as_yaml::YamlNode, PathError> {
let segments = path_segments_required(path)?;
remove_path_impl(
crate::as_yaml::YamlNode::Mapping(self.clone()),
&segments,
policy,
)
}
}
fn resolve_alias_for_write(
alias: &crate::yaml::Alias,
segment: &PathSegment,
policy: AliasPolicy,
) -> Result<crate::as_yaml::YamlNode, PathError> {
use rowan::ast::AstNode;
let name = alias.name();
if policy == AliasPolicy::Refuse {
return Err(PathError::AliasRefused {
at: segment_display(segment),
alias: name,
});
}
let undefined = || PathError::UndefinedAlias {
at: segment_display(segment),
alias: name.clone(),
};
let root = alias
.syntax()
.ancestors()
.last()
.unwrap_or_else(|| alias.syntax().clone());
let target = crate::anchor_resolution::AnchorRegistry::from_tree(&root)
.resolve(&name)
.cloned()
.ok_or_else(undefined)?;
let target = if policy == AliasPolicy::Expand {
crate::yaml::SyntaxNode::new_root_mut(target.green().into_owned())
} else {
target
};
crate::as_yaml::YamlNode::from_syntax_peeled(target).ok_or_else(undefined)
}
fn set_path_on_node<V: crate::AsYaml>(
node: crate::as_yaml::YamlNode,
segments: &[PathSegment],
value: V,
policy: AliasPolicy,
via: &PathSegment,
) -> Result<(), PathError> {
if let Some(m) = node.as_mapping() {
set_path_on_mapping(m, segments, value, policy)
} else if let Some(seq) = node.as_sequence() {
set_path_on_sequence(seq, segments, value, policy)
} else {
Err(PathError::TypeMismatch {
at: segment_display(via),
})
}
}
fn set_path_on_mapping<V: crate::AsYaml>(
mapping: &Mapping,
segments: &[PathSegment],
value: V,
policy: AliasPolicy,
) -> Result<(), PathError> {
debug_assert!(!segments.is_empty(), "caller must reject empty paths");
let first_key_owned = segment_key(&segments[0]);
let first_key = first_key_owned.as_str();
if segments.len() == 1 {
mapping.set(first_key, value);
return Ok(());
}
if let Some(crate::as_yaml::YamlNode::Alias(alias)) = mapping.get(first_key) {
let resolved = resolve_alias_for_write(&alias, &segments[0], policy)?;
let target = if policy == AliasPolicy::Expand {
mapping.set(first_key, resolved);
mapping
.get(first_key)
.ok_or_else(|| PathError::TypeMismatch {
at: segment_display(&segments[0]),
})?
} else {
resolved
};
return set_path_on_node(target, &segments[1..], value, policy, &segments[0]);
}
let next_wants_sequence = matches!(segments[1], PathSegment::Index(_));
if let Some(existing) = mapping.get(first_key) {
if let Some(sc) = existing.as_scalar() {
let s = sc.as_string();
let is_null_placeholder = s.is_empty() || s.eq_ignore_ascii_case("null") || s == "~";
if !is_null_placeholder {
return Err(PathError::TypeMismatch {
at: segment_display(&segments[0]),
});
}
}
}
let existing = mapping.get(first_key);
if next_wants_sequence {
if let Some(nested) = existing.as_ref().and_then(node_as_sequence) {
return set_path_on_sequence(&nested, &segments[1..], value, policy);
}
if let Some(nested) = existing.as_ref().and_then(node_as_mapping) {
return set_path_on_mapping(&nested, &segments[1..], value, policy);
}
if existing.as_ref().is_some_and(|n| !is_replaceable_scalar(n)) {
return Err(PathError::TypeMismatch {
at: segment_display(&segments[0]),
});
}
if let PathSegment::Index(index) = segments[1] {
if index >= MAX_INDEX_GROWTH {
return Err(PathError::IndexTooFar {
at: segment_display(&segments[1]),
len: 0,
index,
});
}
}
if mapping.is_flow_style() {
mapping.set(first_key, crate::yaml::Sequence::new_flow());
} else {
mapping.set(first_key, crate::yaml::Sequence::new_pending_block());
}
let nested = mapping
.get_sequence(first_key)
.expect("we just inserted this key as a sequence");
return set_path_on_sequence(&nested, &segments[1..], value, policy);
}
if let Some(nested) = existing.as_ref().and_then(node_as_mapping) {
return set_path_on_mapping(&nested, &segments[1..], value, policy);
}
if existing.as_ref().is_some_and(|n| !is_replaceable_scalar(n)) {
return Err(PathError::TypeMismatch {
at: segment_display(&segments[0]),
});
}
if mapping.is_flow_style() {
mapping.set(first_key, Mapping::new_flow());
} else {
mapping.set(first_key, Mapping::new_pending_block());
}
let nested = mapping
.get_mapping(first_key)
.expect("we just inserted this key as a mapping");
set_path_on_mapping(&nested, &segments[1..], value, policy)
}
fn set_path_on_sequence<V: crate::AsYaml>(
sequence: &crate::yaml::Sequence,
segments: &[PathSegment],
value: V,
policy: AliasPolicy,
) -> Result<(), PathError> {
debug_assert!(!segments.is_empty(), "caller must reject empty paths");
let index = match &segments[0] {
PathSegment::Index(i) => *i,
PathSegment::Key(_) => {
return Err(PathError::TypeMismatch {
at: segment_display(&segments[0]),
});
}
};
let len = sequence.len();
if index >= len && index - len >= MAX_INDEX_GROWTH {
return Err(PathError::IndexTooFar {
at: segment_display(&segments[0]),
len,
index,
});
}
if let Some(growth) = (index + 1).checked_sub(len) {
sequence.extend_with(crate::scalar::ScalarValue::null(), growth);
}
if segments.len() == 1 {
sequence.set(index, value);
return Ok(());
}
if let Some(crate::as_yaml::YamlNode::Alias(alias)) = sequence.get(index) {
let resolved = resolve_alias_for_write(&alias, &segments[0], policy)?;
let target = if policy == AliasPolicy::Expand {
sequence.set(index, resolved);
sequence.get(index).ok_or_else(|| PathError::TypeMismatch {
at: segment_display(&segments[0]),
})?
} else {
resolved
};
return set_path_on_node(target, &segments[1..], value, policy, &segments[0]);
}
if let Some(existing) = sequence.get(index) {
if let Some(sc) = existing.as_scalar() {
let s = sc.as_string();
let is_null_placeholder = s.is_empty() || s.eq_ignore_ascii_case("null") || s == "~";
if !is_null_placeholder {
return Err(PathError::TypeMismatch {
at: segment_display(&segments[0]),
});
}
}
}
let next_wants_sequence = matches!(segments[1], PathSegment::Index(_));
let existing = sequence.get(index);
if next_wants_sequence {
if let Some(nested) = existing.as_ref().and_then(node_as_sequence) {
return set_path_on_sequence(&nested, &segments[1..], value, policy);
}
if let Some(nested) = existing.as_ref().and_then(node_as_mapping) {
return set_path_on_mapping(&nested, &segments[1..], value, policy);
}
if existing.as_ref().is_some_and(|n| !is_replaceable_scalar(n)) {
return Err(PathError::TypeMismatch {
at: segment_display(&segments[0]),
});
}
let flow_empty = crate::builder::SequenceBuilder::new()
.build_document()
.as_sequence()
.expect("SequenceBuilder always produces a sequence");
sequence.set(index, flow_empty);
let nested = sequence
.get(index)
.and_then(|n| n.as_sequence().cloned())
.expect("we just inserted a sequence at this index");
return set_path_on_sequence(&nested, &segments[1..], value, policy);
}
if let Some(nested) = existing.as_ref().and_then(node_as_mapping) {
return set_path_on_mapping(&nested, &segments[1..], value, policy);
}
if existing.as_ref().is_some_and(|n| !is_replaceable_scalar(n)) {
return Err(PathError::TypeMismatch {
at: segment_display(&segments[0]),
});
}
let flow_empty = crate::builder::MappingBuilder::new()
.build_document()
.as_mapping()
.expect("MappingBuilder always produces a mapping");
sequence.set(index, flow_empty);
let nested = sequence
.get(index)
.and_then(|n| n.as_mapping().cloned())
.expect("we just inserted a mapping at this index");
set_path_on_mapping(&nested, &segments[1..], value, policy)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_path_bounds_sequence_growth() {
use crate::Document;
use std::str::FromStr;
let doc = Document::from_str("m: {}\n").unwrap();
let err = doc
.try_set_path("999.hh.hh.999.9aa", "")
.expect_err("a far index must be refused");
assert!(
matches!(err, PathError::IndexTooFar { .. }),
"unexpected error: {err}"
);
assert!(doc.to_string().len() < 64, "{:?}", doc.to_string());
let doc = Document::from_str("m: {}\n").unwrap();
doc.try_set_path("s[5]", "v").expect("small index is fine");
assert_eq!(doc.try_get_path("s[5]").unwrap().to_string(), "v");
let doc = Document::from_str("m: {}\n").unwrap();
assert!(matches!(
doc.try_set_path(&format!("s[{MAX_INDEX_GROWTH}]"), "v"),
Err(PathError::IndexTooFar { .. })
));
}
fn merge_doc() -> crate::yaml::YamlFile {
use std::str::FromStr;
crate::yaml::YamlFile::from_str(
"base: &b\n a: 1\n shared: x\nderived:\n <<: *b\n own: 2\n a: 99\n",
)
.unwrap()
}
#[test]
fn get_path_resolves_merged_keys() {
let file = merge_doc();
let doc = file.document().unwrap();
assert_eq!(doc.try_get_path("derived.shared").unwrap().to_string(), "x");
assert_eq!(doc.try_get_path("derived.a").unwrap().to_string(), "99");
assert_eq!(doc.try_get_path("derived.own").unwrap().to_string(), "2");
assert_eq!(doc.try_get_path("base.a").unwrap().to_string(), "1");
}
#[test]
fn get_path_still_reports_genuinely_absent_keys() {
let file = merge_doc();
let doc = file.document().unwrap();
assert!(doc.try_get_path("derived.nope").is_err());
}
#[test]
fn remove_path_stays_literal_for_merged_keys() {
let file = merge_doc();
let before = file.to_string();
let doc = file.document().unwrap();
assert!(doc.try_remove_path("derived.shared").is_err());
assert_eq!(file.to_string(), before);
}
#[test]
fn test_parse_path_basic() {
assert_eq!(try_parse_path("").unwrap(), Vec::<PathSegment>::new());
assert_eq!(
try_parse_path("key").unwrap(),
vec![PathSegment::Key("key".to_string())]
);
assert_eq!(
try_parse_path("a.b").unwrap(),
vec![
PathSegment::Key("a".to_string()),
PathSegment::Key("b".to_string())
]
);
assert_eq!(
try_parse_path("a.b.c.d").unwrap(),
vec![
PathSegment::Key("a".to_string()),
PathSegment::Key("b".to_string()),
PathSegment::Key("c".to_string()),
PathSegment::Key("d".to_string())
]
);
}
#[test]
fn test_try_parse_path_invalid_brackets() {
assert_eq!(
try_parse_path("items[abc].name"),
Err(PathParseError::InvalidIndex("abc".to_string()))
);
assert_eq!(
try_parse_path("items[].name"),
Err(PathParseError::InvalidIndex(String::new()))
);
assert_eq!(
try_parse_path("items[0"),
Err(PathParseError::UnclosedIndex)
);
}
#[test]
#[allow(deprecated)]
fn test_parse_path_deprecated_wrapper_empty_on_bad_index() {
assert_eq!(parse_path("items[abc].name"), Vec::<PathSegment>::new());
assert_eq!(parse_path("items[0"), Vec::<PathSegment>::new());
}
#[test]
fn test_parse_path_with_array_indices() {
assert_eq!(
try_parse_path("items[0]").unwrap(),
vec![PathSegment::Key("items".to_string()), PathSegment::Index(0)]
);
assert_eq!(
try_parse_path("items[0].name").unwrap(),
vec![
PathSegment::Key("items".to_string()),
PathSegment::Index(0),
PathSegment::Key("name".to_string())
]
);
assert_eq!(
try_parse_path("data.items[5].value").unwrap(),
vec![
PathSegment::Key("data".to_string()),
PathSegment::Key("items".to_string()),
PathSegment::Index(5),
PathSegment::Key("value".to_string())
]
);
}
#[test]
fn test_parse_path_with_numeric_indices() {
assert_eq!(
try_parse_path("items.0").unwrap(),
vec![PathSegment::Key("items".to_string()), PathSegment::Index(0)]
);
assert_eq!(
try_parse_path("items.0.name").unwrap(),
vec![
PathSegment::Key("items".to_string()),
PathSegment::Index(0),
PathSegment::Key("name".to_string())
]
);
}
#[test]
fn test_parse_path_with_escaping() {
assert_eq!(
try_parse_path("key\\.with\\.dots").unwrap(),
vec![PathSegment::Key("key.with.dots".to_string())]
);
assert_eq!(
try_parse_path("a.key\\.with\\.dots.b").unwrap(),
vec![
PathSegment::Key("a".to_string()),
PathSegment::Key("key.with.dots".to_string()),
PathSegment::Key("b".to_string())
]
);
}
#[test]
fn test_get_path_with_array_index() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = r#"
items:
- name: first
value: 1
- name: second
value: 2
"#;
let doc = Document::from_str(yaml).unwrap();
let name = doc.try_get_path("items[0].name").ok();
assert_eq!(
name.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.as_string()),
Some("first".to_string())
);
let value = doc.try_get_path("items[1].value").ok();
assert_eq!(
value
.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.as_string()),
Some("2".to_string())
);
}
#[test]
fn test_get_path_with_numeric_index() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = r#"
items:
- name: first
value: 1
- name: second
value: 2
"#;
let doc = Document::from_str(yaml).unwrap();
let name = doc.try_get_path("items.0.name").ok();
assert_eq!(
name.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.as_string()),
Some("first".to_string())
);
let value = doc.try_get_path("items.1.value").ok();
assert_eq!(
value
.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.as_string()),
Some("2".to_string())
);
}
#[test]
fn test_get_path_with_escaping() {
use crate::yaml::Document;
let doc = Document::new();
doc.set("key.with.dots", "test value");
assert!(doc.try_get_path("key.with.dots").is_err());
let value = doc.try_get_path("key\\.with\\.dots").ok();
assert_eq!(
value
.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.as_string()),
Some("test value".to_string())
);
}
#[test]
fn test_get_path_array_only() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = r#"
- first
- second
- third
"#;
let doc = Document::from_str(yaml).unwrap();
let item = doc.try_get_path("0").ok();
assert_eq!(
item.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.as_string()),
Some("first".to_string())
);
let item = doc.try_get_path("2").ok();
assert_eq!(
item.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.as_string()),
Some("third".to_string())
);
}
#[test]
fn test_remove_path_with_array_index() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = r#"
items:
- name: first
nested:
key: value
"#;
let doc = Document::from_str(yaml).unwrap();
assert!(doc.try_remove_path("items[0].nested.key").is_ok());
assert!(doc.try_get_path("items[0].nested.key").is_err());
assert!(doc.try_get_path("items[0].nested").is_ok());
}
#[test]
fn test_mapping_get_path_with_indices() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = r#"
config:
servers:
- host: server1.com
port: 8080
- host: server2.com
port: 9090
"#;
let doc = Document::from_str(yaml).unwrap();
let mapping = doc.as_mapping().unwrap();
let host = mapping.try_get_path("config.servers[0].host").ok();
assert_eq!(
host.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.as_string()),
Some("server1.com".to_string())
);
let port = mapping.try_get_path("config.servers.1.port").ok();
assert_eq!(
port.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.as_string()),
Some("9090".to_string())
);
}
#[test]
fn test_get_path_simple() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = Document::from_str("name: Alice\nage: 30\n").unwrap();
let name = yaml.try_get_path("name").ok();
assert_eq!(
name.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.to_string()),
Some("Alice".to_string())
);
let age = yaml.try_get_path("age").ok();
assert_eq!(
age.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.to_string()),
Some("30".to_string())
);
}
#[test]
fn test_get_path_nested() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = Document::from_str("server:\n host: localhost\n port: 8080\n").unwrap();
let host = yaml.try_get_path("server.host").ok();
assert_eq!(
host.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.to_string()),
Some("localhost".to_string())
);
let port = yaml.try_get_path("server.port").ok();
assert_eq!(
port.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.to_string()),
Some("8080".to_string())
);
}
#[test]
fn test_get_path_deeply_nested() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = Document::from_str(
"app:\n database:\n primary:\n host: db.example.com\n port: 5432\n",
)
.unwrap();
let host = yaml.try_get_path("app.database.primary.host").ok();
assert_eq!(
host.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.to_string()),
Some("db.example.com".to_string())
);
let port = yaml.try_get_path("app.database.primary.port").ok();
assert_eq!(
port.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.to_string()),
Some("5432".to_string())
);
}
#[test]
fn test_get_path_missing() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = Document::from_str("name: Alice\n").unwrap();
assert!(matches!(
yaml.try_get_path("missing"),
Err(PathError::NotFound { .. })
));
assert!(matches!(
yaml.try_get_path("name.nested"),
Err(PathError::TypeMismatch { .. })
));
assert_eq!(yaml.try_get_path(""), Err(PathError::EmptyPath));
}
#[test]
fn test_set_path_existing_key() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = Document::from_str("name: Alice\nage: 30\n").unwrap();
yaml.try_set_path("name", "Bob").expect("set_path");
assert_eq!(yaml.to_string(), "name: Bob\nage: 30\n");
}
#[test]
fn test_set_path_new_key() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = Document::from_str("name: Alice\n").unwrap();
yaml.try_set_path("age", 30).expect("set_path");
assert_eq!(yaml.to_string(), "name: Alice\nage: 30\n");
}
#[test]
fn test_set_path_nested_existing() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = Document::from_str("server:\n host: localhost\n port: 8080\n").unwrap();
yaml.try_set_path("server.port", 9000).expect("set_path");
assert_eq!(
yaml.to_string(),
"server:\n host: localhost\n port: 9000\n"
);
}
#[test]
fn test_set_path_nested_new() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = Document::from_str("server:\n host: localhost\n").unwrap();
yaml.try_set_path("server.port", 8080).expect("set_path");
assert_eq!(
yaml.to_string(),
"server:\n host: localhost\n port: 8080\n"
);
}
#[test]
fn test_set_path_create_intermediate() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = Document::from_str("name: test\n").unwrap();
yaml.try_set_path("server.database.host", "localhost")
.expect("set_path");
assert_eq!(
yaml.to_string(),
"name: test\nserver:\n database:\n host: localhost\n"
);
let host = yaml.try_get_path("server.database.host").ok();
assert_eq!(
host.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.to_string()),
Some("localhost".to_string())
);
}
#[test]
fn test_set_path_deeply_nested_create() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = Document::from_str("app: {}\n").unwrap();
yaml.try_set_path("app.database.primary.host", "db.example.com")
.expect("set_path");
yaml.try_set_path("app.database.primary.port", 5432)
.expect("set_path");
assert_eq!(
yaml.to_string().trim(),
r#"app: {database: {primary: {host: "db.example.com", port: 5432}}}"#
);
let host = yaml.try_get_path("app.database.primary.host").ok();
assert_eq!(
host.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.as_string()),
Some("db.example.com".to_string())
);
let port = yaml.try_get_path("app.database.primary.port").ok();
assert_eq!(port.as_ref().and_then(|v| v.to_i64()), Some(5432));
}
#[test]
fn test_remove_path_simple() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = Document::from_str("name: Alice\nage: 30\n").unwrap();
let result = yaml.try_remove_path("age").is_ok();
assert!(result);
assert_eq!(yaml.to_string(), "name: Alice\n");
}
#[test]
fn test_remove_path_nested() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = Document::from_str("server:\n host: localhost\n port: 8080\n").unwrap();
let result = yaml.try_remove_path("server.port").is_ok();
assert!(result);
assert_eq!(yaml.to_string(), "server:\n host: localhost\n");
}
#[test]
fn test_remove_path_missing() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = Document::from_str("name: Alice\n").unwrap();
let result = yaml.try_remove_path("missing").is_ok();
assert!(!result);
let result = yaml.try_remove_path("name.nested").is_ok();
assert!(!result);
assert_eq!(yaml.to_string(), "name: Alice\n");
}
#[test]
fn test_remove_path_deeply_nested() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = Document::from_str(
"app:\n database:\n primary:\n host: db.example.com\n port: 5432\n",
)
.unwrap();
let result = yaml.try_remove_path("app.database.primary.port").is_ok();
assert!(result);
assert_eq!(
yaml.to_string(),
"app:\n database:\n primary:\n host: db.example.com\n"
);
}
#[test]
fn test_remove_path_keeps_sibling_indentation() {
use crate::yaml::Document;
use std::str::FromStr;
let doc = Document::from_str("config:\n a: 1\n b: 2\n c: 3\n").unwrap();
doc.try_remove_path("config.b").unwrap();
assert_eq!(doc.to_string(), "config:\n a: 1\n c: 3\n");
let doc = Document::from_str("config:\n a: 1\n b: 2\n c: 3\n").unwrap();
doc.try_remove_path("config.a").unwrap();
assert_eq!(doc.to_string(), "config:\n b: 2\n c: 3\n");
let doc = Document::from_str("config:\n a: 1\n b: 2\n c: 3\n").unwrap();
doc.try_remove_path("config.c").unwrap();
assert_eq!(doc.to_string(), "config:\n a: 1\n b: 2\n");
let doc = Document::from_str("x:\n y:\n a: 1\n b: 2\n").unwrap();
doc.try_remove_path("x.y.a").unwrap();
assert_eq!(doc.to_string(), "x:\n y:\n b: 2\n");
}
#[test]
fn test_path_on_mapping_directly() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = Document::from_str("server:\n host: localhost\n").unwrap();
let mapping = yaml.as_mapping().unwrap();
let host = mapping.try_get_path("server.host").ok();
assert_eq!(
host.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.to_string()),
Some("localhost".to_string())
);
mapping.try_set_path("server.port", 8080).expect("set_path");
assert_eq!(
yaml.to_string(),
"server:\n host: localhost\n port: 8080\n"
);
let result = mapping.try_remove_path("server.port").is_ok();
assert!(result);
let result_missing = mapping.try_remove_path("nonexistent.path").is_ok();
assert!(!result_missing);
}
#[test]
fn test_mapping_remove_path_single_segment() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = Document::from_str("a: 1\nb: 2\n").unwrap();
let mapping = yaml.as_mapping().unwrap();
assert!(mapping.try_remove_path("a").is_ok());
assert!(mapping.try_get_path("a").is_err());
assert!(mapping.try_get_path("b").is_ok());
assert!(mapping.try_remove_path("missing").is_err());
}
#[test]
fn test_set_path_preserves_formatting() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = Document::from_str("server:\n host: localhost # production server\n").unwrap();
yaml.try_set_path("server.host", "newhost")
.expect("set_path");
assert_eq!(
yaml.to_string(),
"server:\n host: newhost # production server\n"
);
}
#[test]
fn test_multiple_path_operations() {
use crate::yaml::Document;
use std::str::FromStr;
let yaml = Document::from_str("name: test\n").unwrap();
yaml.try_set_path("server.host", "localhost")
.expect("set_path");
yaml.try_set_path("server.port", 8080).expect("set_path");
yaml.try_set_path("database.host", "db.local")
.expect("set_path");
yaml.try_set_path("database.port", 5432).expect("set_path");
assert_eq!(
yaml.try_get_path("server.host")
.ok()
.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.to_string()),
Some("localhost".to_string())
);
assert_eq!(
yaml.try_get_path("server.port")
.ok()
.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.to_string()),
Some("8080".to_string())
);
assert_eq!(
yaml.try_get_path("database.host")
.ok()
.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.to_string()),
Some("db.local".to_string())
);
assert_eq!(
yaml.try_get_path("database.port")
.ok()
.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.to_string()),
Some("5432".to_string())
);
yaml.try_remove_path("server.port").expect("remove_path");
yaml.try_remove_path("database.host").expect("remove_path");
assert!(yaml.try_get_path("server.port").is_err());
assert!(yaml.try_get_path("database.host").is_err());
assert_eq!(
yaml.try_get_path("server.host")
.ok()
.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.to_string()),
Some("localhost".to_string())
);
assert_eq!(
yaml.try_get_path("database.port")
.ok()
.as_ref()
.and_then(|v| v.as_scalar())
.map(|s| s.to_string()),
Some("5432".to_string())
);
}
#[test]
fn test_set_path_creates_intermediate_sequence() {
use crate::yaml::Document;
use std::str::FromStr;
let doc = Document::from_str("base: true\n").unwrap();
doc.try_set_path("a.b[0].c", "value").expect("set_path");
assert_eq!(
doc.to_string(),
"base: true\na:\n b:\n - {c: \"value\"}\n"
);
}
#[test]
fn test_set_path_into_existing_sequence_by_index() {
use crate::yaml::Document;
use std::str::FromStr;
let doc = Document::from_str("items:\n - a\n - b\n").unwrap();
doc.try_set_path("items[1]", "B").expect("set_path");
assert_eq!(doc.to_string(), "items:\n - a\n - B\n");
}
#[test]
fn test_set_path_grows_sequence_with_nulls() {
use crate::yaml::Document;
use std::str::FromStr;
let doc = Document::from_str("items:\n - a\n").unwrap();
doc.try_set_path("items[3]", "z").expect("set_path");
assert_eq!(
doc.to_string(),
"items:\n - a\n - null\n - null\n - z\n"
);
}
#[test]
fn test_set_path_index_does_not_replace_existing_mapping() {
use crate::yaml::Document;
use std::str::FromStr;
let doc = Document::from_str("m:\n a: 1\n b: 2\n").unwrap();
doc.try_set_path("m[0]", "z").expect("set_path");
assert_eq!(doc.to_string(), "m:\n a: 1\n b: 2\n '0': z\n");
let doc = Document::from_str("m:\n \"0\":\n x: 1\n").unwrap();
assert_eq!(
doc.try_get_path("m.0.x")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"1"
);
doc.try_set_path("m.0.x", "2").expect("set_path");
assert_eq!(
doc.try_get_path("m.0.x")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"2"
);
assert!(doc.get_mapping("m").is_some());
let doc = Document::from_str("items:\n - a: 1\n b: 2\n").unwrap();
doc.try_set_path("items[0][0]", "z").expect("set_path");
assert_eq!(doc.to_string(), "items:\n - a: 1\n b: 2\n '0': z\n");
}
#[test]
fn test_set_path_key_does_not_replace_existing_sequence() {
use crate::yaml::Document;
use std::str::FromStr;
let original = "items:\n- a\n- b\n";
let doc = Document::from_str(original).unwrap();
assert!(matches!(
doc.try_get_path("items.foo"),
Err(PathError::TypeMismatch { .. })
));
assert!(matches!(
doc.try_set_path("items.foo", "x"),
Err(PathError::TypeMismatch { .. })
));
assert_eq!(doc.to_string(), original);
let original = "other: &o\n a: 1\nitems: *o\n";
let doc = Document::from_str(original).unwrap();
assert_eq!(
doc.try_set_path("items.foo", "x"),
Err(PathError::AliasRefused {
at: "items".to_string(),
alias: "o".to_string()
})
);
assert_eq!(doc.to_string(), original);
}
#[test]
fn test_set_path_index_does_not_replace_alias() {
use crate::yaml::Document;
use std::str::FromStr;
let original = "other: &o\n- a\n- b\nitems: *o\n";
let doc = Document::from_str(original).unwrap();
assert_eq!(
doc.try_get_path("items[0]").unwrap().to_string(),
"a".to_string()
);
assert_eq!(
doc.try_set_path("items[0]", "x"),
Err(PathError::AliasRefused {
at: "items".to_string(),
alias: "o".to_string()
})
);
assert_eq!(doc.to_string(), original);
let doc = Document::from_str("items: !!seq [1, 2]\n").unwrap();
doc.try_set_path("items[1]", "x").unwrap();
assert_eq!(doc.to_string(), "items: !!seq [1, x]\n");
let original = "other: &o\n a: 1\ntop:\n - *o\n";
let doc = Document::from_str(original).unwrap();
assert_eq!(
doc.try_set_path("top[0][0]", "x"),
Err(PathError::AliasRefused {
at: "[0]".to_string(),
alias: "o".to_string()
})
);
assert_eq!(doc.to_string(), original);
let doc = Document::from_str("top:\n - !custom\n a: 1\n").unwrap();
doc.try_set_path("top[0][0]", "x").unwrap();
assert_eq!(doc.to_string(), "top:\n - !custom\n a: 1\n '0': x\n");
}
#[test]
fn test_path_descends_into_tagged_mapping() {
use crate::yaml::Document;
use std::str::FromStr;
let doc = Document::from_str("config: !custom\n a: 1\n b: 2\n").unwrap();
assert_eq!(
doc.try_get_path("config.a")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"1"
);
doc.try_set_path("config.a", 5).unwrap();
doc.try_set_path("config.c", 7).unwrap();
assert_eq!(doc.to_string(), "config: !custom\n a: 5\n b: 2\n c: 7\n");
}
#[test]
fn test_path_descends_into_tagged_sequence() {
use crate::yaml::Document;
use std::str::FromStr;
let doc = Document::from_str("items: !!seq [1, 2]\n").unwrap();
assert_eq!(
doc.try_get_path("items[0]")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"1"
);
doc.try_set_path("items[0]", 9).unwrap();
doc.try_set_path("items[2]", 7).unwrap();
assert_eq!(doc.to_string(), "items: !!seq [9, 2, 7]\n");
}
#[test]
fn test_path_descends_through_nested_tags() {
use crate::yaml::Document;
use std::str::FromStr;
let doc = Document::from_str("a: !outer\n b: !inner\n c: 1\n").unwrap();
doc.try_set_path("a.b.c", 2).unwrap();
assert_eq!(doc.to_string(), "a: !outer\n b: !inner\n c: 2\n");
let doc = Document::from_str("l: !t\n - x: 1\n").unwrap();
doc.try_set_path("l[0].x", 9).unwrap();
assert_eq!(doc.to_string(), "l: !t\n - x: 9\n");
}
#[test]
fn test_remove_path_in_tagged_mapping_keeps_tag() {
use crate::yaml::Document;
use std::str::FromStr;
let doc = Document::from_str("config: !custom\n a: 1\n b: 2\n").unwrap();
let removed = doc.try_remove_path("config.b").unwrap();
assert_eq!(removed.as_scalar().unwrap().as_string(), "2");
assert_eq!(doc.to_string(), "config: !custom\n a: 1\n");
doc.try_remove_path("config.a").unwrap();
assert_eq!(doc.to_string(), "config: !custom {}\n");
}
#[test]
fn test_path_does_not_descend_into_tagged_scalar() {
use crate::yaml::Document;
use std::str::FromStr;
let original = "config: !custom hello\n";
let doc = Document::from_str(original).unwrap();
assert!(matches!(
doc.try_get_path("config.a"),
Err(PathError::TypeMismatch { .. })
));
assert!(matches!(
doc.try_set_path("config.a", 1),
Err(PathError::TypeMismatch { .. })
));
assert_eq!(doc.to_string(), original);
}
const ALIASED: &str = "shared: &shared\n timeout: 30\nservice: *shared\n";
#[test]
fn test_get_path_resolves_alias_by_default() {
use crate::yaml::Document;
use std::str::FromStr;
let doc = Document::from_str(ALIASED).unwrap();
assert_eq!(
doc.try_get_path("service.timeout").unwrap().to_string(),
"30".to_string()
);
assert_eq!(doc.to_string(), ALIASED.to_string());
}
#[test]
fn test_get_path_refuse_policy_reports_alias() {
use crate::yaml::Document;
use std::str::FromStr;
let doc = Document::from_str(ALIASED).unwrap();
assert_eq!(
doc.try_get_path_with("service.timeout", AliasPolicy::Refuse),
Err(PathError::AliasRefused {
at: "timeout".to_string(),
alias: "shared".to_string()
})
);
}
#[test]
fn test_get_path_undefined_alias() {
use crate::yaml::Document;
use std::str::FromStr;
let doc = Document::from_str(
"service: *missing
",
)
.unwrap();
assert_eq!(
doc.try_get_path("service.timeout"),
Err(PathError::UndefinedAlias {
at: "timeout".to_string(),
alias: "missing".to_string()
})
);
}
#[test]
fn test_set_path_refuses_alias_by_default() {
use crate::yaml::Document;
use std::str::FromStr;
let doc = Document::from_str(ALIASED).unwrap();
assert_eq!(
doc.try_set_path("service.timeout", 60),
Err(PathError::AliasRefused {
at: "service".to_string(),
alias: "shared".to_string()
})
);
assert_eq!(doc.to_string(), ALIASED.to_string());
}
#[test]
fn test_set_path_follow_writes_through_to_anchor() {
use crate::yaml::Document;
use std::str::FromStr;
let doc = Document::from_str(ALIASED).unwrap();
doc.try_set_path_with("service.timeout", 60, AliasPolicy::Follow)
.unwrap();
assert_eq!(
doc.to_string(),
"shared: &shared\n timeout: 60\nservice: *shared\n".to_string()
);
}
#[test]
fn test_set_path_expand_copies_without_anchor() {
use crate::yaml::Document;
use std::str::FromStr;
let doc = Document::from_str(ALIASED).unwrap();
doc.try_set_path_with("service.timeout", 60, AliasPolicy::Expand)
.unwrap();
assert_eq!(
doc.to_string(),
"shared: &shared\n timeout: 30\nservice:\n timeout: 60\n".to_string()
);
}
#[test]
fn test_set_path_expand_leaves_other_aliases_alone() {
use crate::yaml::Document;
use std::str::FromStr;
let src = "shared: &shared\n timeout: 30\na: *shared\nb: *shared\n";
let doc = Document::from_str(src).unwrap();
doc.try_set_path_with("a.timeout", 60, AliasPolicy::Expand)
.unwrap();
assert_eq!(
doc.to_string(),
"shared: &shared\n timeout: 30\na:\n timeout: 60\nb: *shared\n".to_string()
);
}
#[test]
fn test_set_path_replacing_alias_outright_needs_no_policy() {
use crate::yaml::Document;
use std::str::FromStr;
let doc = Document::from_str(ALIASED).unwrap();
doc.try_set_path("service", 1).unwrap();
assert_eq!(
doc.to_string(),
"shared: &shared\n timeout: 30\nservice: 1\n".to_string()
);
}
#[test]
fn test_set_path_undefined_alias_is_reported() {
use crate::yaml::Document;
use std::str::FromStr;
let src = "service: *missing\n";
let doc = Document::from_str(src).unwrap();
assert_eq!(
doc.try_set_path_with("service.timeout", 1, AliasPolicy::Follow),
Err(PathError::UndefinedAlias {
at: "service".to_string(),
alias: "missing".to_string()
})
);
assert_eq!(doc.to_string(), src.to_string());
}
#[test]
fn test_remove_path_refuses_alias_by_default() {
use crate::yaml::Document;
use std::str::FromStr;
let doc = Document::from_str(ALIASED).unwrap();
assert_eq!(
doc.try_remove_path("service.timeout"),
Err(PathError::AliasRefused {
at: "timeout".to_string(),
alias: "shared".to_string()
})
);
assert_eq!(doc.to_string(), ALIASED.to_string());
}
#[test]
fn test_remove_path_follow_removes_from_anchor() {
use crate::yaml::Document;
use std::str::FromStr;
let src = "shared: &shared\n timeout: 30\n retries: 3\nservice: *shared\n";
let doc = Document::from_str(src).unwrap();
let removed = doc
.try_remove_path_with("service.timeout", AliasPolicy::Follow)
.unwrap();
assert_eq!(removed.to_string(), "30".to_string());
assert_eq!(
doc.to_string(),
"shared: &shared\n retries: 3\nservice: *shared\n".to_string()
);
}
#[test]
fn test_alias_to_sequence_resolves_for_read() {
use crate::yaml::Document;
use std::str::FromStr;
let doc = Document::from_str("other: &o\n- a\n- b\nitems: *o\n").unwrap();
assert_eq!(
doc.try_get_path("items[1]").unwrap().to_string(),
"b".to_string()
);
}
#[test]
fn test_get_path_on_mapping_resolves_document_anchors() {
use crate::yaml::Document;
use std::str::FromStr;
let doc =
Document::from_str("shared: &shared\n timeout: 30\nouter:\n svc: *shared\n").unwrap();
let outer = doc.as_mapping().unwrap().get_mapping("outer").unwrap();
assert_eq!(
outer.try_get_path("svc.timeout").unwrap().to_string(),
"30".to_string()
);
}
#[test]
fn test_recursive_alias_terminates() {
use crate::yaml::Document;
use std::str::FromStr;
let doc = Document::from_str("a: &x\n b: *x\n").unwrap();
assert!(doc.try_get_path("a.b.b.b").is_ok());
}
}