use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Segment {
Key(String),
Index(usize),
}
pub fn parse_path(s: &str) -> Result<Vec<Segment>, String> {
if s.is_empty() {
return Err("empty path".to_string());
}
let bytes = s.as_bytes();
let mut segments = Vec::new();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'.' => {
i += 1;
let start = i;
if i >= bytes.len() {
return Err(format!("expected identifier at position {i}"));
}
if !is_ident_start(bytes[i]) {
return Err(format!(
"expected identifier start at position {i}, found {:?}",
peek_char(s, i)
));
}
i += 1;
while i < bytes.len() && is_ident_continue(bytes[i]) {
i += 1;
}
let ident = &s[start..i];
segments.push(Segment::Key(ident.to_string()));
}
b'[' => {
i += 1;
if i >= bytes.len() {
return Err(format!("expected digits or quoted key at position {i}"));
}
if bytes[i] == b'"' {
let (key, consumed) = parse_quoted_key(&bytes[i..], i)?;
i += consumed;
if i >= bytes.len() || bytes[i] != b']' {
return Err(format!("expected ']' at position {i}"));
}
i += 1; segments.push(Segment::Key(key));
} else {
let start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
if start == i {
return Err(format!("expected digits or quoted key at position {start}"));
}
let digits = &s[start..i];
if i >= bytes.len() || bytes[i] != b']' {
return Err(format!("expected ']' at position {i}"));
}
let index: usize = digits
.parse()
.map_err(|e| format!("invalid index {digits:?}: {e}"))?;
i += 1; segments.push(Segment::Index(index));
}
}
_ => {
return Err(format!(
"expected '.' or '[' at position {i}, found {:?}",
peek_char(s, i)
));
}
}
}
if segments.is_empty() {
return Err("empty path".to_string());
}
Ok(segments)
}
pub fn extract<'a>(value: &'a Value, path: &[Segment]) -> Option<&'a Value> {
let mut cur = value;
for seg in path {
match seg {
Segment::Key(k) => cur = cur.as_object()?.get(k)?,
Segment::Index(i) => cur = cur.as_array()?.get(*i)?,
}
}
Some(cur)
}
fn is_ident_start(b: u8) -> bool {
b.is_ascii_alphabetic() || b == b'_'
}
fn is_ident_continue(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_'
}
fn peek_char(s: &str, byte_offset: usize) -> char {
s[byte_offset..].chars().next().unwrap_or('?')
}
fn parse_quoted_key(bytes: &[u8], offset: usize) -> Result<(String, usize), String> {
debug_assert!(!bytes.is_empty() && bytes[0] == b'"');
let mut out: Vec<u8> = Vec::new();
let mut j = 1; while j < bytes.len() {
match bytes[j] {
b'"' => {
let key = String::from_utf8(out)
.map_err(|e| format!("invalid utf-8 in quoted key at {offset}: {e}"))?;
return Ok((key, j + 1));
}
b'\\' => {
if j + 1 >= bytes.len() {
return Err(format!("unterminated escape at position {}", offset + j));
}
match bytes[j + 1] {
b'"' => out.push(b'"'),
b'\\' => out.push(b'\\'),
other => {
return Err(format!(
"unknown escape '\\{}' at position {}",
other as char,
offset + j
));
}
}
j += 2;
}
b => {
out.push(b);
j += 1;
}
}
}
Err(format!(
"unterminated quoted key starting at position {offset}"
))
}