1use crate::bos::Bos;
2use crate::deps::fluent_uri::Uri;
3use crate::{
4 DefaultStr, IntoStatic,
5 types::{
6 aturi::{AtUri, validate_and_index},
7 cid::Cid,
8 collection::Collection,
9 did::{Did, validate_did},
10 nsid::Nsid,
11 string::{AtStrError, StrParseKind},
12 },
13};
14use alloc::string::{String, ToString};
15use core::{fmt::Display, marker::PhantomData, ops::Deref, str::FromStr};
16use serde::{Deserialize, Deserializer, Serialize, Serializer};
17
18#[derive(Debug, Clone, PartialEq, Eq, Hash)]
25pub enum UriValue<S: Bos<str> + AsRef<str> = DefaultStr> {
26 Did(Did<S>),
28 At(AtUri<S>),
30 Https(Uri<String>),
32 Wss(Uri<String>),
34 Cid(Cid<S>),
36 Any(S),
38}
39
40#[derive(Debug, thiserror::Error, miette::Diagnostic)]
42#[non_exhaustive]
43pub enum UriParseError {
44 #[error("Invalid atproto string: {0}")]
46 At(#[from] AtStrError),
47 #[error(transparent)]
49 Uri(#[from] crate::deps::fluent_uri::ParseError),
50 #[error(transparent)]
52 Cid(#[from] crate::types::cid::Error),
53}
54
55impl<S: Bos<str> + AsRef<str>> UriValue<S> {
60 pub fn new(uri: S) -> Result<Self, UriParseError> {
64 let s = uri.as_ref();
65 if s.starts_with("did:") {
66 if validate_did(s).is_ok() {
67 return Ok(UriValue::Did(unsafe { Did::unchecked(uri) }));
68 }
69 } else if s.starts_with("at://") {
70 if let Ok(indices) = validate_and_index(s) {
71 return Ok(UriValue::At(unsafe { AtUri::from_parts(uri, indices) }));
72 }
73 } else if s.starts_with("https://") {
74 if let Ok(parsed) = Uri::parse(s) {
75 return Ok(UriValue::Https(parsed.to_owned()));
76 }
77 } else if s.starts_with("wss://") {
78 if let Ok(parsed) = Uri::parse(s) {
79 return Ok(UriValue::Wss(parsed.to_owned()));
80 }
81 } else if s.starts_with("ipld://") {
82 return Ok(UriValue::Cid(unsafe { Cid::unchecked_str(uri) }));
83 }
84 Ok(UriValue::Any(uri))
85 }
86}
87
88impl<S: Bos<str> + AsRef<str> + FromStr> UriValue<S> {
93 pub fn new_owned(uri: impl AsRef<str>) -> Result<Self, UriParseError> {
95 let uri_str = uri.as_ref();
96 if uri_str.starts_with("did:") {
97 Ok(UriValue::Did(Did::new_owned(uri_str)?))
98 } else if uri_str.starts_with("at://") {
99 Ok(UriValue::At(AtUri::new_owned(uri_str)?))
100 } else if uri_str.starts_with("https://") {
101 Ok(UriValue::Https(Uri::parse(uri_str)?.to_owned()))
102 } else if uri_str.starts_with("wss://") {
103 Ok(UriValue::Wss(Uri::parse(uri_str)?.to_owned()))
104 } else if uri_str.starts_with("ipld://") {
105 let cid_part = &uri_str[7..];
106 if cid_part.is_empty() {
107 let s = S::from_str(uri_str).map_err(|_| {
108 UriParseError::At(AtStrError::new(
109 "uri",
110 uri_str.to_string(),
111 StrParseKind::Conversion,
112 ))
113 })?;
114 Ok(UriValue::Any(s))
115 } else {
116 let s = S::from_str(cid_part).map_err(|_| {
117 UriParseError::At(AtStrError::new(
118 "uri",
119 cid_part.to_string(),
120 StrParseKind::Conversion,
121 ))
122 })?;
123 Ok(UriValue::Cid(unsafe { Cid::unchecked_str(s) }))
124 }
125 } else {
126 let s = S::from_str(uri_str).map_err(|_| {
127 UriParseError::At(AtStrError::new(
128 "uri",
129 uri_str.to_string(),
130 StrParseKind::Conversion,
131 ))
132 })?;
133 Ok(UriValue::Any(s))
134 }
135 }
136}
137
138impl<S: Bos<str> + AsRef<str>> UriValue<S> {
143 pub fn as_str(&self) -> &str {
145 match self {
146 UriValue::Did(did) => did.as_str(),
147 UriValue::At(at_uri) => at_uri.as_str(),
148 UriValue::Https(url) => url.as_str(),
149 UriValue::Wss(url) => url.as_str(),
150 UriValue::Cid(cid) => cid.as_str(),
151 UriValue::Any(s) => s.as_ref(),
152 }
153 }
154}
155
156impl<S: Bos<str> + AsRef<str>> Serialize for UriValue<S> {
161 fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error>
162 where
163 Ser: Serializer,
164 {
165 serializer.serialize_str(self.as_str())
166 }
167}
168
169impl<'de, S: Bos<str> + AsRef<str> + Deserialize<'de>> Deserialize<'de> for UriValue<S> {
170 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
171 where
172 D: Deserializer<'de>,
173 {
174 let value: S = Deserialize::deserialize(deserializer)?;
175 Self::new(value).map_err(serde::de::Error::custom)
176 }
177}
178
179impl<S: Bos<str> + AsRef<str>> AsRef<str> for UriValue<S> {
184 fn as_ref(&self) -> &str {
185 self.as_str()
186 }
187}
188
189impl<S: Bos<str> + AsRef<str> + IntoStatic> IntoStatic for UriValue<S>
190where
191 S::Output: Bos<str> + AsRef<str>,
192{
193 type Output = UriValue<S::Output>;
194
195 fn into_static(self) -> Self::Output {
196 match self {
197 UriValue::Did(did) => UriValue::Did(did.into_static()),
198 UriValue::At(at_uri) => UriValue::At(at_uri.into_static()),
199 UriValue::Https(url) => UriValue::Https(url),
200 UriValue::Wss(url) => UriValue::Wss(url),
201 UriValue::Cid(cid) => UriValue::Cid(cid.into_static()),
202 UriValue::Any(s) => UriValue::Any(s.into_static()),
203 }
204 }
205}
206
207impl<S: Bos<str> + AsRef<str>> UriValue<S> {
208 pub fn convert<B: Bos<str> + AsRef<str> + From<S>>(self) -> UriValue<B> {
210 match self {
211 UriValue::Did(did) => UriValue::Did(did.convert()),
212 UriValue::At(at_uri) => UriValue::At(at_uri.convert()),
213 UriValue::Https(url) => UriValue::Https(url),
214 UriValue::Wss(url) => UriValue::Wss(url),
215 UriValue::Cid(cid) => UriValue::Cid(cid.convert()),
216 UriValue::Any(s) => UriValue::Any(B::from(s)),
217 }
218 }
219}
220
221#[repr(transparent)]
226pub struct RecordUri<S: Bos<str> + AsRef<str>, R: Collection>(AtUri<S>, PhantomData<R>);
230
231impl<S: Bos<str> + AsRef<str>, R: Collection> RecordUri<S, R> {
232 pub fn try_from_uri(uri: AtUri<S>) -> Result<Self, UriError> {
234 if let Some(collection) = uri.collection() {
235 if collection.as_str() == R::NSID {
236 return Ok(Self(uri, PhantomData));
237 }
238 }
239 Err(UriError::CollectionMismatch {
240 expected: R::NSID,
241 found: uri
242 .collection()
243 .map(|c| Nsid::new_owned(c.as_str()).unwrap()),
244 })
245 }
246
247 pub fn into_inner(self) -> AtUri<S> {
249 self.0
250 }
251
252 pub fn as_uri(&self) -> &AtUri<S> {
254 &self.0
255 }
256}
257
258impl<S: Bos<str> + AsRef<str>, R: Collection> Display for RecordUri<S, R> {
259 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
260 self.0.fmt(f)
261 }
262}
263
264impl<S: Bos<str> + AsRef<str>, R: Collection> AsRef<AtUri<S>> for RecordUri<S, R> {
265 fn as_ref(&self) -> &AtUri<S> {
266 &self.0
267 }
268}
269
270impl<S: Bos<str> + AsRef<str>, R: Collection> Deref for RecordUri<S, R> {
271 type Target = AtUri<S>;
272
273 fn deref(&self) -> &Self::Target {
274 &self.0
275 }
276}
277
278#[derive(Debug, Clone, PartialEq, thiserror::Error, miette::Diagnostic)]
279#[non_exhaustive]
280pub enum UriError {
282 #[error("Collection mismatch: expected {expected}, found {found:?}")]
284 CollectionMismatch {
285 expected: &'static str,
287 found: Option<Nsid>,
289 },
290 #[error("Invalid URI: {0}")]
292 InvalidUri(#[from] AtStrError),
293}
294
295#[cfg(test)]
296mod tests {
297 use smol_str::SmolStr;
298
299 use crate::CowStr;
300
301 use super::*;
302
303 #[test]
304 fn test_wss_variant_parsing() {
305 let uri = UriValue::new("wss://example.com/path").expect("valid wss uri");
306 assert!(
307 matches!(uri, UriValue::Wss(_)),
308 "wss:// should parse to UriValue::Wss"
309 );
310 assert_eq!(uri.as_str(), "wss://example.com/path");
311 }
312
313 #[test]
314 fn test_https_variant_parsing() {
315 let uri = UriValue::new("https://example.com/path").expect("valid https uri");
316 assert!(
317 matches!(uri, UriValue::Https(_)),
318 "https:// should parse to UriValue::Https"
319 );
320 assert_eq!(uri.as_str(), "https://example.com/path");
321 }
322
323 #[test]
324 fn test_wss_owned_variant_parsing() {
325 let uri: UriValue<SmolStr> =
326 UriValue::new_owned("wss://example.com").expect("valid wss uri");
327 assert!(
328 matches!(uri, UriValue::Wss(_)),
329 "owned wss:// should parse to UriValue::Wss"
330 );
331 assert_eq!(uri.as_str(), "wss://example.com");
332 }
333
334 #[test]
335 fn test_https_owned_variant_parsing() {
336 let uri: UriValue<SmolStr> =
337 UriValue::new_owned("https://example.com").expect("valid https uri");
338 assert!(
339 matches!(uri, UriValue::Https(_)),
340 "owned https:// should parse to UriValue::Https"
341 );
342 assert_eq!(uri.as_str(), "https://example.com");
343 }
344
345 #[test]
346 fn test_wss_cow_variant_parsing() {
347 let uri = UriValue::new(CowStr::Borrowed("wss://example.com")).expect("valid wss uri");
348 assert!(
349 matches!(uri, UriValue::Wss(_)),
350 "cow wss:// should parse to UriValue::Wss"
351 );
352 assert_eq!(uri.as_str(), "wss://example.com");
353 }
354
355 #[test]
356 fn test_https_cow_variant_parsing() {
357 let uri = UriValue::new(CowStr::Borrowed("https://example.com")).expect("valid https uri");
358 assert!(
359 matches!(uri, UriValue::Https(_)),
360 "cow https:// should parse to UriValue::Https"
361 );
362 assert_eq!(uri.as_str(), "https://example.com");
363 }
364
365 #[test]
366 fn test_uri_display() {
367 let wss: UriValue<SmolStr> = UriValue::new_owned("wss://example.com").unwrap();
368 assert_eq!(wss.as_str(), "wss://example.com");
369
370 let https: UriValue<SmolStr> = UriValue::new_owned("https://example.com").unwrap();
371 assert_eq!(https.as_str(), "https://example.com");
372 }
373
374 #[test]
375 fn test_into_static_preserves_variant() {
376 let wss: UriValue<SmolStr> = UriValue::new_owned("wss://example.com").unwrap();
377 let static_wss = wss.into_static();
378 assert!(matches!(static_wss, UriValue::Wss(_)));
379
380 let https: UriValue<SmolStr> = UriValue::new_owned("https://example.com").unwrap();
381 let static_https = https.into_static();
382 assert!(matches!(static_https, UriValue::Https(_)));
383 }
384}