forked from
microcosm.blue/microcosm-rs
Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm
1use dasl::drisl::Value as DrislValue;
2use tinyjson::JsonValue;
3
4use crate::{parse_any_link, CollectedLink};
5
6pub fn walk_record(path: &str, v: &JsonValue, found: &mut Vec<CollectedLink>) {
7 match v {
8 JsonValue::Object(o) => {
9 for (key, child) in o {
10 walk_record(&format!("{path}.{key}"), child, found)
11 }
12 }
13 JsonValue::Array(a) => {
14 for child in a {
15 let child_p = match child {
16 JsonValue::Object(o) => {
17 if let Some(JsonValue::String(t)) = o.get("$type") {
18 format!("{path}[{t}]")
19 } else {
20 format!("{path}[]")
21 }
22 }
23 _ => format!("{path}[]"),
24 };
25 walk_record(&child_p, child, found)
26 }
27 }
28 JsonValue::String(s) => {
29 if let Some(link) = parse_any_link(s) {
30 found.push(CollectedLink {
31 path: path.to_string(),
32 target: link,
33 });
34 }
35 }
36 _ => {}
37 }
38}
39
40pub fn walk_drisl(path: &str, v: &DrislValue, found: &mut Vec<CollectedLink>) {
41 match v {
42 DrislValue::Map(o) => {
43 for (key, child) in o {
44 walk_drisl(&format!("{path}.{key}"), child, found)
45 }
46 }
47 DrislValue::Array(a) => {
48 for child in a {
49 let child_p = match child {
50 DrislValue::Map(o) => {
51 if let Some(DrislValue::Text(t)) = o.get("$type") {
52 format!("{path}[{t}]")
53 } else {
54 format!("{path}[]")
55 }
56 }
57 _ => format!("{path}[]"),
58 };
59 walk_drisl(&child_p, child, found)
60 }
61 }
62 DrislValue::Text(s) => {
63 if let Some(link) = parse_any_link(s) {
64 found.push(CollectedLink {
65 path: path.to_string(),
66 target: link,
67 });
68 }
69 }
70 _ => {}
71 }
72}
73
74pub fn collect_links(v: &JsonValue) -> Vec<CollectedLink> {
75 let mut found = vec![];
76 walk_record("", v, &mut found);
77 found
78}
79
80pub fn collect_links_drisl(v: &DrislValue) -> Vec<CollectedLink> {
81 let mut found = vec![];
82 walk_drisl("", v, &mut found);
83 found
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use crate::Link;
90
91 fn l(path: &str, target: Link) -> CollectedLink {
92 CollectedLink {
93 path: path.into(),
94 target,
95 }
96 }
97
98 #[test]
99 fn test_collect_links() {
100 let rec = r#"{"a": "https://example.com", "b": "not a link"}"#;
101 let json = collect_links(&rec.parse().unwrap());
102 assert_eq!(json, vec![l(".a", Link::Uri("https://example.com".into()))]);
103 }
104
105 #[test]
106 fn test_bsky_feed_post_record_reply() {
107 let rec = r#"{
108 "$type": "app.bsky.feed.post",
109 "createdAt": "2025-01-08T20:52:43.041Z",
110 "langs": [
111 "en"
112 ],
113 "reply": {
114 "parent": {
115 "cid": "bafyreifk3bwnmulk37ezrarg4ouheqnhgucypynftqafl4limssogvzk6i",
116 "uri": "at://did:plc:b3rzzkblqsxhr3dgcueymkqe/app.bsky.feed.post/3lf6yc4drhk2f"
117 },
118 "root": {
119 "cid": "bafyreifk3bwnmulk37ezrarg4ouheqnhgucypynftqafl4limssogvzk6i",
120 "uri": "at://did:plc:b3rzzkblqsxhr3dgcueymkqe/app.bsky.feed.post/3lf6yc4drhk2f"
121 }
122 },
123 "text": "Yup!"
124 }"#;
125 let mut json = collect_links(&rec.parse().unwrap());
126 json.sort_by_key(|c| (c.path.clone(), c.target.clone()));
127 assert_eq!(
128 json,
129 vec![
130 l(
131 ".reply.parent.uri",
132 Link::AtUri(
133 "at://did:plc:b3rzzkblqsxhr3dgcueymkqe/app.bsky.feed.post/3lf6yc4drhk2f"
134 .into()
135 )
136 ),
137 l(
138 ".reply.root.uri",
139 Link::AtUri(
140 "at://did:plc:b3rzzkblqsxhr3dgcueymkqe/app.bsky.feed.post/3lf6yc4drhk2f"
141 .into()
142 )
143 ),
144 ]
145 )
146 }
147
148 #[test]
149 fn test_bsky_feed_post_record_embed() {
150 let rec = r#"{
151 "$type": "app.bsky.feed.post",
152 "createdAt": "2025-01-08T20:52:39.539Z",
153 "embed": {
154 "$type": "app.bsky.embed.external",
155 "external": {
156 "description": "YouTube video by More Perfect Union",
157 "thumb": {
158 "$type": "blob",
159 "ref": {
160 "$link": "bafkreifxuvkbqksq5usi4cryex37o4absjexuouvgenlb62ojsx443b2tm"
161 },
162 "mimeType": "image/jpeg",
163 "size": 477460
164 },
165 "title": "Corporations & Wealthy Elites Are Coopting Our Government. Who Can Stop Them?",
166 "uri": "https://youtu.be/oKXm4szEP1Q?si=_0n_uPu4qNKokMnq"
167 }
168 },
169 "facets": [
170 {
171 "features": [
172 {
173 "$type": "app.bsky.richtext.facet#link",
174 "uri": "https://youtu.be/oKXm4szEP1Q?si=_0n_uPu4qNKokMnq"
175 }
176 ],
177 "index": {
178 "byteEnd": 24,
179 "byteStart": 0
180 }
181 }
182 ],
183 "langs": [
184 "en"
185 ],
186 "text": "youtu.be/oKXm4szEP1Q?..."
187 }"#;
188 let mut json = collect_links(&rec.parse().unwrap());
189 json.sort_by_key(|c| (c.path.clone(), c.target.clone()));
190 assert_eq!(
191 json,
192 vec![
193 l(
194 ".embed.external.uri",
195 Link::Uri("https://youtu.be/oKXm4szEP1Q?si=_0n_uPu4qNKokMnq".into()),
196 ),
197 l(
198 ".facets[].features[app.bsky.richtext.facet#link].uri",
199 Link::Uri("https://youtu.be/oKXm4szEP1Q?si=_0n_uPu4qNKokMnq".into()),
200 ),
201 ]
202 )
203 }
204}