use std::fmt;
use std::str::FromStr;
use crate::Value;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum PathError {
#[error("expected KEY.PATH=VALUE")]
MissingEquals,
#[error("empty path")]
EmptyPath,
#[error("empty segment in path `{path}`")]
EmptySegment {
path: String,
},
#[error("malformed index in path `{path}`")]
BadIndex {
path: String,
},
#[error("`{path}` contains an array index; merge paths take keys only")]
IndexInKeyPath {
path: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Seg {
Key(String),
Index(usize),
}
pub fn render_path(path: &[Seg]) -> String {
let mut out = String::new();
for seg in path {
match seg {
Seg::Key(k) => {
if !out.is_empty() {
out.push('.');
}
out.push_str(k);
}
Seg::Index(i) => out.push_str(&format!("[{i}]")),
}
}
out
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RefPath {
path: Vec<Seg>,
}
impl RefPath {
pub fn from_keys(keys: Vec<String>) -> Result<Self, PathError> {
if keys.is_empty() {
return Err(PathError::EmptyPath);
}
if keys.iter().any(|k| k.is_empty()) {
return Err(PathError::EmptySegment {
path: keys.join("."),
});
}
Ok(Self {
path: keys.into_iter().map(Seg::Key).collect(),
})
}
pub fn segs(&self) -> &[Seg] {
&self.path
}
pub fn into_segs(self) -> Vec<Seg> {
self.path
}
pub fn try_into_keys(self) -> Result<Vec<String>, PathError> {
if self.path.iter().any(|seg| matches!(seg, Seg::Index(_))) {
return Err(PathError::IndexInKeyPath {
path: render_path(&self.path),
});
}
Ok(self
.path
.into_iter()
.map(|seg| match seg {
Seg::Key(key) => key,
Seg::Index(_) => unreachable!("index steps were rejected above"),
})
.collect())
}
}
impl FromStr for RefPath {
type Err = PathError;
fn from_str(body: &str) -> Result<Self, Self::Err> {
if body.is_empty() {
return Err(PathError::EmptyPath);
}
let bad_index = || PathError::BadIndex {
path: body.to_string(),
};
let empty_segment = || PathError::EmptySegment {
path: body.to_string(),
};
let bytes = body.as_bytes();
let mut path = Vec::new();
let mut cursor = 0;
let (key, next) = take_key(body, cursor);
if key.is_empty() {
return Err(if bytes[cursor] == b']' {
bad_index()
} else {
empty_segment()
});
}
path.push(Seg::Key(key));
cursor = next;
while cursor < body.len() {
match bytes[cursor] {
b'.' => {
let (key, next) = take_key(body, cursor + 1);
if key.is_empty() {
return Err(empty_segment());
}
path.push(Seg::Key(key));
cursor = next;
}
b'[' => {
let digits_start = cursor + 1;
let mut end = digits_start;
while end < body.len() && bytes[end].is_ascii_digit() {
end += 1;
}
if end == digits_start || bytes.get(end) != Some(&b']') {
return Err(bad_index());
}
let index: usize = body[digits_start..end].parse().map_err(|_| bad_index())?;
path.push(Seg::Index(index));
cursor = end + 1;
}
_ => return Err(bad_index()),
}
}
Ok(Self { path })
}
}
fn take_key(body: &str, from: usize) -> (String, usize) {
let mut end = from;
while end < body.len() && !matches!(body.as_bytes()[end], b'.' | b'[' | b']') {
end += 1;
}
(body[from..end].to_string(), end)
}
impl fmt::Display for RefPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&render_path(&self.path))
}
}
pub(crate) fn render_keys(path: &[String]) -> String {
if path.is_empty() {
"<root>".to_string()
} else {
path.join(".")
}
}
pub(crate) fn lookup<'a>(root: &'a Value, path: &[Seg]) -> Option<&'a Value> {
let mut node = root;
for seg in path {
node = match (seg, node) {
(Seg::Key(k), Value::Object(map)) => map.get(k)?,
(Seg::Index(i), Value::Array(items)) => items.get(*i)?,
_ => return None,
};
}
Some(node)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Map, Number};
#[test]
fn render_path_mixes_keys_and_indices() {
assert_eq!(render_path(&[]), "");
assert_eq!(
render_path(&[Seg::Key("a".into()), Seg::Key("b".into())]),
"a.b"
);
assert_eq!(
render_path(&[Seg::Key("tags".into()), Seg::Index(0)]),
"tags[0]"
);
}
fn seg(key: &str) -> Seg {
Seg::Key(key.into())
}
#[test]
fn ref_path_parses_mixed_steps() {
let cases: &[(&str, &[Seg])] = &[
("a", &[seg("a")]),
("a.b", &[seg("a"), seg("b")]),
("a[0]", &[seg("a"), Seg::Index(0)]),
("a.b[2].c", &[seg("a"), seg("b"), Seg::Index(2), seg("c")]),
("a[0][1]", &[seg("a"), Seg::Index(0), Seg::Index(1)]),
("a:b[3]", &[seg("a:b"), Seg::Index(3)]),
];
for (body, want) in cases {
let parsed: RefPath = body.parse().unwrap();
assert_eq!(parsed.segs(), *want, "{body}");
}
}
#[test]
fn ref_path_rejects_malformed_bodies() {
let cases: &[(&str, PathError)] = &[
(".a", empty_segment(".a")),
("a..b", empty_segment("a..b")),
("a[0]..b", empty_segment("a[0]..b")),
("[0].a", empty_segment("[0].a")),
("a[]", bad_index("a[]")),
("a[x]", bad_index("a[x]")),
("a[1", bad_index("a[1")),
("a[-1]", bad_index("a[-1]")),
("a]", bad_index("a]")),
(
"a[99999999999999999999999999]",
bad_index("a[99999999999999999999999999]"),
),
];
for (body, want) in cases {
assert_eq!(&body.parse::<RefPath>().unwrap_err(), want, "{body}");
}
assert_eq!("".parse::<RefPath>().unwrap_err(), PathError::EmptyPath);
}
fn empty_segment(path: &str) -> PathError {
PathError::EmptySegment { path: path.into() }
}
fn bad_index(path: &str) -> PathError {
PathError::BadIndex { path: path.into() }
}
#[test]
fn ref_path_displays_as_rendered_witness() {
let parsed: RefPath = "a.b[2].c".parse().unwrap();
assert_eq!(parsed.to_string(), "a.b[2].c");
assert_eq!("a.b".parse::<RefPath>().unwrap().to_string(), "a.b");
}
#[test]
fn from_keys_rejects_empty_path_and_empty_segments() {
assert_eq!(
RefPath::from_keys(vec![]).unwrap_err(),
PathError::EmptyPath
);
assert_eq!(
RefPath::from_keys(vec!["".into()]).unwrap_err(),
PathError::EmptySegment { path: "".into() }
);
assert_eq!(
RefPath::from_keys(vec!["a".into(), "".into()]).unwrap_err(),
PathError::EmptySegment { path: "a.".into() }
);
assert_eq!(
RefPath::from_keys(vec!["db".into(), "plugins".into()]).unwrap(),
"db.plugins".parse().unwrap()
);
}
#[test]
fn try_into_keys_passes_all_key_paths_whole() {
let parsed: RefPath = "db.plugins".parse().unwrap();
assert_eq!(parsed.try_into_keys().unwrap(), ["db", "plugins"]);
}
#[test]
fn try_into_keys_rejects_index_steps_with_the_full_path() {
for (indexed, want) in [("a[0]", "a[0]"), ("a.b[2].c", "a.b[2].c")] {
let err = indexed
.parse::<RefPath>()
.unwrap()
.try_into_keys()
.unwrap_err();
assert_eq!(err, PathError::IndexInKeyPath { path: want.into() });
}
let err = "a[0]"
.parse::<RefPath>()
.unwrap()
.try_into_keys()
.unwrap_err();
assert_eq!(
err.to_string(),
"`a[0]` contains an array index; merge paths take keys only"
);
}
#[test]
fn equals_is_an_ordinary_key_character() {
let parsed: RefPath = "a=b".parse().unwrap();
assert_eq!(parsed.try_into_keys().unwrap(), ["a=b"]);
}
fn doc() -> Value {
let mut inner = Map::new();
inner.insert("host".into(), Value::String("h".into()));
let mut root = Map::new();
root.insert("db".into(), Value::Object(inner));
root.insert(
"tags".into(),
Value::Array(vec![Value::Number(Number::I64(1))]),
);
Value::Object(root)
}
#[test]
fn lookup_walks_objects_and_arrays() {
let doc = doc();
assert_eq!(lookup(&doc, &[]), Some(&doc));
assert_eq!(
lookup(&doc, &[Seg::Key("db".into()), Seg::Key("host".into())]),
Some(&Value::String("h".into()))
);
assert_eq!(
lookup(&doc, &[Seg::Key("tags".into()), Seg::Index(0)]),
Some(&Value::Number(Number::I64(1)))
);
}
#[test]
fn lookup_misses_are_none() {
let doc = doc();
assert_eq!(lookup(&doc, &[Seg::Key("nope".into())]), None);
assert_eq!(lookup(&doc, &[Seg::Key("db".into()), Seg::Index(0)]), None);
assert_eq!(
lookup(&doc, &[Seg::Key("tags".into()), Seg::Index(9)]),
None
);
}
}