This repository has no description
1//! subset of microcosm.blue/RecordPath
2
3use serde_json::Value;
4
5#[derive(Debug, Clone, PartialEq)]
6pub(crate) enum Modifier {
7 /// `[]`
8 Elements,
9 /// `[nsid]`
10 UnionElements(String),
11 /// `{nsid}`
12 Union(String),
13}
14
15#[derive(Debug, Clone, PartialEq)]
16pub(crate) struct Segment {
17 field: String,
18 modifier: Option<Modifier>,
19}
20
21#[derive(Debug, Clone, PartialEq)]
22pub(crate) struct RecordPath(Vec<Segment>);
23
24impl RecordPath {
25 pub(crate) fn parse(path: &str) -> Result<Self, String> {
26 parse_record_path(path).map(Self)
27 }
28}
29
30pub(crate) fn parse_record_path(path: &str) -> Result<Vec<Segment>, String> {
31 let chars: Vec<char> = path.chars().collect();
32 let mut segments = Vec::new();
33 let mut field = String::new();
34 let mut i = 0;
35 let mut saw_any = false;
36
37 let unescape = |chars: &[char], i: &mut usize| -> Result<char, String> {
38 *i += 1;
39 match chars.get(*i) {
40 Some(&c @ ('.' | '[' | ']' | '{' | '}' | '!')) => Ok(c),
41 Some(c) => Err(format!("invalid escape !{c}")),
42 None => Err("trailing ! escape".to_owned()),
43 }
44 };
45
46 while i < chars.len() {
47 let c = chars[i];
48 match c {
49 '.' => {
50 if !saw_any {
51 return Err("empty path segment".to_owned());
52 }
53 segments.push(Segment {
54 field: std::mem::take(&mut field),
55 modifier: None,
56 });
57 saw_any = false;
58 i += 1;
59 }
60 '!' => {
61 field.push(unescape(&chars, &mut i)?);
62 saw_any = true;
63 i += 1;
64 }
65 '[' | '{' => {
66 let (close, is_union_brace) = if c == '[' { (']', false) } else { ('}', true) };
67 let mut inner = String::new();
68 i += 1;
69 loop {
70 match chars.get(i) {
71 None => return Err(format!("unclosed {c}")),
72 Some(&close_c) if close_c == close => break,
73 Some(&'!') => inner.push(unescape(&chars, &mut i)?),
74 Some(&ch) => inner.push(ch),
75 }
76 i += 1;
77 }
78 i += 1; // consume closer
79 let modifier = match (is_union_brace, inner.is_empty()) {
80 (false, true) => Modifier::Elements,
81 (false, false) => Modifier::UnionElements(inner),
82 (true, true) => return Err("empty {} union ref".to_owned()),
83 (true, false) => Modifier::Union(inner),
84 };
85 if field.is_empty() && !saw_any {
86 return Err("modifier without a field".to_owned());
87 }
88 segments.push(Segment {
89 field: std::mem::take(&mut field),
90 modifier: Some(modifier),
91 });
92 saw_any = false;
93 // a modified segment must end the path or be followed by '.'
94 match chars.get(i) {
95 None => break,
96 Some('.') => {
97 i += 1;
98 }
99 Some(other) => {
100 return Err(format!("expected '.' after {c}{close}, got {other:?}"));
101 }
102 }
103 }
104 _ => {
105 field.push(c);
106 saw_any = true;
107 i += 1;
108 }
109 }
110 }
111 if saw_any {
112 segments.push(Segment {
113 field,
114 modifier: None,
115 });
116 }
117 if segments.is_empty() {
118 return Err("empty path".to_owned());
119 }
120 Ok(segments)
121}
122
123pub(crate) fn type_tag(value: &Value) -> Option<&str> {
124 value.get("$type").and_then(Value::as_str)
125}
126
127pub(crate) fn walk_path<'a>(
128 path: &RecordPath,
129 roots: impl IntoIterator<Item = &'a Value>,
130) -> Vec<&'a Value> {
131 let mut nodes: Vec<&Value> = roots.into_iter().collect();
132 for segment in &path.0 {
133 let mut next: Vec<&Value> = nodes
134 .into_iter()
135 .filter_map(|node| node.get(&segment.field))
136 .collect();
137 if let Some(modifier) = &segment.modifier {
138 next = match modifier {
139 Modifier::Elements => next
140 .into_iter()
141 .flat_map(|node| node.as_array().into_iter().flatten())
142 .collect(),
143 Modifier::UnionElements(nsid) => next
144 .into_iter()
145 .flat_map(|node| node.as_array().into_iter().flatten())
146 .filter(|item| type_tag(item) == Some(nsid.as_str()))
147 .collect(),
148 Modifier::Union(nsid) => next
149 .into_iter()
150 .filter(|node| type_tag(node) == Some(nsid.as_str()))
151 .collect(),
152 };
153 }
154 nodes = next;
155 }
156 nodes
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use serde_json::json;
163
164 fn parse_ok(path: &str) -> Vec<Segment> {
165 parse_record_path(path).unwrap_or_else(|e| panic!("{path}: {e}"))
166 }
167
168 #[test]
169 fn parses_field_paths() {
170 assert_eq!(
171 parse_ok("subject.uri"),
172 vec![
173 Segment {
174 field: "subject".into(),
175 modifier: None
176 },
177 Segment {
178 field: "uri".into(),
179 modifier: None
180 },
181 ]
182 );
183 }
184
185 #[test]
186 fn parses_array_descents() {
187 assert_eq!(
188 parse_ok("repos[]"),
189 vec![Segment {
190 field: "repos".into(),
191 modifier: Some(Modifier::Elements)
192 }]
193 );
194 assert_eq!(
195 parse_ok("facets[].features[app.bsky.richtext.facet#mention].did"),
196 vec![
197 Segment {
198 field: "facets".into(),
199 modifier: Some(Modifier::Elements)
200 },
201 Segment {
202 field: "features".into(),
203 modifier: Some(Modifier::UnionElements(
204 "app.bsky.richtext.facet#mention".into()
205 ))
206 },
207 Segment {
208 field: "did".into(),
209 modifier: None
210 },
211 ]
212 );
213 assert_eq!(
214 parse_ok("embed{app.bsky.embed.record}.record.uri"),
215 vec![
216 Segment {
217 field: "embed".into(),
218 modifier: Some(Modifier::Union("app.bsky.embed.record".into()))
219 },
220 Segment {
221 field: "record".into(),
222 modifier: None
223 },
224 Segment {
225 field: "uri".into(),
226 modifier: None
227 },
228 ]
229 );
230 }
231
232 #[test]
233 fn parses_escaped_field_names() {
234 assert_eq!(
235 parse_ok("meta.dot!.name"),
236 vec![Segment {
237 field: "meta".into(),
238 modifier: None
239 }]
240 .into_iter()
241 .chain([Segment {
242 field: "dot.name".into(),
243 modifier: None
244 }])
245 .collect::<Vec<_>>()
246 );
247 assert_eq!(
248 parse_ok("meta.a!!b"),
249 vec![
250 Segment {
251 field: "meta".into(),
252 modifier: None
253 },
254 Segment {
255 field: "a!b".into(),
256 modifier: None
257 },
258 ]
259 );
260 assert_eq!(
261 parse_ok("meta.$unknown"),
262 vec![
263 Segment {
264 field: "meta".into(),
265 modifier: None
266 },
267 Segment {
268 field: "$unknown".into(),
269 modifier: None
270 },
271 ]
272 );
273 }
274
275 #[test]
276 fn rejects_malformed_paths() {
277 for bad in [
278 "", ".", "a..b", "a!", "a!x", "a[", "a[]b", "a{}", "[did]", "a[nsid]b",
279 ] {
280 assert!(parse_record_path(bad).is_err(), "{bad} should fail");
281 }
282 }
283
284 #[test]
285 fn walks_vector_matches() {
286 let doc = json!({
287 "repos": [
288 {"uri": "at://did:plc:a/sh.tangled.repo/x"},
289 {"uri": "at://did:plc:b/sh.tangled.repo/y"},
290 ],
291 "owner": "did:plc:z",
292 });
293 let path = RecordPath::parse("repos[].uri").unwrap();
294 let found = walk_path(&path, [&doc]);
295 assert_eq!(found.len(), 2);
296 }
297
298 #[test]
299 fn walks_union_filters() {
300 let doc = json!({
301 "items": [
302 {"$type": "sh.tangled.repo", "uri": "at://did:plc:a/sh.tangled.repo/x"},
303 {"$type": "sh.tangled.actor.profile", "did": "did:plc:b"},
304 ]
305 });
306 let segs = parse_ok("items[sh.tangled.repo].uri");
307 let found = walk_path(&RecordPath(segs), [&doc]);
308 assert_eq!(found, vec![&json!("at://did:plc:a/sh.tangled.repo/x")]);
309 }
310}