Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311//! 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<Modifier>,}
#[derive(Debug, Clone, PartialEq)]pub(crate) struct RecordPath(Vec<Segment>);
impl RecordPath { pub(crate) fn parse(path: &str) -> Result<Self, String> { parse_record_path(path).map(Self) }}
pub(crate) fn parse_record_path(path: &str) -> Result<Vec<Segment>, String> { let chars: Vec<char> = 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<char, String> { *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<Item = &'a Value>,) -> 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<Segment> { 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::<Vec<_>>() ); 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")]); }}