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