//! subset of microcosm.blue/RecordPath use serde_json::Value; #[derive(Debug, Clone, PartialEq)] pub(crate) enum Modifier { /// `[]` Elements, /// `[nsid]` UnionElements(String), /// `{nsid}` Union(String), } #[derive(Debug, Clone, PartialEq)] pub(crate) struct Segment { field: String, modifier: Option, } #[derive(Debug, Clone, PartialEq)] pub(crate) struct RecordPath(Vec); impl RecordPath { pub(crate) fn parse(path: &str) -> Result { parse_record_path(path).map(Self) } } pub(crate) fn parse_record_path(path: &str) -> Result, String> { let chars: Vec = path.chars().collect(); let mut segments = Vec::new(); let mut field = String::new(); let mut i = 0; let mut saw_any = false; let unescape = |chars: &[char], i: &mut usize| -> Result { *i += 1; match chars.get(*i) { Some(&c @ ('.' | '[' | ']' | '{' | '}' | '!')) => Ok(c), Some(c) => Err(format!("invalid escape !{c}")), None => Err("trailing ! escape".to_owned()), } }; while i < chars.len() { let c = chars[i]; match c { '.' => { if !saw_any { return Err("empty path segment".to_owned()); } segments.push(Segment { field: std::mem::take(&mut field), modifier: None, }); saw_any = false; i += 1; } '!' => { field.push(unescape(&chars, &mut i)?); saw_any = true; i += 1; } '[' | '{' => { let (close, is_union_brace) = if c == '[' { (']', false) } else { ('}', true) }; let mut inner = String::new(); i += 1; loop { match chars.get(i) { None => return Err(format!("unclosed {c}")), Some(&close_c) if close_c == close => break, Some(&'!') => inner.push(unescape(&chars, &mut i)?), Some(&ch) => inner.push(ch), } i += 1; } i += 1; // consume closer let modifier = match (is_union_brace, inner.is_empty()) { (false, true) => Modifier::Elements, (false, false) => Modifier::UnionElements(inner), (true, true) => return Err("empty {} union ref".to_owned()), (true, false) => Modifier::Union(inner), }; if field.is_empty() && !saw_any { return Err("modifier without a field".to_owned()); } segments.push(Segment { field: std::mem::take(&mut field), modifier: Some(modifier), }); saw_any = false; // a modified segment must end the path or be followed by '.' match chars.get(i) { None => break, Some('.') => { i += 1; } Some(other) => { return Err(format!("expected '.' after {c}{close}, got {other:?}")); } } } _ => { field.push(c); saw_any = true; i += 1; } } } if saw_any { segments.push(Segment { field, modifier: None, }); } if segments.is_empty() { return Err("empty path".to_owned()); } Ok(segments) } pub(crate) fn type_tag(value: &Value) -> Option<&str> { value.get("$type").and_then(Value::as_str) } pub(crate) fn walk_path<'a>( path: &RecordPath, roots: impl IntoIterator, ) -> Vec<&'a Value> { let mut nodes: Vec<&Value> = roots.into_iter().collect(); for segment in &path.0 { let mut next: Vec<&Value> = nodes .into_iter() .filter_map(|node| node.get(&segment.field)) .collect(); if let Some(modifier) = &segment.modifier { next = match modifier { Modifier::Elements => next .into_iter() .flat_map(|node| node.as_array().into_iter().flatten()) .collect(), Modifier::UnionElements(nsid) => next .into_iter() .flat_map(|node| node.as_array().into_iter().flatten()) .filter(|item| type_tag(item) == Some(nsid.as_str())) .collect(), Modifier::Union(nsid) => next .into_iter() .filter(|node| type_tag(node) == Some(nsid.as_str())) .collect(), }; } nodes = next; } nodes } #[cfg(test)] mod tests { use super::*; use serde_json::json; fn parse_ok(path: &str) -> Vec { parse_record_path(path).unwrap_or_else(|e| panic!("{path}: {e}")) } #[test] fn parses_field_paths() { assert_eq!( parse_ok("subject.uri"), vec![ Segment { field: "subject".into(), modifier: None }, Segment { field: "uri".into(), modifier: None }, ] ); } #[test] fn parses_array_descents() { assert_eq!( parse_ok("repos[]"), vec![Segment { field: "repos".into(), modifier: Some(Modifier::Elements) }] ); assert_eq!( parse_ok("facets[].features[app.bsky.richtext.facet#mention].did"), vec![ Segment { field: "facets".into(), modifier: Some(Modifier::Elements) }, Segment { field: "features".into(), modifier: Some(Modifier::UnionElements( "app.bsky.richtext.facet#mention".into() )) }, Segment { field: "did".into(), modifier: None }, ] ); assert_eq!( parse_ok("embed{app.bsky.embed.record}.record.uri"), vec![ Segment { field: "embed".into(), modifier: Some(Modifier::Union("app.bsky.embed.record".into())) }, Segment { field: "record".into(), modifier: None }, Segment { field: "uri".into(), modifier: None }, ] ); } #[test] fn parses_escaped_field_names() { assert_eq!( parse_ok("meta.dot!.name"), vec![Segment { field: "meta".into(), modifier: None }] .into_iter() .chain([Segment { field: "dot.name".into(), modifier: None }]) .collect::>() ); assert_eq!( parse_ok("meta.a!!b"), vec![ Segment { field: "meta".into(), modifier: None }, Segment { field: "a!b".into(), modifier: None }, ] ); assert_eq!( parse_ok("meta.$unknown"), vec![ Segment { field: "meta".into(), modifier: None }, Segment { field: "$unknown".into(), modifier: None }, ] ); } #[test] fn rejects_malformed_paths() { for bad in [ "", ".", "a..b", "a!", "a!x", "a[", "a[]b", "a{}", "[did]", "a[nsid]b", ] { assert!(parse_record_path(bad).is_err(), "{bad} should fail"); } } #[test] fn walks_vector_matches() { let doc = json!({ "repos": [ {"uri": "at://did:plc:a/sh.tangled.repo/x"}, {"uri": "at://did:plc:b/sh.tangled.repo/y"}, ], "owner": "did:plc:z", }); let path = RecordPath::parse("repos[].uri").unwrap(); let found = walk_path(&path, [&doc]); assert_eq!(found.len(), 2); } #[test] fn walks_union_filters() { let doc = json!({ "items": [ {"$type": "sh.tangled.repo", "uri": "at://did:plc:a/sh.tangled.repo/x"}, {"$type": "sh.tangled.actor.profile", "did": "did:plc:b"}, ] }); let segs = parse_ok("items[sh.tangled.repo].uri"); let found = walk_path(&RecordPath(segs), [&doc]); assert_eq!(found, vec![&json!("at://did:plc:a/sh.tangled.repo/x")]); } }