use crate::builder::MappingBuilder;
use crate::yaml::Mapping;
pub trait YamlPath {
fn try_get_path(&self, path: &str) -> Result<crate::as_yaml::YamlNode, PathError>;
fn try_set_path(&self, path: &str, value: impl crate::AsYaml) -> Result<(), PathError>;
fn try_remove_path(&self, path: &str) -> 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 {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathError {
Parse(PathParseError),
EmptyPath,
NoRoot,
TypeMismatch {
at: String,
},
NotFound {
at: String,
},
}
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"),
}
}
}
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 navigate_path(
mut current: crate::as_yaml::YamlNode,
segments: &[PathSegment],
) -> Result<crate::as_yaml::YamlNode, PathError> {
for segment in segments {
current = descend_one(current, segment)?;
}
Ok(current)
}
fn descend_one(
current: crate::as_yaml::YamlNode,
segment: &PathSegment,
) -> Result<crate::as_yaml::YamlNode, PathError> {
match segment {
PathSegment::Key(key) => {
let mapping = current
.as_mapping()
.ok_or_else(|| PathError::TypeMismatch {
at: segment_display(segment),
})?;
mapping.get(key).ok_or_else(|| PathError::NotFound {
at: segment_display(segment),
})
}
PathSegment::Index(index) => {
if let Some(seq) = current.as_sequence() {
seq.get(*index).ok_or_else(|| PathError::NotFound {
at: segment_display(segment),
})
} else if let Some(map) = current.as_mapping() {
map.get(index.to_string().as_str())
.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(&self, path: &str) -> 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)
}
fn try_set_path(&self, path: &str, value: impl crate::AsYaml) -> Result<(), PathError> {
let segments = path_segments_required(path)?;
let mapping = self.as_mapping().ok_or(PathError::NoRoot)?;
set_path_on_mapping(&mapping, &segments, value)
}
fn try_remove_path(&self, path: &str) -> 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)
}
}
fn remove_path_impl(
root: crate::as_yaml::YamlNode,
segments: &[PathSegment],
) -> Result<crate::as_yaml::YamlNode, PathError> {
debug_assert!(!segments.is_empty(), "caller must reject empty paths");
if segments.len() == 1 {
let seg = &segments[0];
let key = match seg {
PathSegment::Key(key) => key.clone(),
PathSegment::Index(index) => {
if root.as_mapping().is_none() {
return Err(PathError::TypeMismatch {
at: segment_display(seg),
});
}
index.to_string()
}
};
let mapping = root.as_mapping().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])?;
remove_path_impl(nested, &segments[1..])
}
impl YamlPath for Mapping {
fn try_get_path(&self, path: &str) -> Result<crate::as_yaml::YamlNode, PathError> {
let segments = path_segments_required(path)?;
navigate_path(crate::as_yaml::YamlNode::Mapping(self.clone()), &segments)
}
fn try_set_path(&self, path: &str, value: impl crate::AsYaml) -> Result<(), PathError> {
let segments = path_segments_required(path)?;
set_path_on_mapping(self, &segments, value)
}
fn try_remove_path(&self, path: &str) -> Result<crate::as_yaml::YamlNode, PathError> {
let segments = path_segments_required(path)?;
remove_path_impl(crate::as_yaml::YamlNode::Mapping(self.clone()), &segments)
}
}
fn set_path_on_mapping<V: crate::AsYaml>(
mapping: &Mapping,
segments: &[PathSegment],
value: V,
) -> 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(());
}
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]),
});
}
}
}
if next_wants_sequence {
if let Some(nested) = mapping.get_sequence(first_key) {
return set_path_on_sequence(&nested, &segments[1..], value);
}
if mapping.is_flow_style() {
let flow_empty = crate::builder::SequenceBuilder::new()
.build_document()
.as_sequence()
.expect("SequenceBuilder always produces a sequence");
mapping.set(first_key, flow_empty);
} else {
mapping.set(first_key, crate::yaml::Sequence::new());
}
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);
}
if let Some(nested) = mapping.get_mapping(first_key) {
return set_path_on_mapping(&nested, &segments[1..], value);
}
if mapping.is_flow_style() {
let flow_empty = MappingBuilder::new()
.build_document()
.as_mapping()
.expect("MappingBuilder always produces a mapping");
mapping.set(first_key, flow_empty);
} else {
mapping.set(first_key, Mapping::new());
}
let nested = mapping
.get_mapping(first_key)
.expect("we just inserted this key as a mapping");
set_path_on_mapping(&nested, &segments[1..], value)
}
fn set_path_on_sequence<V: crate::AsYaml>(
sequence: &crate::yaml::Sequence,
segments: &[PathSegment],
value: V,
) -> 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]),
});
}
};
while sequence.len() <= index {
sequence.push(crate::scalar::ScalarValue::null());
}
if segments.len() == 1 {
sequence.set(index, value);
return Ok(());
}
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(_));
if next_wants_sequence {
if let Some(nested) = sequence.get(index).and_then(|n| n.as_sequence().cloned()) {
return set_path_on_sequence(&nested, &segments[1..], value);
}
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);
}
if let Some(nested) = sequence.get(index).and_then(|n| n.as_mapping().cloned()) {
return set_path_on_mapping(&nested, &segments[1..], value);
}
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)
}
#[cfg(test)]
mod tests {
use super::*;
#[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_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"
);
}
}