use crate::bos::Bos;
use crate::deps::fluent_uri::Uri;
use crate::{
DefaultStr, IntoStatic,
types::{
aturi::{AtUri, validate_and_index},
cid::Cid,
collection::Collection,
did::{Did, validate_did},
nsid::Nsid,
string::{AtStrError, StrParseKind},
},
};
use alloc::string::{String, ToString};
use core::{fmt::Display, marker::PhantomData, ops::Deref, str::FromStr};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum UriValue<S: Bos<str> + AsRef<str> = DefaultStr> {
Did(Did<S>),
At(AtUri<S>),
Https(Uri<String>),
Wss(Uri<String>),
Cid(Cid<S>),
Any(S),
}
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
#[non_exhaustive]
pub enum UriParseError {
#[error("Invalid atproto string: {0}")]
At(#[from] AtStrError),
#[error(transparent)]
Uri(#[from] crate::deps::fluent_uri::ParseError),
#[error(transparent)]
Cid(#[from] crate::types::cid::Error),
}
impl<S: Bos<str> + AsRef<str>> UriValue<S> {
pub fn new(uri: S) -> Result<Self, UriParseError> {
let s = uri.as_ref();
if s.starts_with("did:") {
if validate_did(s).is_ok() {
return Ok(UriValue::Did(unsafe { Did::unchecked(uri) }));
}
} else if s.starts_with("at://") {
if let Ok(indices) = validate_and_index(s) {
return Ok(UriValue::At(unsafe { AtUri::from_parts(uri, indices) }));
}
} else if s.starts_with("https://") {
if let Ok(parsed) = Uri::parse(s) {
return Ok(UriValue::Https(parsed.to_owned()));
}
} else if s.starts_with("wss://") {
if let Ok(parsed) = Uri::parse(s) {
return Ok(UriValue::Wss(parsed.to_owned()));
}
} else if s.starts_with("ipld://") {
return Ok(UriValue::Cid(unsafe { Cid::unchecked_str(uri) }));
}
Ok(UriValue::Any(uri))
}
}
impl<S: Bos<str> + AsRef<str> + FromStr> UriValue<S> {
pub fn new_owned(uri: impl AsRef<str>) -> Result<Self, UriParseError> {
let uri_str = uri.as_ref();
if uri_str.starts_with("did:") {
Ok(UriValue::Did(Did::new_owned(uri_str)?))
} else if uri_str.starts_with("at://") {
Ok(UriValue::At(AtUri::new_owned(uri_str)?))
} else if uri_str.starts_with("https://") {
Ok(UriValue::Https(Uri::parse(uri_str)?.to_owned()))
} else if uri_str.starts_with("wss://") {
Ok(UriValue::Wss(Uri::parse(uri_str)?.to_owned()))
} else if uri_str.starts_with("ipld://") {
let cid_part = &uri_str[7..];
if cid_part.is_empty() {
let s = S::from_str(uri_str).map_err(|_| {
UriParseError::At(AtStrError::new(
"uri",
uri_str.to_string(),
StrParseKind::Conversion,
))
})?;
Ok(UriValue::Any(s))
} else {
let s = S::from_str(cid_part).map_err(|_| {
UriParseError::At(AtStrError::new(
"uri",
cid_part.to_string(),
StrParseKind::Conversion,
))
})?;
Ok(UriValue::Cid(unsafe { Cid::unchecked_str(s) }))
}
} else {
let s = S::from_str(uri_str).map_err(|_| {
UriParseError::At(AtStrError::new(
"uri",
uri_str.to_string(),
StrParseKind::Conversion,
))
})?;
Ok(UriValue::Any(s))
}
}
}
impl<S: Bos<str> + AsRef<str>> UriValue<S> {
pub fn as_str(&self) -> &str {
match self {
UriValue::Did(did) => did.as_str(),
UriValue::At(at_uri) => at_uri.as_str(),
UriValue::Https(url) => url.as_str(),
UriValue::Wss(url) => url.as_str(),
UriValue::Cid(cid) => cid.as_str(),
UriValue::Any(s) => s.as_ref(),
}
}
}
impl<S: Bos<str> + AsRef<str>> Serialize for UriValue<S> {
fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error>
where
Ser: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de, S: Bos<str> + AsRef<str> + Deserialize<'de>> Deserialize<'de> for UriValue<S> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value: S = Deserialize::deserialize(deserializer)?;
Self::new(value).map_err(serde::de::Error::custom)
}
}
impl<S: Bos<str> + AsRef<str>> AsRef<str> for UriValue<S> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl<S: Bos<str> + AsRef<str> + IntoStatic> IntoStatic for UriValue<S>
where
S::Output: Bos<str> + AsRef<str>,
{
type Output = UriValue<S::Output>;
fn into_static(self) -> Self::Output {
match self {
UriValue::Did(did) => UriValue::Did(did.into_static()),
UriValue::At(at_uri) => UriValue::At(at_uri.into_static()),
UriValue::Https(url) => UriValue::Https(url),
UriValue::Wss(url) => UriValue::Wss(url),
UriValue::Cid(cid) => UriValue::Cid(cid.into_static()),
UriValue::Any(s) => UriValue::Any(s.into_static()),
}
}
}
impl<S: Bos<str> + AsRef<str>> UriValue<S> {
pub fn convert<B: Bos<str> + AsRef<str> + From<S>>(self) -> UriValue<B> {
match self {
UriValue::Did(did) => UriValue::Did(did.convert()),
UriValue::At(at_uri) => UriValue::At(at_uri.convert()),
UriValue::Https(url) => UriValue::Https(url),
UriValue::Wss(url) => UriValue::Wss(url),
UriValue::Cid(cid) => UriValue::Cid(cid.convert()),
UriValue::Any(s) => UriValue::Any(B::from(s)),
}
}
}
#[repr(transparent)]
pub struct RecordUri<S: Bos<str> + AsRef<str>, R: Collection>(AtUri<S>, PhantomData<R>);
impl<S: Bos<str> + AsRef<str>, R: Collection> RecordUri<S, R> {
pub fn try_from_uri(uri: AtUri<S>) -> Result<Self, UriError> {
if let Some(collection) = uri.collection() {
if collection.as_str() == R::NSID {
return Ok(Self(uri, PhantomData));
}
}
Err(UriError::CollectionMismatch {
expected: R::NSID,
found: uri
.collection()
.map(|c| Nsid::new_owned(c.as_str()).unwrap()),
})
}
pub fn into_inner(self) -> AtUri<S> {
self.0
}
pub fn as_uri(&self) -> &AtUri<S> {
&self.0
}
}
impl<S: Bos<str> + AsRef<str>, R: Collection> Display for RecordUri<S, R> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.0.fmt(f)
}
}
impl<S: Bos<str> + AsRef<str>, R: Collection> AsRef<AtUri<S>> for RecordUri<S, R> {
fn as_ref(&self) -> &AtUri<S> {
&self.0
}
}
impl<S: Bos<str> + AsRef<str>, R: Collection> Deref for RecordUri<S, R> {
type Target = AtUri<S>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, thiserror::Error, miette::Diagnostic)]
#[non_exhaustive]
pub enum UriError {
#[error("Collection mismatch: expected {expected}, found {found:?}")]
CollectionMismatch {
expected: &'static str,
found: Option<Nsid>,
},
#[error("Invalid URI: {0}")]
InvalidUri(#[from] AtStrError),
}
#[cfg(test)]
mod tests {
use smol_str::SmolStr;
use crate::CowStr;
use super::*;
#[test]
fn test_wss_variant_parsing() {
let uri = UriValue::new("wss://example.com/path").expect("valid wss uri");
assert!(
matches!(uri, UriValue::Wss(_)),
"wss:// should parse to UriValue::Wss"
);
assert_eq!(uri.as_str(), "wss://example.com/path");
}
#[test]
fn test_https_variant_parsing() {
let uri = UriValue::new("https://example.com/path").expect("valid https uri");
assert!(
matches!(uri, UriValue::Https(_)),
"https:// should parse to UriValue::Https"
);
assert_eq!(uri.as_str(), "https://example.com/path");
}
#[test]
fn test_wss_owned_variant_parsing() {
let uri: UriValue<SmolStr> =
UriValue::new_owned("wss://example.com").expect("valid wss uri");
assert!(
matches!(uri, UriValue::Wss(_)),
"owned wss:// should parse to UriValue::Wss"
);
assert_eq!(uri.as_str(), "wss://example.com");
}
#[test]
fn test_https_owned_variant_parsing() {
let uri: UriValue<SmolStr> =
UriValue::new_owned("https://example.com").expect("valid https uri");
assert!(
matches!(uri, UriValue::Https(_)),
"owned https:// should parse to UriValue::Https"
);
assert_eq!(uri.as_str(), "https://example.com");
}
#[test]
fn test_wss_cow_variant_parsing() {
let uri = UriValue::new(CowStr::Borrowed("wss://example.com")).expect("valid wss uri");
assert!(
matches!(uri, UriValue::Wss(_)),
"cow wss:// should parse to UriValue::Wss"
);
assert_eq!(uri.as_str(), "wss://example.com");
}
#[test]
fn test_https_cow_variant_parsing() {
let uri = UriValue::new(CowStr::Borrowed("https://example.com")).expect("valid https uri");
assert!(
matches!(uri, UriValue::Https(_)),
"cow https:// should parse to UriValue::Https"
);
assert_eq!(uri.as_str(), "https://example.com");
}
#[test]
fn test_uri_display() {
let wss: UriValue<SmolStr> = UriValue::new_owned("wss://example.com").unwrap();
assert_eq!(wss.as_str(), "wss://example.com");
let https: UriValue<SmolStr> = UriValue::new_owned("https://example.com").unwrap();
assert_eq!(https.as_str(), "https://example.com");
}
#[test]
fn test_into_static_preserves_variant() {
let wss: UriValue<SmolStr> = UriValue::new_owned("wss://example.com").unwrap();
let static_wss = wss.into_static();
assert!(matches!(static_wss, UriValue::Wss(_)));
let https: UriValue<SmolStr> = UriValue::new_owned("https://example.com").unwrap();
let static_https = https.into_static();
assert!(matches!(static_https, UriValue::Https(_)));
}
}