use super::{PathError, PathSegment};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum PathParseError {
UnclosedIndex,
InvalidIndex(String),
TrailingBackslash,
EmptySegment,
MissingSeparator,
}
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}]")
}
PathParseError::TrailingBackslash => {
write!(f, "path ends with an unescaped '\\'")
}
PathParseError::EmptySegment => write!(f, "empty segment in path"),
PathParseError::MissingSeparator => {
write!(f, "expected '.' or '[' after ']' in path")
}
}
}
}
impl std::error::Error for PathParseError {}
pub fn try_parse_path(path: &str) -> Result<Vec<PathSegment>, PathParseError> {
#[derive(PartialEq)]
enum State {
Start,
AfterDot,
InSegment,
AfterIndex,
}
let mut segments = Vec::new();
let mut current = String::new();
let mut current_has_escape = false;
let mut state = State::Start;
let mut chars = path.chars();
while let Some(ch) = chars.next() {
match (ch, &state) {
('.', State::Start | State::AfterDot) | ('[', State::AfterDot) => {
return Err(PathParseError::EmptySegment);
}
('.', State::InSegment) => {
segments.push(dotted_segment(¤t, current_has_escape));
current.clear();
current_has_escape = false;
state = State::AfterDot;
}
('.', State::AfterIndex) => state = State::AfterDot,
('[', _) => {
if state == State::InSegment {
segments.push(PathSegment::Key(std::mem::take(&mut current)));
current_has_escape = false;
}
let mut index_str = String::new();
loop {
match chars.next() {
Some(']') => break,
Some(c) => index_str.push(c),
None => return Err(PathParseError::UnclosedIndex),
}
}
let index = parse_index(&index_str)
.ok_or_else(|| PathParseError::InvalidIndex(index_str.clone()))?;
segments.push(PathSegment::Index(index));
state = State::AfterIndex;
}
(_, State::AfterIndex) => return Err(PathParseError::MissingSeparator),
('\\', _) => {
current.push(chars.next().ok_or(PathParseError::TrailingBackslash)?);
current_has_escape = true;
state = State::InSegment;
}
_ => {
current.push(ch);
state = State::InSegment;
}
}
}
match state {
State::AfterDot => return Err(PathParseError::EmptySegment),
State::InSegment => segments.push(dotted_segment(¤t, current_has_escape)),
State::Start | State::AfterIndex => {}
}
Ok(segments)
}
fn parse_index(text: &str) -> Option<usize> {
if text.is_empty() || !text.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
text.parse().ok()
}
fn dotted_segment(text: &str, has_escape: bool) -> PathSegment {
match parse_index(text) {
Some(index) if !has_escape && (text == "0" || !text.starts_with('0')) => {
PathSegment::Index(index)
}
_ => PathSegment::Key(text.to_string()),
}
}
#[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()
}
pub(super) 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)
}
}
pub(super) fn segment_display(segment: &PathSegment) -> String {
match segment {
PathSegment::Key(key) => key.clone(),
PathSegment::Index(index) => format!("[{index}]"),
}
}
#[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]
fn test_try_parse_path_trailing_backslash() {
assert_eq!(
try_parse_path("a\\"),
Err(PathParseError::TrailingBackslash)
);
assert_eq!(
try_parse_path("a.\\"),
Err(PathParseError::TrailingBackslash)
);
}
#[test]
fn test_try_parse_path_empty_segment() {
for path in [".", "..", ".a", "a.", "a..b", "a.[0]", "a[0].", "a[0]..b"] {
assert_eq!(
try_parse_path(path),
Err(PathParseError::EmptySegment),
"{path:?}"
);
}
}
#[test]
fn test_try_parse_path_missing_separator_after_index() {
assert_eq!(
try_parse_path("a[0]b"),
Err(PathParseError::MissingSeparator)
);
assert_eq!(
try_parse_path("a[0]\\.b"),
Err(PathParseError::MissingSeparator)
);
}
#[test]
fn test_try_parse_path_index_must_be_digits() {
assert_eq!(
try_parse_path("a[+1]"),
Err(PathParseError::InvalidIndex("+1".to_string()))
);
assert_eq!(
try_parse_path("a.+1").unwrap(),
vec![
PathSegment::Key("a".to_string()),
PathSegment::Key("+1".to_string())
]
);
}
#[test]
fn test_try_parse_path_index_chains() {
assert_eq!(
try_parse_path("[0][1].a").unwrap(),
vec![
PathSegment::Index(0),
PathSegment::Index(1),
PathSegment::Key("a".to_string())
]
);
assert_eq!(
try_parse_path("a[0].1").unwrap(),
vec![
PathSegment::Key("a".to_string()),
PathSegment::Index(0),
PathSegment::Index(1)
]
);
}
#[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())
]
);
assert_eq!(
try_parse_path("a.\\0").unwrap(),
vec![
PathSegment::Key("a".to_string()),
PathSegment::Key("0".to_string())
]
);
assert_eq!(
try_parse_path("\\1.b").unwrap(),
vec![
PathSegment::Key("1".to_string()),
PathSegment::Key("b".to_string())
]
);
}
}