use std::fmt;
use std::hash::{Hash, Hasher};
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathSegment {
Key(String),
Index(usize),
}
impl Hash for PathSegment {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
PathSegment::Key(s) => {
state.write_u8(0);
s.hash(state);
}
PathSegment::Index(i) => {
state.write_u8(1);
i.hash(state);
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct JsonPath {
segments: Vec<PathSegment>,
}
impl JsonPath {
pub fn new() -> Self {
Self {
segments: Vec::new(),
}
}
pub fn from_segments(segments: Vec<PathSegment>) -> Self {
Self { segments }
}
pub fn segments(&self) -> &[PathSegment] {
&self.segments
}
pub fn is_empty(&self) -> bool {
self.segments.is_empty()
}
pub fn len(&self) -> usize {
self.segments.len()
}
pub fn push(&mut self, segment: PathSegment) {
self.segments.push(segment);
}
pub fn parent(&self) -> Option<Self> {
if self.segments.len() <= 1 {
None
} else {
Some(Self {
segments: self.segments[..self.segments.len() - 1].to_vec(),
})
}
}
pub fn matches_prefix(&self, prefix: &JsonPath) -> bool {
if prefix.segments.len() > self.segments.len() {
return false;
}
self.segments
.iter()
.zip(prefix.segments.iter())
.all(|(a, b)| a == b)
}
pub fn prefix(&self, n: usize) -> Option<Self> {
if n == 0 || n > self.segments.len() {
return None;
}
Some(Self {
segments: self.segments[..n].to_vec(),
})
}
pub fn to_json_pointer(&self) -> String {
if self.segments.is_empty() {
return String::new();
}
let mut result = String::new();
for segment in &self.segments {
result.push('/');
match segment {
PathSegment::Key(key) => {
let encoded = key.replace('~', "~0").replace('/', "~1");
result.push_str(&encoded);
}
PathSegment::Index(i) => {
result.push_str(&i.to_string());
}
}
}
result
}
}
impl Default for JsonPath {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for JsonPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, segment) in self.segments.iter().enumerate() {
match segment {
PathSegment::Key(key) => {
if i > 0 {
write!(f, ".")?;
}
write!(f, "{}", key)?;
}
PathSegment::Index(idx) => {
write!(f, "[{}]", idx)?;
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum ParseError {
#[error("Empty path")]
EmptyPath,
#[error("Invalid array index at position {position}: expected digit, found '{found}'")]
InvalidArrayIndex { position: usize, found: char },
#[error("Unclosed bracket at position {position}")]
UnclosedBracket { position: usize },
#[error("Unexpected character '{0}' at position {1}")]
UnexpectedCharacter(char, usize),
}
impl FromStr for JsonPath {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.trim().is_empty() {
return Ok(Self::new());
}
let mut segments = Vec::new();
let mut chars = s.chars().peekable();
let mut pos = 0;
while let Some(ch) = chars.next() {
match ch {
'.' => {
pos += 1;
}
'[' => {
pos += 1;
let mut index_str = String::new();
while let Some(&c) = chars.peek() {
if c.is_ascii_digit() {
index_str.push(c);
chars.next();
pos += 1;
} else {
break;
}
}
if index_str.is_empty() {
return Err(ParseError::InvalidArrayIndex {
position: pos,
found: chars.next().unwrap_or(' '),
});
}
match chars.next() {
Some(']') => {
pos += 1;
}
Some(c) => {
return Err(ParseError::UnexpectedCharacter(c, pos));
}
None => {
return Err(ParseError::UnclosedBracket { position: pos });
}
}
let index: usize =
index_str
.parse()
.map_err(|_| ParseError::InvalidArrayIndex {
position: pos - index_str.len() - 1,
found: index_str.chars().next().unwrap_or(' '),
})?;
segments.push(PathSegment::Index(index));
}
']' => {
return Err(ParseError::UnexpectedCharacter(ch, pos));
}
_ => {
let mut key = String::new();
key.push(ch);
pos += 1;
while let Some(&c) = chars.peek() {
if c == '.' || c == '[' {
break;
}
key.push(c);
chars.next();
pos += 1;
}
if !key.is_empty() {
segments.push(PathSegment::Key(key));
}
}
}
}
if segments.is_empty() {
return Err(ParseError::EmptyPath);
}
Ok(Self { segments })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_path() {
let path = JsonPath::new();
assert!(path.is_empty());
assert_eq!(path.len(), 0);
assert_eq!(path.to_string(), "");
assert_eq!(path.to_json_pointer(), "");
}
#[test]
fn test_parse_root_key() {
let path: JsonPath = "name".parse().unwrap();
assert_eq!(path.segments(), &[PathSegment::Key("name".to_string())]);
assert_eq!(path.to_string(), "name");
}
#[test]
fn test_parse_nested_keys() {
let path: JsonPath = "user.profile.email".parse().unwrap();
assert_eq!(
path.segments(),
&[
PathSegment::Key("user".to_string()),
PathSegment::Key("profile".to_string()),
PathSegment::Key("email".to_string())
]
);
assert_eq!(path.to_string(), "user.profile.email");
}
#[test]
fn test_parse_array_index() {
let path: JsonPath = "items[0]".parse().unwrap();
assert_eq!(
path.segments(),
&[PathSegment::Key("items".to_string()), PathSegment::Index(0)]
);
assert_eq!(path.to_string(), "items[0]");
}
#[test]
fn test_parse_combined() {
let path: JsonPath = "users[0].email".parse().unwrap();
assert_eq!(
path.segments(),
&[
PathSegment::Key("users".to_string()),
PathSegment::Index(0),
PathSegment::Key("email".to_string())
]
);
assert_eq!(path.to_string(), "users[0].email");
}
#[test]
fn test_parse_deep_nesting() {
let path: JsonPath = "a.b.c.d.e".parse().unwrap();
assert_eq!(path.len(), 5);
assert_eq!(path.to_string(), "a.b.c.d.e");
}
#[test]
fn test_to_json_pointer_simple() {
let path: JsonPath = "name".parse().unwrap();
assert_eq!(path.to_json_pointer(), "/name");
}
#[test]
fn test_to_json_pointer_nested() {
let path: JsonPath = "user.name".parse().unwrap();
assert_eq!(path.to_json_pointer(), "/user/name");
}
#[test]
fn test_to_json_pointer_array() {
let path: JsonPath = "users[0]".parse().unwrap();
assert_eq!(path.to_json_pointer(), "/users/0");
}
#[test]
fn test_to_json_pointer_combined() {
let path: JsonPath = "users[0].email".parse().unwrap();
assert_eq!(path.to_json_pointer(), "/users/0/email");
}
#[test]
fn test_to_json_pointer_special_chars() {
let path: JsonPath = "user/name".parse().unwrap();
assert_eq!(path.to_json_pointer(), "/user~1name");
let path: JsonPath = "user~name".parse().unwrap();
assert_eq!(path.to_json_pointer(), "/user~0name");
}
#[test]
fn test_parent() {
let path: JsonPath = "user.profile.email".parse().unwrap();
let parent = path.parent().unwrap();
assert_eq!(parent.to_string(), "user.profile");
let root: JsonPath = "name".parse().unwrap();
assert!(root.parent().is_none());
}
#[test]
fn test_matches_prefix() {
let path: JsonPath = "user.profile.email".parse().unwrap();
let prefix: JsonPath = "user.profile".parse().unwrap();
assert!(path.matches_prefix(&prefix));
assert!(!prefix.matches_prefix(&path));
}
#[test]
fn test_push_segment() {
let mut path = JsonPath::new();
path.push(PathSegment::Key("users".to_string()));
path.push(PathSegment::Index(0));
path.push(PathSegment::Key("email".to_string()));
assert_eq!(path.to_string(), "users[0].email");
}
#[test]
fn test_round_trip() {
let original = "users[0].profile.email";
let path: JsonPath = original.parse().unwrap();
assert_eq!(path.to_string(), original);
}
#[test]
fn test_parse_empty_string() {
let path: Result<JsonPath, _> = "".parse();
assert!(path.is_ok());
assert!(path.unwrap().is_empty());
}
#[test]
fn test_parse_invalid_array_no_digits() {
let path: Result<JsonPath, _> = "items[]".parse();
assert!(path.is_err());
}
#[test]
fn test_parse_unclosed_bracket() {
let path: Result<JsonPath, _> = "items[0".parse();
assert!(path.is_err());
}
#[test]
fn test_equality() {
let path1: JsonPath = "users[0].email".parse().unwrap();
let path2: JsonPath = "users[0].email".parse().unwrap();
assert_eq!(path1, path2);
}
#[test]
fn test_hash() {
use std::collections::HashSet;
let path1: JsonPath = "users[0].email".parse().unwrap();
let path2: JsonPath = "users[0].email".parse().unwrap();
let path3: JsonPath = "users[1].email".parse().unwrap();
let mut set = HashSet::new();
set.insert(path1.clone());
set.insert(path2);
set.insert(path3.clone());
assert_eq!(set.len(), 2); assert!(set.contains(&path1));
assert!(set.contains(&path3));
}
}