use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum JsonPathResolveError {
JsonParse(String),
PathSyntax(String),
PathNotFound(String),
}
impl JsonPathResolveError {
pub fn user_reason(&self) -> String {
match self {
Self::JsonParse(e) => format!("invalid_json: {}", e),
Self::PathSyntax(msg) => format!("invalid_path_syntax: {}", msg),
Self::PathNotFound(msg) => format!("path_not_found: {}", msg),
}
}
}
pub fn resolve_json_path_value(json_str: &str, path: &str) -> Result<Value, JsonPathResolveError> {
let path = path.trim();
let root: Value = serde_json::from_str(json_str.trim())
.map_err(|e| JsonPathResolveError::JsonParse(e.to_string()))?;
if path.is_empty() {
return Ok(root.clone());
}
let resolved_ref = if path.starts_with('/') {
resolve_json_pointer_ref(&root, path)?
} else {
resolve_legacy_dot_path_ref(&root, path)?
};
Ok(resolved_ref.clone())
}
fn resolve_json_pointer_ref<'a>(
root: &'a Value,
pointer: &str,
) -> Result<&'a Value, JsonPathResolveError> {
let pointer = pointer.trim();
if !pointer.starts_with('/') {
return Err(JsonPathResolveError::PathSyntax(
"internal: JSON Pointer must start with '/'".into(),
));
}
let mut cur = root;
for raw_token in pointer.split('/').skip(1) {
let token = decode_json_pointer_token(raw_token)?;
cur = navigate_pointer_token(cur, &token)?;
}
Ok(cur)
}
fn decode_json_pointer_token(raw: &str) -> Result<String, JsonPathResolveError> {
let mut out = String::with_capacity(raw.len());
let mut it = raw.chars().peekable();
while let Some(c) = it.next() {
if c != '~' {
out.push(c);
continue;
}
match it.next() {
Some('0') => out.push('~'),
Some('1') => out.push('/'),
Some(other) => {
return Err(JsonPathResolveError::PathSyntax(format!(
"invalid JSON Pointer escape ~{}",
other
)));
}
None => {
return Err(JsonPathResolveError::PathSyntax(
"truncated JSON Pointer escape".into(),
));
}
}
}
Ok(out)
}
fn navigate_pointer_token<'a>(
current: &'a Value,
token: &str,
) -> Result<&'a Value, JsonPathResolveError> {
if let Some(arr) = current.as_array()
&& let Ok(i) = token.parse::<usize>()
{
return arr.get(i).ok_or_else(|| {
JsonPathResolveError::PathNotFound(format!(
"array index {} out of bounds (len {})",
i,
arr.len()
))
});
}
current
.get(token)
.ok_or_else(|| JsonPathResolveError::PathNotFound(format!("missing key {:?}", token)))
}
fn resolve_legacy_dot_path_ref<'a>(
root: &'a Value,
path: &str,
) -> Result<&'a Value, JsonPathResolveError> {
let segments: Vec<&str> = path
.split('.')
.map(str::trim)
.filter(|s| !s.is_empty())
.collect();
if segments.is_empty() {
return Err(JsonPathResolveError::PathSyntax(
"legacy path has no segments".into(),
));
}
let mut cur = root;
for (i, seg) in segments.iter().enumerate() {
cur = apply_legacy_segment(cur, seg, i == 0)?;
}
Ok(cur)
}
fn strip_legacy_root_dollar(seg: &str, is_first: bool) -> &str {
if is_first && seg.starts_with('$') {
&seg[1..]
} else {
seg
}
}
fn split_legacy_field_and_brackets(s: &str) -> (&str, &str) {
match s.find('[') {
Some(pos) => (s[..pos].trim(), &s[pos..]),
None => (s.trim(), ""),
}
}
fn apply_legacy_field<'a>(
cur: &'a Value,
field: &str,
) -> Result<&'a Value, JsonPathResolveError> {
if field.is_empty() {
return Ok(cur);
}
cur.get(field)
.ok_or_else(|| JsonPathResolveError::PathNotFound(format!("missing key {:?}", field)))
}
fn apply_legacy_indices<'a>(
mut v: &'a Value,
indices: &[usize],
) -> Result<&'a Value, JsonPathResolveError> {
for idx in indices {
v = v.get(*idx).ok_or_else(|| {
JsonPathResolveError::PathNotFound(format!("array index {} out of bounds", idx))
})?;
}
Ok(v)
}
fn apply_legacy_segment<'a>(
cur: &'a Value,
seg: &str,
is_first: bool,
) -> Result<&'a Value, JsonPathResolveError> {
let s = strip_legacy_root_dollar(seg, is_first);
let (field, bracket_tail) = split_legacy_field_and_brackets(s);
let v = apply_legacy_field(cur, field)?;
let indices = parse_bracket_suffix(bracket_tail)?;
apply_legacy_indices(v, &indices)
}
fn parse_bracket_suffix(bracket_tail: &str) -> Result<Vec<usize>, JsonPathResolveError> {
if bracket_tail.is_empty() {
return Ok(Vec::new());
}
let mut indices = Vec::new();
let mut rest = bracket_tail;
while !rest.is_empty() {
let inner_rest = rest.strip_prefix('[').ok_or_else(|| {
JsonPathResolveError::PathSyntax(format!(
"expected '[' at start of bracket suffix, got {:?}",
rest.chars().take(8).collect::<String>()
))
})?;
let close = inner_rest
.find(']')
.ok_or_else(|| JsonPathResolveError::PathSyntax("unclosed '[' in path".into()))?;
let num = inner_rest[..close].trim();
if num.is_empty() {
return Err(JsonPathResolveError::PathSyntax(
"empty array index in []".into(),
));
}
let idx = num.parse::<usize>().map_err(|_| {
JsonPathResolveError::PathSyntax(format!(
"invalid array index {:?} (non-negative integer required)",
num
))
})?;
indices.push(idx);
rest = &inner_rest[close + 1..];
}
Ok(indices)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn legacy_nested_and_multi_bracket_segment() {
let v = json!({"data":{"items":[[1,2],[3,4]]}});
let s = v.to_string();
let r = resolve_json_path_value(&s, "$.data.items[0][1]").unwrap();
assert_eq!(r, json!(2));
}
#[test]
fn legacy_invalid_index_is_syntax_error() {
let v = json!({"items":[1]});
let s = v.to_string();
let e = resolve_json_path_value(&s, "$.items[abc]").unwrap_err();
assert!(matches!(e, JsonPathResolveError::PathSyntax(_)));
}
#[test]
fn legacy_trailing_junk_after_bracket() {
let v = json!({"items":[1]});
let s = v.to_string();
let e = resolve_json_path_value(&s, "$.items[0]oops").unwrap_err();
assert!(matches!(e, JsonPathResolveError::PathSyntax(_)));
}
#[test]
fn pointer_empty_string_key_and_slash_escape() {
let v = json!({"": 1, "a/b": 2});
let s = v.to_string();
assert_eq!(resolve_json_path_value(&s, "/").unwrap(), json!(1));
assert_eq!(resolve_json_path_value(&s, "/a~1b").unwrap(), json!(2));
}
#[test]
fn empty_path_means_whole_document() {
let v = json!({"x": 1});
let s = v.to_string();
assert_eq!(resolve_json_path_value(&s, "").unwrap(), v);
}
#[test]
fn pointer_array_index() {
let v = json!(["x", "y"]);
let s = v.to_string();
assert_eq!(resolve_json_path_value(&s, "/1").unwrap(), json!("y"));
}
#[test]
fn json_parse_error_variant() {
let e = resolve_json_path_value("not json", "$.a").unwrap_err();
assert!(matches!(e, JsonPathResolveError::JsonParse(_)));
}
}