use serde_json::Value;
use crate::FaceError;
pub fn resolve<'v>(value: &'v Value, path: &str) -> Result<&'v Value, FaceError> {
resolve_inner(value, path).map_err(|_| FaceError::UnknownItemsPath {
path: path.to_string(),
})
}
pub fn resolve_owned(value: &Value, path: &str) -> Result<Value, FaceError> {
resolve(value, path).cloned()
}
fn resolve_inner<'v>(value: &'v Value, path: &str) -> Result<&'v Value, &'static str> {
let trimmed = path.strip_prefix('.').unwrap_or(path);
if trimmed.is_empty() {
return Ok(value);
}
let mut current = value;
for segment in parse_segments(trimmed)? {
current = step(current, segment)?;
}
Ok(current)
}
enum Segment<'a> {
Key(&'a str),
Index(usize),
}
fn parse_segments(body: &str) -> Result<Vec<Segment<'_>>, &'static str> {
let mut out = Vec::new();
let mut rest = body;
while let Some(after_open) = rest.strip_prefix('[') {
let close = after_open.find(']').ok_or("unterminated `[`")?;
let index_str = &after_open[..close];
if index_str.is_empty() {
return Err("empty array index");
}
let index: usize = index_str.parse().map_err(|_| "non-integer array index")?;
out.push(Segment::Index(index));
rest = &after_open[close + 1..];
}
if let Some(after_dot) = rest.strip_prefix('.') {
if after_dot.is_empty() {
return Err("trailing `.`");
}
rest = after_dot;
}
while !rest.is_empty() {
let key_end = rest.find(['.', '[']).unwrap_or(rest.len());
let key = &rest[..key_end];
if key.is_empty() {
return Err("empty segment");
}
out.push(Segment::Key(key));
rest = &rest[key_end..];
while let Some(after_open) = rest.strip_prefix('[') {
let close = after_open.find(']').ok_or("unterminated `[`")?;
let index_str = &after_open[..close];
if index_str.is_empty() {
return Err("empty array index");
}
let index: usize = index_str.parse().map_err(|_| "non-integer array index")?;
out.push(Segment::Index(index));
rest = &after_open[close + 1..];
}
if let Some(after_dot) = rest.strip_prefix('.') {
if after_dot.is_empty() {
return Err("trailing `.`");
}
rest = after_dot;
}
}
Ok(out)
}
fn step<'v>(current: &'v Value, segment: Segment<'_>) -> Result<&'v Value, &'static str> {
match segment {
Segment::Key(key) => current
.as_object()
.ok_or("not an object")?
.get(key)
.ok_or("missing object key"),
Segment::Index(idx) => current
.as_array()
.ok_or("not an array")?
.get(idx)
.ok_or("index out of range"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn err_path(e: &FaceError) -> &str {
match e {
FaceError::UnknownItemsPath { path } => path,
other => panic!("expected UnknownItemsPath, got {other:?}"),
}
}
#[test]
fn resolves_identity() {
let v = json!({"a": 1});
assert_eq!(resolve(&v, ".").unwrap(), &v);
assert_eq!(resolve(&v, "").unwrap(), &v);
}
#[test]
fn resolves_nested_dotted() {
let v = json!({"data": {"path": {"text": "src/cli.rs"}}});
let got = resolve(&v, "data.path.text").unwrap();
assert_eq!(got, &json!("src/cli.rs"));
let got2 = resolve(&v, ".data.path.text").unwrap();
assert_eq!(got2, &json!("src/cli.rs"));
}
#[test]
fn resolves_array_index() {
let v = json!({"hits": [{"score": 10}, {"score": 20}]});
let got = resolve(&v, ".hits[1].score").unwrap();
assert_eq!(got, &json!(20));
}
#[test]
fn resolves_chained_indices() {
let v = json!({"matrix": [[1, 2], [3, 4]]});
let got = resolve(&v, ".matrix[1][0]").unwrap();
assert_eq!(got, &json!(3));
}
#[test]
fn resolves_top_level_array_index() {
let v = json!([10, 20, 30]);
let got = resolve(&v, ".[1]").unwrap();
assert_eq!(got, &json!(20));
}
#[test]
fn missing_path_errors() {
let v = json!({"a": 1});
let err = resolve(&v, ".b").unwrap_err();
assert_eq!(err_path(&err), ".b");
let err = resolve(&v, ".a.b").unwrap_err();
assert_eq!(err_path(&err), ".a.b");
}
#[test]
fn out_of_range_index_errors() {
let v = json!({"a": [1, 2]});
let err = resolve(&v, ".a[5]").unwrap_err();
assert_eq!(err_path(&err), ".a[5]");
}
#[test]
fn rejects_malformed_paths() {
let v = json!({"a": 1});
assert!(resolve(&v, "..").is_err());
assert!(resolve(&v, "a.").is_err());
assert!(resolve(&v, ".a[").is_err());
assert!(resolve(&v, ".a[]").is_err());
assert!(resolve(&v, ".a[abc]").is_err());
}
#[test]
fn resolve_owned_clones() {
let v = json!({"x": [1, 2, 3]});
let got = resolve_owned(&v, ".x").unwrap();
assert_eq!(got, json!([1, 2, 3]));
}
}