#[derive(Debug, Clone)]
pub struct ParsedPath<'a> {
pub raw: &'a str,
pub segments: Vec<PathSegment<'a>>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct List {
pub elements_nullable: bool,
}
#[derive(Debug, Clone)]
pub struct PathSegment<'a> {
pub field: &'a str,
pub alias: Option<&'a str>,
pub is_nullable: bool,
pub list: Option<List>,
}
impl<'a> PathSegment<'a> {
pub fn json_key(&self) -> &str {
self.alias.unwrap_or(self.field)
}
pub fn is_list(&self) -> bool {
self.list.is_some()
}
}
impl<'a> ParsedPath<'a> {
pub fn parse(path: &'a str) -> Result<Self, PathParseError<'a>> {
if path.is_empty() {
return Err(PathParseError::Empty);
}
let raw_segments: Vec<&str> = path.split('.').collect();
let mut segments = Vec::with_capacity(raw_segments.len());
for segment in raw_segments {
if segment.is_empty() {
return Err(PathParseError::EmptySegment { path });
}
let suffix_start = segment.find(['?', '[']).unwrap_or(segment.len());
let (field_part, suffix) = segment.split_at(suffix_start);
let (is_nullable, list) = match suffix {
"" => (false, None),
"?" => (true, None),
"[]" => (
false,
Some(List {
elements_nullable: false,
}),
),
"?[]" => (
true,
Some(List {
elements_nullable: false,
}),
),
"[]?" => (
false,
Some(List {
elements_nullable: true,
}),
),
"?[]?" => (
true,
Some(List {
elements_nullable: true,
}),
),
_ => return Err(PathParseError::InvalidSuffix { path, suffix }),
};
let (field, alias) = if let Some(colon_pos) = field_part.find(':') {
let alias = &field_part[..colon_pos];
let field = &field_part[colon_pos + 1..];
(field, Some(alias))
} else {
(field_part, None)
};
if field.is_empty() {
return Err(PathParseError::EmptySegment { path });
}
segments.push(PathSegment {
field,
alias,
is_nullable,
list,
});
}
Ok(ParsedPath {
raw: path,
segments,
})
}
}
#[derive(Debug, Clone)]
pub enum PathParseError<'a> {
Empty,
EmptySegment { path: &'a str },
InvalidSuffix { path: &'a str, suffix: &'a str },
}
impl<'a> std::fmt::Display for PathParseError<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PathParseError::Empty => write!(f, "Field path cannot be empty"),
PathParseError::EmptySegment { path } => {
write!(f, "Empty segment in path '{}'", path)
}
PathParseError::InvalidSuffix { path, suffix } => {
write!(
f,
"Invalid suffix '{}' in path '{}'. Valid suffixes: ?, [], ?[], []?, ?[]?",
suffix, path
)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_path() {
let path = ParsedPath::parse("object.address").unwrap();
assert_eq!(path.segments.len(), 2);
assert_eq!(path.segments[0].field, "object");
assert!(path.segments[0].alias.is_none());
assert!(!path.segments[0].is_nullable);
assert!(!path.segments[0].is_list());
assert_eq!(path.segments[1].field, "address");
assert!(!path.segments[1].is_list());
}
#[test]
fn test_parse_nested_path() {
let path = ParsedPath::parse("data.nodes.name").unwrap();
assert_eq!(path.segments.len(), 3);
assert_eq!(path.segments[0].field, "data");
assert!(!path.segments[0].is_list());
assert_eq!(path.segments[1].field, "nodes");
assert!(!path.segments[1].is_list());
assert_eq!(path.segments[2].field, "name");
assert!(!path.segments[2].is_list());
}
#[test]
fn test_parse_single_field() {
let path = ParsedPath::parse("chainIdentifier").unwrap();
assert_eq!(path.segments.len(), 1);
assert_eq!(path.segments[0].field, "chainIdentifier");
assert!(!path.segments[0].is_list());
}
#[test]
fn test_parse_with_alias() {
let path = ParsedPath::parse("epoch.firstCheckpoint:checkpoints.nodes[]").unwrap();
assert_eq!(path.segments.len(), 3);
assert_eq!(path.segments[0].field, "epoch");
assert!(path.segments[0].alias.is_none());
assert!(!path.segments[0].is_list());
assert_eq!(path.segments[1].field, "checkpoints");
assert_eq!(path.segments[1].alias, Some("firstCheckpoint"));
assert!(!path.segments[1].is_list());
assert_eq!(path.segments[2].field, "nodes");
assert!(path.segments[2].is_list());
}
#[test]
fn test_parse_array_with_alias() {
let path = ParsedPath::parse("myObjects:objects[]").unwrap();
assert_eq!(path.segments.len(), 1);
assert_eq!(path.segments[0].field, "objects");
assert_eq!(path.segments[0].alias, Some("myObjects"));
assert!(path.segments[0].is_list());
}
#[test]
fn test_json_key() {
let path = ParsedPath::parse("alias:field.normal").unwrap();
assert_eq!(path.segments[0].json_key(), "alias");
assert_eq!(path.segments[1].json_key(), "normal");
}
#[test]
fn test_parse_empty_error() {
let err = ParsedPath::parse("").unwrap_err();
assert!(matches!(err, PathParseError::Empty));
}
#[test]
fn test_parse_empty_segment_error() {
let err = ParsedPath::parse("foo..bar").unwrap_err();
assert!(matches!(err, PathParseError::EmptySegment { .. }));
let err = ParsedPath::parse(".foo").unwrap_err();
assert!(matches!(err, PathParseError::EmptySegment { .. }));
}
#[test]
fn test_parse_array_syntax() {
let path = ParsedPath::parse("items[].name").unwrap();
assert_eq!(path.segments.len(), 2);
assert_eq!(path.segments[0].field, "items");
assert!(path.segments[0].is_list());
assert_eq!(path.segments[1].field, "name");
assert!(!path.segments[1].is_list());
}
#[test]
fn test_parse_nested_arrays() {
let path = ParsedPath::parse("groups[].members[].name").unwrap();
assert_eq!(path.segments.len(), 3);
assert_eq!(path.segments[0].field, "groups");
assert!(path.segments[0].is_list());
assert_eq!(path.segments[1].field, "members");
assert!(path.segments[1].is_list());
assert_eq!(path.segments[2].field, "name");
assert!(!path.segments[2].is_list());
}
#[test]
fn test_parse_trailing_array() {
let path = ParsedPath::parse("items[].tags[]").unwrap();
assert_eq!(path.segments.len(), 2);
assert_eq!(path.segments[0].field, "items");
assert!(path.segments[0].is_list());
assert_eq!(path.segments[1].field, "tags");
assert!(path.segments[1].is_list());
}
#[test]
fn test_parse_empty_array_field_error() {
let err = ParsedPath::parse("[].name").unwrap_err();
assert!(matches!(err, PathParseError::EmptySegment { .. }));
}
#[test]
fn test_parse_nullable_field() {
let path = ParsedPath::parse("object?.address?").unwrap();
assert_eq!(path.segments.len(), 2);
assert!(path.segments[0].is_nullable);
assert!(!path.segments[0].is_list());
assert!(path.segments[1].is_nullable);
assert!(!path.segments[1].is_list());
}
#[test]
fn test_parse_nullable_array() {
let path = ParsedPath::parse("items?[].name").unwrap();
assert!(path.segments[0].is_nullable);
assert!(path.segments[0].is_list());
assert!(!path.segments[0].list.as_ref().unwrap().elements_nullable);
}
#[test]
fn test_parse_elements_nullable() {
let path = ParsedPath::parse("items[]?.name").unwrap();
assert!(!path.segments[0].is_nullable);
assert!(path.segments[0].is_list());
assert!(path.segments[0].list.as_ref().unwrap().elements_nullable);
}
#[test]
fn test_parse_both_nullable() {
let path = ParsedPath::parse("items?[]?.name").unwrap();
assert!(path.segments[0].is_nullable);
assert!(path.segments[0].is_list());
assert!(path.segments[0].list.as_ref().unwrap().elements_nullable);
}
#[test]
fn test_parse_nullable_with_alias() {
let path = ParsedPath::parse("myAddr:address?").unwrap();
assert_eq!(path.segments[0].field, "address");
assert_eq!(path.segments[0].alias, Some("myAddr"));
assert!(path.segments[0].is_nullable);
}
#[test]
fn test_parse_nullable_array_with_alias() {
let path = ParsedPath::parse("myItems:items?[]?.name").unwrap();
assert_eq!(path.segments[0].field, "items");
assert_eq!(path.segments[0].alias, Some("myItems"));
assert!(path.segments[0].is_nullable);
assert!(path.segments[0].list.as_ref().unwrap().elements_nullable);
}
#[test]
fn test_parse_mixed_nullable_path() {
let path = ParsedPath::parse("epoch?.checkpoints?.nodes?[].digest").unwrap();
assert_eq!(path.segments.len(), 4);
assert!(path.segments[0].is_nullable);
assert!(!path.segments[0].is_list());
assert!(path.segments[1].is_nullable);
assert!(!path.segments[1].is_list());
assert!(path.segments[2].is_nullable);
assert!(path.segments[2].is_list());
assert!(!path.segments[3].is_nullable);
assert!(!path.segments[3].is_list());
}
#[test]
fn test_parse_invalid_suffix_error() {
let err = ParsedPath::parse("foo[?].name").unwrap_err();
assert!(matches!(err, PathParseError::InvalidSuffix { .. }));
}
}