Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm
1use fluent_uri::Uri;
2
3pub mod at_uri;
4pub mod did;
5#[cfg(feature = "json")]
6pub mod record;
7
8#[cfg(feature = "json")]
9pub use record::collect_links;
10
11#[derive(Debug, Clone, Ord, Eq, PartialOrd, PartialEq)]
12pub enum Link {
13 AtUri(String),
14 Uri(String),
15 Did(String),
16}
17
18impl Link {
19 pub fn into_string(self) -> String {
20 match self {
21 Link::AtUri(s) => s,
22 Link::Uri(s) => s,
23 Link::Did(s) => s,
24 }
25 }
26 pub fn as_str(&self) -> &str {
27 match self {
28 Link::AtUri(s) => s,
29 Link::Uri(s) => s,
30 Link::Did(s) => s,
31 }
32 }
33 pub fn name(&self) -> &'static str {
34 match self {
35 Link::AtUri(_) => "at-uri",
36 Link::Uri(_) => "uri",
37 Link::Did(_) => "did",
38 }
39 }
40 pub fn at_uri_collection(&self) -> Option<String> {
41 if let Link::AtUri(at_uri) = self {
42 at_uri::at_uri_collection(at_uri)
43 } else {
44 None
45 }
46 }
47 pub fn did(&self) -> Option<String> {
48 let did = match self {
49 Link::AtUri(s) => {
50 let rest = s.strip_prefix("at://")?; // todo: this might be safe to unwrap?
51 if let Some((did, _)) = rest.split_once("/") {
52 did
53 } else {
54 rest
55 }
56 }
57 Link::Uri(_) => return None,
58 Link::Did(did) => did,
59 };
60 Some(did.to_string())
61 }
62}
63
64#[derive(Debug, PartialEq)]
65pub struct CollectedLink {
66 pub path: String,
67 pub target: Link,
68}
69
70// normalizing is a bit opinionated but eh
71pub fn parse_uri(s: &str) -> Option<String> {
72 Uri::parse(s).map(|u| u.normalize().into_string()).ok()
73}
74
75pub fn parse_any_link(s: &str) -> Option<Link> {
76 at_uri::parse_at_uri(s).map(Link::AtUri).or_else(|| {
77 did::parse_did(s)
78 .map(Link::Did)
79 .or_else(|| parse_uri(s).map(Link::Uri))
80 })
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn test_uri_parse() {
89 let s = "https://example.com";
90 let uri = parse_uri(s).unwrap();
91 assert_eq!(uri.as_str(), s);
92 }
93
94 #[test]
95 fn test_uri_normalizes() {
96 let s = "HTTPS://example.com/../";
97 let uri = parse_uri(s).unwrap();
98 assert_eq!(uri.as_str(), "https://example.com/");
99 }
100
101 #[test]
102 fn test_uri_invalid() {
103 assert!(parse_uri("https:\\bad-example.com").is_none());
104 }
105
106 #[test]
107 fn test_any_parse() {
108 assert_eq!(
109 parse_any_link("https://example.com"),
110 Some(Link::Uri("https://example.com".into()))
111 );
112
113 assert_eq!(
114 parse_any_link(
115 "at://did:plc:44ybard66vv44zksje25o7dz/app.bsky.feed.post/3jwdwj2ctlk26"
116 ),
117 Some(Link::AtUri(
118 "at://did:plc:44ybard66vv44zksje25o7dz/app.bsky.feed.post/3jwdwj2ctlk26".into()
119 )),
120 );
121
122 assert_eq!(
123 parse_any_link("did:plc:44ybard66vv44zksje25o7dz"),
124 Some(Link::Did("did:plc:44ybard66vv44zksje25o7dz".into()))
125 );
126
127 assert_eq!(
128 parse_any_link("tel:5551234567"),
129 Some(Link::Uri("tel:5551234567".into())),
130 );
131
132 assert_eq!(parse_any_link("3jwdwj2ctlk26"), None);
133 assert_eq!(parse_any_link("self"), None);
134 assert_eq!(parse_any_link(""), None);
135 }
136
137 #[test]
138 fn test_at_uri_collection() {
139 assert_eq!(
140 parse_any_link("https://example.com")
141 .unwrap()
142 .at_uri_collection(),
143 None
144 );
145 assert_eq!(
146 parse_any_link("did:web:bad-example.com")
147 .unwrap()
148 .at_uri_collection(),
149 None
150 );
151 assert_eq!(
152 parse_any_link("at://did:web:bad-example.com/my.collection/3jwdwj2ctlk26")
153 .unwrap()
154 .at_uri_collection(),
155 Some("my.collection".into())
156 );
157 }
158}