tangled
alpha
login
or
join now
bad-example.com
/
microcosm-links
7
fork
atom
APIs for links and references in the ATmosphere
7
fork
atom
overview
issues
pulls
pipelines
walk record json and get links
bad-example.com
1 year ago
f670a5d7
212ff2ee
+156
-4
2 changed files
expand all
collapse all
unified
split
src
lib.rs
record.rs
+19
-4
src/lib.rs
···
2
2
3
3
pub mod at_uri;
4
4
pub mod did;
5
5
+
pub mod record;
6
6
+
7
7
+
pub use record::collect_links;
5
8
6
9
#[derive(Debug, PartialEq)]
7
10
pub enum Link {
···
10
13
Did(String),
11
14
}
12
15
16
16
+
impl Link {
17
17
+
pub fn into_string(self) -> String {
18
18
+
match self {
19
19
+
Link::AtUri(s) => s,
20
20
+
Link::Uri(s) => s,
21
21
+
Link::Did(s) => s,
22
22
+
}
23
23
+
}
24
24
+
}
25
25
+
13
26
// normalizing is a bit opinionated but eh
14
27
pub fn parse_uri(s: &str) -> Option<String> {
15
28
Uri::parse(s).map(|u| u.normalize().into_string()).ok()
16
29
}
17
30
18
18
-
pub fn parse_any(s: &str) -> Option<Link> {
31
31
+
pub fn parse_any_link(s: &str) -> Option<Link> {
19
32
at_uri::parse_at_uri(s).map(Link::AtUri).or_else(|| {
20
33
did::parse_did(s)
21
34
.map(Link::Did)
···
49
62
#[test]
50
63
fn test_any_parse() {
51
64
assert_eq!(
52
52
-
parse_any("https://example.com"),
65
65
+
parse_any_link("https://example.com"),
53
66
Some(Link::Uri("https://example.com".into()))
54
67
);
55
68
56
69
assert_eq!(
57
57
-
parse_any("at://did:plc:44ybard66vv44zksje25o7dz/app.bsky.feed.post/3jwdwj2ctlk26"),
70
70
+
parse_any_link(
71
71
+
"at://did:plc:44ybard66vv44zksje25o7dz/app.bsky.feed.post/3jwdwj2ctlk26"
72
72
+
),
58
73
Some(Link::AtUri(
59
74
"at://did:plc:44ybard66vv44zksje25o7dz/app.bsky.feed.post/3jwdwj2ctlk26".into()
60
75
)),
61
76
);
62
77
63
78
assert_eq!(
64
64
-
parse_any("did:plc:44ybard66vv44zksje25o7dz"),
79
79
+
parse_any_link("did:plc:44ybard66vv44zksje25o7dz"),
65
80
Some(Link::Did("did:plc:44ybard66vv44zksje25o7dz".into()))
66
81
)
67
82
}
+137
src/record.rs
···
1
1
+
use tinyjson::JsonValue;
2
2
+
3
3
+
use crate::parse_any_link;
4
4
+
5
5
+
pub fn walk_record(path: &str, v: &JsonValue, found: &mut Vec<(String, String)>) {
6
6
+
match v {
7
7
+
JsonValue::Object(o) => {
8
8
+
for (key, child) in o {
9
9
+
walk_record(&format!("{path}.{key}"), child, found)
10
10
+
}
11
11
+
}
12
12
+
JsonValue::Array(a) => {
13
13
+
let p = format!("{path}[]");
14
14
+
for child in a {
15
15
+
walk_record(&p, child, found)
16
16
+
}
17
17
+
}
18
18
+
JsonValue::String(s) => {
19
19
+
if let Some(link) = parse_any_link(s) {
20
20
+
found.push((path.to_string(), link.into_string()));
21
21
+
}
22
22
+
}
23
23
+
_ => {}
24
24
+
}
25
25
+
}
26
26
+
27
27
+
pub fn collect_links(v: JsonValue) -> Vec<(String, String)> {
28
28
+
let mut found = vec![];
29
29
+
walk_record("", &v, &mut found);
30
30
+
found
31
31
+
}
32
32
+
33
33
+
#[cfg(test)]
34
34
+
mod tests {
35
35
+
use super::*;
36
36
+
37
37
+
#[test]
38
38
+
fn test_collect_links() {
39
39
+
let rec = r#"{"a": "https://example.com", "b": "not a link"}"#;
40
40
+
let json = collect_links(rec.parse().unwrap());
41
41
+
assert_eq!(json, vec![(".a".into(), "https://example.com".into())]);
42
42
+
}
43
43
+
44
44
+
#[test]
45
45
+
fn test_bsky_feed_post_record_reply() {
46
46
+
let rec = r#"{
47
47
+
"$type": "app.bsky.feed.post",
48
48
+
"createdAt": "2025-01-08T20:52:43.041Z",
49
49
+
"langs": [
50
50
+
"en"
51
51
+
],
52
52
+
"reply": {
53
53
+
"parent": {
54
54
+
"cid": "bafyreifk3bwnmulk37ezrarg4ouheqnhgucypynftqafl4limssogvzk6i",
55
55
+
"uri": "at://did:plc:b3rzzkblqsxhr3dgcueymkqe/app.bsky.feed.post/3lf6yc4drhk2f"
56
56
+
},
57
57
+
"root": {
58
58
+
"cid": "bafyreifk3bwnmulk37ezrarg4ouheqnhgucypynftqafl4limssogvzk6i",
59
59
+
"uri": "at://did:plc:b3rzzkblqsxhr3dgcueymkqe/app.bsky.feed.post/3lf6yc4drhk2f"
60
60
+
}
61
61
+
},
62
62
+
"text": "Yup!"
63
63
+
}"#;
64
64
+
let mut json = collect_links(rec.parse().unwrap());
65
65
+
json.sort();
66
66
+
assert_eq!(
67
67
+
json,
68
68
+
vec![
69
69
+
(
70
70
+
".reply.parent.uri".into(),
71
71
+
"at://did:plc:b3rzzkblqsxhr3dgcueymkqe/app.bsky.feed.post/3lf6yc4drhk2f".into()
72
72
+
),
73
73
+
(
74
74
+
".reply.root.uri".into(),
75
75
+
"at://did:plc:b3rzzkblqsxhr3dgcueymkqe/app.bsky.feed.post/3lf6yc4drhk2f".into()
76
76
+
),
77
77
+
]
78
78
+
)
79
79
+
}
80
80
+
81
81
+
#[test]
82
82
+
fn test_bsky_feed_post_record_embed() {
83
83
+
let rec = r#"{
84
84
+
"$type": "app.bsky.feed.post",
85
85
+
"createdAt": "2025-01-08T20:52:39.539Z",
86
86
+
"embed": {
87
87
+
"$type": "app.bsky.embed.external",
88
88
+
"external": {
89
89
+
"description": "YouTube video by More Perfect Union",
90
90
+
"thumb": {
91
91
+
"$type": "blob",
92
92
+
"ref": {
93
93
+
"$link": "bafkreifxuvkbqksq5usi4cryex37o4absjexuouvgenlb62ojsx443b2tm"
94
94
+
},
95
95
+
"mimeType": "image/jpeg",
96
96
+
"size": 477460
97
97
+
},
98
98
+
"title": "Corporations & Wealthy Elites Are Coopting Our Government. Who Can Stop Them?",
99
99
+
"uri": "https://youtu.be/oKXm4szEP1Q?si=_0n_uPu4qNKokMnq"
100
100
+
}
101
101
+
},
102
102
+
"facets": [
103
103
+
{
104
104
+
"features": [
105
105
+
{
106
106
+
"$type": "app.bsky.richtext.facet#link",
107
107
+
"uri": "https://youtu.be/oKXm4szEP1Q?si=_0n_uPu4qNKokMnq"
108
108
+
}
109
109
+
],
110
110
+
"index": {
111
111
+
"byteEnd": 24,
112
112
+
"byteStart": 0
113
113
+
}
114
114
+
}
115
115
+
],
116
116
+
"langs": [
117
117
+
"en"
118
118
+
],
119
119
+
"text": "youtu.be/oKXm4szEP1Q?..."
120
120
+
}"#;
121
121
+
let mut json = collect_links(rec.parse().unwrap());
122
122
+
json.sort();
123
123
+
assert_eq!(
124
124
+
json,
125
125
+
vec![
126
126
+
(
127
127
+
".embed.external.uri".into(),
128
128
+
"https://youtu.be/oKXm4szEP1Q?si=_0n_uPu4qNKokMnq".into()
129
129
+
),
130
130
+
(
131
131
+
".facets[].features[].uri".into(),
132
132
+
"https://youtu.be/oKXm4szEP1Q?si=_0n_uPu4qNKokMnq".into()
133
133
+
),
134
134
+
]
135
135
+
)
136
136
+
}
137
137
+
}