mod alias;
mod parse;
mod read;
mod write;
#[allow(deprecated)]
pub use parse::parse_path;
pub use parse::{try_parse_path, PathParseError};
use crate::yaml::Mapping;
use parse::path_segments_required;
use read::navigate_path;
use write::{remove_path_impl, set_path_on_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),
}
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,
},
InvalidMergeOperand {
at: String,
operand: 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:?}"
),
PathError::InvalidMergeOperand { at, operand } => write!(
f,
"looking up path segment {at:?}: merge key operand {operand:?} is not a mapping"
),
}
}
}
impl std::error::Error for PathError {}
impl From<PathParseError> for PathError {
fn from(e: PathParseError) -> Self {
PathError::Parse(e)
}
}
fn root_node(doc: &crate::yaml::Document) -> Result<crate::as_yaml::YamlNode, PathError> {
doc.as_mapping()
.map(crate::as_yaml::YamlNode::Mapping)
.or_else(|| doc.as_sequence().map(crate::as_yaml::YamlNode::Sequence))
.or_else(|| doc.as_scalar().map(crate::as_yaml::YamlNode::Scalar))
.ok_or(PathError::NoRoot)
}
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)?;
navigate_path(root_node(self)?, &segments, policy)
}
fn try_set_path_with(
&self,
path: &str,
value: impl crate::AsYaml,
policy: AliasPolicy,
) -> Result<(), PathError> {
let segments = path_segments_required(path)?;
self.try_edit_root_mapping(|mapping| set_path_on_mapping(mapping, &segments, value, policy))
.ok_or(PathError::NoRoot)?
}
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(root_node(self)?, None, &segments, 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()),
None,
&segments,
policy,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_path_error_display() {
for (error, text) in [
(
PathError::Parse(PathParseError::UnclosedIndex),
"path parse error: unclosed '[' in path",
),
(
PathError::Parse(PathParseError::InvalidIndex("x".to_string())),
"path parse error: invalid path index [x]",
),
(
PathError::Parse(PathParseError::TrailingBackslash),
"path parse error: path ends with an unescaped '\\'",
),
(
PathError::Parse(PathParseError::EmptySegment),
"path parse error: empty segment in path",
),
(
PathError::Parse(PathParseError::MissingSeparator),
"path parse error: expected '.' or '[' after ']' in path",
),
(PathError::EmptyPath, "path is empty (no segments)"),
(PathError::NoRoot, "document has no root to descend into"),
(
PathError::TypeMismatch { at: "a".into() },
"path segment \"a\" is not a container",
),
(
PathError::NotFound { at: "a".into() },
"path segment \"a\" not found",
),
(
PathError::AliasRefused {
at: "a".into(),
alias: "x".into(),
},
"path segment \"a\" is an alias to \"x\"; refusing to descend through it",
),
(
PathError::IndexTooFar {
at: "[300]".into(),
len: 2,
index: 300,
},
"path segment \"[300]\" would grow a sequence of 2 by 299 null entries to reach index 300",
),
(
PathError::UndefinedAlias {
at: "a".into(),
alias: "x".into(),
},
"path segment \"a\" is an alias to undefined anchor \"x\"",
),
(
PathError::InvalidMergeOperand {
at: "a".into(),
operand: "1".into(),
},
"looking up path segment \"a\": merge key operand \"1\" is not a mapping",
),
] {
assert_eq!(error.to_string(), text);
}
}
}