use crate::types::ident::AtIdentifier;
use crate::types::nsid::Nsid;
use crate::types::recordkey::{RecordKey, Rkey};
use crate::types::string::AtStrError;
use crate::{CowStr, IntoStatic};
use alloc::string::String;
use alloc::string::ToString;
use core::fmt;
use core::hash::{Hash, Hasher};
use core::ops::Deref;
use core::str::FromStr;
#[cfg(all(not(target_arch = "wasm32"), feature = "std"))]
use regex::Regex;
#[cfg(all(not(target_arch = "wasm32"), not(feature = "std")))]
use regex_automata::meta::Regex;
#[cfg(target_arch = "wasm32")]
use regex_lite::Regex;
use serde::Serializer;
use serde::{Deserialize, Deserializer, Serialize, de::Error};
use smol_str::{SmolStr, ToSmolStr};
use super::Lazy;
#[derive(PartialEq, Eq, Debug)]
pub struct AtUri<'u> {
inner: Inner<'u>,
}
#[ouroboros::self_referencing]
#[derive(PartialEq, Eq, Debug)]
struct Inner<'u> {
uri: CowStr<'u>,
#[borrows(uri)]
#[covariant]
pub authority: AtIdentifier<'this>,
#[borrows(uri)]
#[covariant]
pub path: Option<RepoPath<'this>>,
#[borrows(uri)]
#[covariant]
pub fragment: Option<CowStr<'this>>,
}
impl Clone for AtUri<'_> {
fn clone(&self) -> Self {
let uri = self.inner.borrow_uri();
Self {
inner: Inner::new(
CowStr::Owned(uri.as_ref().to_smolstr()),
|uri| {
let parts = ATURI_REGEX.captures(uri).unwrap();
unsafe { AtIdentifier::unchecked(parts.name("authority").unwrap().as_str()) }
},
|uri| {
let parts = ATURI_REGEX.captures(uri).unwrap();
if let Some(collection) = parts.name("collection") {
let collection = unsafe { Nsid::unchecked(collection.as_str()) };
let rkey = if let Some(rkey) = parts.name("rkey") {
let rkey = unsafe { RecordKey::from(Rkey::unchecked(rkey.as_str())) };
Some(rkey)
} else {
None
};
Some(RepoPath { collection, rkey })
} else {
None
}
},
|uri| {
let parts = ATURI_REGEX.captures(uri).unwrap();
parts.name("fragment").map(|fragment| {
let fragment = CowStr::Borrowed(fragment.as_str());
fragment
})
},
),
}
}
}
impl Hash for AtUri<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.inner.borrow_uri().hash(state);
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct RepoPath<'u> {
pub collection: Nsid<'u>,
pub rkey: Option<RecordKey<Rkey<'u>>>,
}
impl fmt::Display for RepoPath<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "/{}", self.collection)?;
if let Some(rkey) = &self.rkey {
write!(f, "/{}", rkey.as_ref())?;
}
Ok(())
}
}
impl IntoStatic for RepoPath<'_> {
type Output = RepoPath<'static>;
fn into_static(self) -> Self::Output {
RepoPath {
collection: self.collection.into_static(),
rkey: self.rkey.map(|rkey| rkey.into_static()),
}
}
}
pub type UriPathBuf = RepoPath<'static>;
pub static ATURI_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r##"^at://(?<authority>[a-zA-Z0-9._:%-]+)(/(?<collection>[a-zA-Z0-9-.]+)(/(?<rkey>[a-zA-Z0-9._~:@!$&%')(*+,;=-]+))?)?(#(?<fragment>/[a-zA-Z0-9._~:@!$&%')(*+,;=\-\[\]/\\]*))?$"##).unwrap()
});
impl<'u> AtUri<'u> {
pub fn new(uri: &'u str) -> Result<Self, AtStrError> {
if let Some(parts) = ATURI_REGEX.captures(uri) {
if let Some(authority) = parts.name("authority") {
let authority = AtIdentifier::new(authority.as_str())
.map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?;
let path = if let Some(collection) = parts.name("collection") {
let collection = Nsid::new(collection.as_str())
.map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?;
let rkey = if let Some(rkey) = parts.name("rkey") {
let rkey =
RecordKey::from(Rkey::new(rkey.as_str()).map_err(|e| {
AtStrError::wrap("at-uri-scheme", uri.to_string(), e)
})?);
Some(rkey)
} else {
None
};
Some(RepoPath { collection, rkey })
} else {
None
};
let fragment = parts.name("fragment").map(|fragment| {
let fragment = CowStr::Borrowed(fragment.as_str());
fragment
});
Ok(AtUri {
inner: InnerBuilder {
uri: CowStr::Borrowed(uri),
authority_builder: |_| authority,
path_builder: |_| path,
fragment_builder: |_| fragment,
}
.build(),
})
} else {
Err(AtStrError::missing("at-uri-scheme", uri, "authority"))
}
} else {
Err(AtStrError::regex(
"at-uri-scheme",
uri,
SmolStr::new_static("doesn't match schema"),
))
}
}
pub fn raw(uri: &'u str) -> Self {
if let Some(parts) = ATURI_REGEX.captures(uri) {
if let Some(authority) = parts.name("authority") {
let authority = AtIdentifier::raw(authority.as_str());
let path = if let Some(collection) = parts.name("collection") {
let collection = Nsid::raw(collection.as_str());
let rkey = if let Some(rkey) = parts.name("rkey") {
let rkey = RecordKey::from(Rkey::raw(rkey.as_str()));
Some(rkey)
} else {
None
};
Some(RepoPath { collection, rkey })
} else {
None
};
let fragment = parts.name("fragment").map(|fragment| {
let fragment = CowStr::Borrowed(fragment.as_str());
fragment
});
AtUri {
inner: InnerBuilder {
uri: CowStr::Borrowed(uri),
authority_builder: |_| authority,
path_builder: |_| path,
fragment_builder: |_| fragment,
}
.build(),
}
} else {
panic!("at:// URI missing authority")
}
} else {
panic!("Invalid at:// URI via regex")
}
}
pub unsafe fn unchecked(uri: &'u str) -> Self {
if let Some(parts) = ATURI_REGEX.captures(uri) {
if let Some(authority) = parts.name("authority") {
let authority = unsafe { AtIdentifier::unchecked(authority.as_str()) };
let path = if let Some(collection) = parts.name("collection") {
let collection = unsafe { Nsid::unchecked(collection.as_str()) };
let rkey = if let Some(rkey) = parts.name("rkey") {
let rkey = RecordKey::from(unsafe { Rkey::unchecked(rkey.as_str()) });
Some(rkey)
} else {
None
};
Some(RepoPath { collection, rkey })
} else {
None
};
let fragment = parts.name("fragment").map(|fragment| {
let fragment = CowStr::Borrowed(fragment.as_str());
fragment
});
AtUri {
inner: InnerBuilder {
uri: CowStr::Borrowed(uri),
authority_builder: |_| authority,
path_builder: |_| path,
fragment_builder: |_| fragment,
}
.build(),
}
} else {
Self {
inner: InnerBuilder {
uri: CowStr::Borrowed(uri),
authority_builder: |_| unsafe { AtIdentifier::unchecked(uri) },
path_builder: |_| None,
fragment_builder: |_| None,
}
.build(),
}
}
} else {
Self {
inner: InnerBuilder {
uri: CowStr::Borrowed(uri),
authority_builder: |_| unsafe { AtIdentifier::unchecked(uri) },
path_builder: |_| None,
fragment_builder: |_| None,
}
.build(),
}
}
}
pub fn fast_clone(&self) -> AtUri<'static> {
self.inner.with(move |u| {
let uri = u.uri.clone().into_static();
let authority = u.authority.clone().into_static();
let path = u.path.clone().into_static();
let fragment = u.fragment.clone().into_static();
AtUri {
inner: InnerBuilder {
uri,
authority_builder: |_| authority,
path_builder: |_| path,
fragment_builder: |_| fragment,
}
.build(),
}
})
}
pub fn as_str(&self) -> &str {
{
let this = &self.inner.borrow_uri();
this
}
}
pub fn authority(&self) -> &AtIdentifier<'_> {
self.inner.borrow_authority()
}
pub fn path(&self) -> &Option<RepoPath<'_>> {
self.inner.borrow_path()
}
pub fn fragment(&self) -> &Option<CowStr<'_>> {
self.inner.borrow_fragment()
}
pub fn collection(&self) -> Option<&Nsid<'_>> {
self.inner.borrow_path().as_ref().map(|p| &p.collection)
}
pub fn rkey(&self) -> Option<&RecordKey<Rkey<'_>>> {
self.inner
.borrow_path()
.as_ref()
.and_then(|p| p.rkey.as_ref())
}
pub fn new_cow(uri: CowStr<'u>) -> Result<Self, AtStrError> {
Self::try_from(uri)
}
}
impl AtUri<'static> {
pub fn from_parts_owned(
authority: impl AsRef<str>,
collection: impl AsRef<str>,
rkey: impl AsRef<str>,
) -> Result<Self, AtStrError> {
let (authority, collection, rkey) =
(authority.as_ref(), collection.as_ref(), rkey.as_ref());
if authority.is_empty() || (collection.is_empty() && !rkey.is_empty()) {
Err(AtStrError::missing(
"at-uri-scheme",
&format!("at://{}/{}/{}", authority, collection, rkey),
"correct uri path",
))
} else if !authority.is_empty() && collection.is_empty() && rkey.is_empty() {
let uri = format!("at://{}", authority);
Self::new_owned(uri)
} else if !collection.is_empty() && rkey.is_empty() {
let uri = format!("at://{}/{}", authority, collection);
Self::new_owned(uri)
} else {
let uri = format!("at://{}/{}/{}", authority, collection, rkey);
Self::new_owned(uri)
}
}
pub fn new_owned(uri: impl AsRef<str>) -> Result<Self, AtStrError> {
if let Some(parts) = ATURI_REGEX.captures(uri.as_ref()) {
if let Some(authority) = parts.name("authority") {
let _authority = AtIdentifier::new(authority.as_str())
.map_err(|e| AtStrError::wrap("at-uri-scheme", uri.as_ref().to_string(), e))?;
let path = if let Some(collection) = parts.name("collection") {
let collection = Nsid::new(collection.as_str()).map_err(|e| {
AtStrError::wrap("at-uri-scheme", uri.as_ref().to_string(), e)
})?;
let rkey = if let Some(rkey) = parts.name("rkey") {
let rkey = RecordKey::from(Rkey::new(rkey.as_str()).map_err(|e| {
AtStrError::wrap("at-uri-scheme", uri.as_ref().to_string(), e)
})?);
Some(rkey)
} else {
None
};
Some(RepoPath { collection, rkey })
} else {
None
};
Ok(AtUri {
inner: Inner::new(
CowStr::Owned(uri.as_ref().to_smolstr()),
|uri| {
let parts = ATURI_REGEX.captures(uri).unwrap();
unsafe {
AtIdentifier::unchecked(parts.name("authority").unwrap().as_str())
}
},
|uri| {
if path.is_some() {
let parts = ATURI_REGEX.captures(uri).unwrap();
if let Some(collection) = parts.name("collection") {
let collection =
unsafe { Nsid::unchecked(collection.as_str()) };
let rkey = if let Some(rkey) = parts.name("rkey") {
let rkey = unsafe {
RecordKey::from(Rkey::unchecked(rkey.as_str()))
};
Some(rkey)
} else {
None
};
Some(RepoPath { collection, rkey })
} else {
None
}
} else {
None
}
},
|uri| {
let parts = ATURI_REGEX.captures(uri).unwrap();
parts.name("fragment").map(|fragment| {
let fragment = CowStr::Borrowed(fragment.as_str());
fragment
})
},
),
})
} else {
Err(AtStrError::missing(
"at-uri-scheme",
&uri.as_ref(),
"authority",
))
}
} else {
Err(AtStrError::regex(
"at-uri-scheme",
&uri.as_ref(),
SmolStr::new_static("doesn't match schema"),
))
}
}
pub fn new_static(uri: &'static str) -> Result<Self, AtStrError> {
let uri = uri.as_ref();
if let Some(parts) = ATURI_REGEX.captures(uri) {
if let Some(authority) = parts.name("authority") {
let authority = AtIdentifier::new_static(authority.as_str())
.map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?;
let path = if let Some(collection) = parts.name("collection") {
let collection = Nsid::new_static(collection.as_str())
.map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?;
let rkey = if let Some(rkey) = parts.name("rkey") {
let rkey =
RecordKey::from(Rkey::new_static(rkey.as_str()).map_err(|e| {
AtStrError::wrap("at-uri-scheme", uri.to_string(), e)
})?);
Some(rkey)
} else {
None
};
Some(RepoPath { collection, rkey })
} else {
None
};
let fragment = parts.name("fragment").map(|fragment| {
let fragment = CowStr::new_static(fragment.as_str());
fragment
});
Ok(AtUri {
inner: InnerBuilder {
uri: CowStr::new_static(uri),
authority_builder: |_| authority,
path_builder: |_| path,
fragment_builder: |_| fragment,
}
.build(),
})
} else {
Err(AtStrError::missing("at-uri-scheme", uri, "authority"))
}
} else {
Err(AtStrError::regex(
"at-uri-scheme",
uri,
SmolStr::new_static("doesn't match schema"),
))
}
}
}
impl FromStr for AtUri<'_> {
type Err = AtStrError;
fn from_str(uri: &str) -> Result<Self, Self::Err> {
if let Some(parts) = ATURI_REGEX.captures(uri.as_ref()) {
if let Some(authority) = parts.name("authority") {
let _authority = AtIdentifier::new(authority.as_str())
.map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?;
let path = if let Some(collection) = parts.name("collection") {
let collection = Nsid::new(collection.as_str())
.map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?;
let rkey = if let Some(rkey) = parts.name("rkey") {
let rkey =
RecordKey::from(Rkey::new(rkey.as_str()).map_err(|e| {
AtStrError::wrap("at-uri-scheme", uri.to_string(), e)
})?);
Some(rkey)
} else {
None
};
Some(RepoPath { collection, rkey })
} else {
None
};
Ok(AtUri {
inner: Inner::new(
CowStr::Owned(uri.to_smolstr()),
|uri| {
let parts = ATURI_REGEX.captures(uri).unwrap();
unsafe {
AtIdentifier::unchecked(parts.name("authority").unwrap().as_str())
}
},
|uri| {
if path.is_some() {
let parts = ATURI_REGEX.captures(uri).unwrap();
if let Some(collection) = parts.name("collection") {
let collection =
unsafe { Nsid::unchecked(collection.as_str()) };
let rkey = if let Some(rkey) = parts.name("rkey") {
let rkey = unsafe {
RecordKey::from(Rkey::unchecked(rkey.as_str()))
};
Some(rkey)
} else {
None
};
Some(RepoPath { collection, rkey })
} else {
None
}
} else {
None
}
},
|uri| {
let parts = ATURI_REGEX.captures(uri).unwrap();
parts.name("fragment").map(|fragment| {
let fragment = CowStr::Borrowed(fragment.as_str());
fragment
})
},
),
})
} else {
Err(AtStrError::missing(
"at-uri-scheme",
&uri.as_ref(),
"authority",
))
}
} else {
Err(AtStrError::regex(
"at-uri-scheme",
&uri.as_ref(),
SmolStr::new_static("doesn't match schema"),
))
}
}
}
impl IntoStatic for AtUri<'_> {
type Output = AtUri<'static>;
fn into_static(self) -> AtUri<'static> {
AtUri {
inner: Inner::new(
self.inner.borrow_uri().clone().into_static(),
|uri| {
let parts = ATURI_REGEX.captures(uri).unwrap();
unsafe { AtIdentifier::unchecked(parts.name("authority").unwrap().as_str()) }
},
|uri| {
if self.inner.borrow_path().is_some() {
let parts = ATURI_REGEX.captures(uri).unwrap();
if let Some(collection) = parts.name("collection") {
let collection = unsafe { Nsid::unchecked(collection.as_str()) };
let rkey = if let Some(rkey) = parts.name("rkey") {
let rkey =
unsafe { RecordKey::from(Rkey::unchecked(rkey.as_str())) };
Some(rkey)
} else {
None
};
Some(RepoPath { collection, rkey })
} else {
None
}
} else {
None
}
},
|uri| {
if self.inner.borrow_fragment().is_some() {
let parts = ATURI_REGEX.captures(uri).unwrap();
parts.name("fragment").map(|fragment| {
let fragment = CowStr::Borrowed(fragment.as_str());
fragment
})
} else {
None
}
},
),
}
}
}
impl<'de, 'a> Deserialize<'de> for AtUri<'a>
where
'de: 'a,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = Deserialize::deserialize(deserializer)?;
Self::new_cow(value).map_err(D::Error::custom)
}
}
impl Serialize for AtUri<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.inner.borrow_uri())
}
}
impl fmt::Display for AtUri<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.inner.borrow_uri())
}
}
impl<'d> From<AtUri<'d>> for String {
fn from(value: AtUri<'d>) -> Self {
value.inner.borrow_uri().to_string()
}
}
impl<'d> From<AtUri<'d>> for CowStr<'d> {
fn from(value: AtUri<'d>) -> Self {
value.inner.borrow_uri().clone()
}
}
impl TryFrom<String> for AtUri<'static> {
type Error = AtStrError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new_owned(&value)
}
}
impl<'d> TryFrom<CowStr<'d>> for AtUri<'d> {
type Error = AtStrError;
fn try_from(uri: CowStr<'d>) -> Result<Self, Self::Error> {
if let Some(parts) = ATURI_REGEX.captures(uri.as_ref()) {
if let Some(authority) = parts.name("authority") {
let _authority = AtIdentifier::new(authority.as_str())
.map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?;
let _path = if let Some(collection) = parts.name("collection") {
let collection = Nsid::new(collection.as_str())
.map_err(|e| AtStrError::wrap("at-uri-scheme", uri.to_string(), e))?;
let rkey = if let Some(rkey) = parts.name("rkey") {
let rkey =
RecordKey::from(Rkey::new(rkey.as_str()).map_err(|e| {
AtStrError::wrap("at-uri-scheme", uri.to_string(), e)
})?);
Some(rkey)
} else {
None
};
Some(RepoPath { collection, rkey })
} else {
None
};
drop(parts);
Ok(AtUri {
inner: Inner::new(
uri,
|uri| {
let parts = ATURI_REGEX.captures(uri).unwrap();
unsafe {
AtIdentifier::unchecked(parts.name("authority").unwrap().as_str())
}
},
|uri| {
let parts = ATURI_REGEX.captures(uri).unwrap();
if let Some(collection) = parts.name("collection") {
let collection = unsafe { Nsid::unchecked(collection.as_str()) };
let rkey = if let Some(rkey) = parts.name("rkey") {
let rkey =
unsafe { RecordKey::from(Rkey::unchecked(rkey.as_str())) };
Some(rkey)
} else {
None
};
Some(RepoPath { collection, rkey })
} else {
None
}
},
|uri| {
let parts = ATURI_REGEX.captures(uri).unwrap();
parts.name("fragment").map(|fragment| {
let fragment = CowStr::Borrowed(fragment.as_str());
fragment
})
},
),
})
} else {
Err(AtStrError::missing(
"at-uri-scheme",
&uri.as_ref(),
"authority",
))
}
} else {
Err(AtStrError::regex(
"at-uri-scheme",
&uri.as_ref(),
SmolStr::new_static("doesn't match schema"),
))
}
}
}
impl AsRef<str> for AtUri<'_> {
fn as_ref(&self) -> &str {
&self.inner.borrow_uri().as_ref()
}
}
impl Deref for AtUri<'_> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.inner.borrow_uri().as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_at_uris() {
assert!(AtUri::new("at://did:plc:foo").is_ok());
assert!(AtUri::new("at://alice.bsky.social").is_ok());
assert!(AtUri::new("at://did:plc:foo/com.example.post").is_ok());
assert!(AtUri::new("at://did:plc:foo/com.example.post/123").is_ok());
}
#[test]
fn authority_only() {
let uri = AtUri::new("at://alice.test").unwrap();
assert_eq!(uri.authority().as_str(), "alice.test");
assert!(uri.collection().is_none());
assert!(uri.rkey().is_none());
}
#[test]
fn authority_and_collection() {
let uri = AtUri::new("at://alice.test/com.example.foo").unwrap();
assert_eq!(uri.authority().as_str(), "alice.test");
assert_eq!(uri.collection().unwrap().as_str(), "com.example.foo");
assert!(uri.rkey().is_none());
}
#[test]
fn full_uri() {
let uri = AtUri::new("at://alice.test/com.example.foo/123").unwrap();
assert_eq!(uri.authority().as_str(), "alice.test");
assert_eq!(uri.collection().unwrap().as_str(), "com.example.foo");
assert_eq!(uri.rkey().unwrap().as_ref(), "123");
}
#[test]
fn with_fragment() {
let uri = AtUri::new("at://alice.test/com.example.foo/123#/path").unwrap();
assert_eq!(uri.fragment().as_ref().unwrap().as_ref(), "/path");
assert!(AtUri::new("at://alice.test#path").is_err());
assert!(AtUri::new("at://alice.test#/foo/bar").is_ok());
}
#[test]
fn no_trailing_slash() {
assert!(AtUri::new("at://alice.test/").is_err());
assert!(AtUri::new("at://alice.test/com.example.foo/").is_err());
}
#[test]
fn must_have_authority() {
assert!(AtUri::new("at://").is_err());
assert!(AtUri::new("at:///com.example.foo").is_err());
}
#[test]
fn must_start_with_at_scheme() {
assert!(AtUri::new("alice.test").is_err());
assert!(AtUri::new("https://alice.test").is_err());
}
#[test]
fn max_length() {
let long_did = format!("did:plc:{}", "a".repeat(8000));
let uri = format!("at://{}", long_did);
assert!(uri.len() < 8192);
}
}