use crate::types::string::AtStrError;
use crate::{CowStr, IntoStatic};
use alloc::string::{String, ToString};
use core::fmt;
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::{Deserialize, Deserializer, Serialize, de::Error};
use smol_str::{SmolStr, ToSmolStr};
use super::Lazy;
#[derive(Clone, PartialEq, Eq, Serialize, Hash, PartialOrd, Ord)]
#[serde(transparent)]
#[repr(transparent)]
pub struct Did<'d>(pub(crate) CowStr<'d>);
pub static DID_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$").unwrap());
impl<'d> Did<'d> {
pub fn new(did: &'d str) -> Result<Self, AtStrError> {
let did = did.strip_prefix("at://").unwrap_or(did);
if did.len() > 2048 {
Err(AtStrError::too_long("did", did, 2048, did.len()))
} else if !DID_REGEX.is_match(did) {
Err(AtStrError::regex(
"did",
did,
SmolStr::new_static("invalid"),
))
} else {
Ok(Self(CowStr::Borrowed(did)))
}
}
pub fn new_cow(did: CowStr<'d>) -> Result<Self, AtStrError> {
let did = if let Some(did) = did.strip_prefix("at://") {
CowStr::copy_from_str(did)
} else {
did
};
if did.len() > 2048 {
Err(AtStrError::too_long("did", &did, 2048, did.len()))
} else if !DID_REGEX.is_match(&did) {
Err(AtStrError::regex(
"did",
&did,
SmolStr::new_static("invalid"),
))
} else {
Ok(Self(did))
}
}
pub fn new_owned(did: impl AsRef<str>) -> Result<Self, AtStrError> {
let did = did.as_ref();
let did = did.strip_prefix("at://").unwrap_or(did);
if did.len() > 2048 {
Err(AtStrError::too_long("did", did, 2048, did.len()))
} else if !DID_REGEX.is_match(did) {
Err(AtStrError::regex(
"did",
did,
SmolStr::new_static("invalid"),
))
} else {
Ok(Self(CowStr::Owned(did.to_smolstr())))
}
}
pub fn new_static(did: &'static str) -> Result<Self, AtStrError> {
let did = did.strip_prefix("at://").unwrap_or(did);
if did.len() > 2048 {
Err(AtStrError::too_long("did", did, 2048, did.len()))
} else if !DID_REGEX.is_match(did) {
Err(AtStrError::regex(
"did",
did,
SmolStr::new_static("invalid"),
))
} else {
Ok(Self(CowStr::new_static(did)))
}
}
pub fn raw(did: &'d str) -> Self {
let did = did.strip_prefix("at://").unwrap_or(did);
if did.len() > 2048 {
panic!("DID too long")
} else if !DID_REGEX.is_match(did) {
panic!("Invalid DID")
} else {
Self(CowStr::Borrowed(did))
}
}
pub unsafe fn unchecked(did: &'d str) -> Self {
Self(CowStr::Borrowed(did))
}
pub fn as_str(&self) -> &str {
{
let this = &self.0;
this
}
}
}
impl FromStr for Did<'_> {
type Err = AtStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new_owned(s)
}
}
impl IntoStatic for Did<'_> {
type Output = Did<'static>;
fn into_static(self) -> Self::Output {
Did(self.0.into_static())
}
}
impl<'de, 'a> Deserialize<'de> for Did<'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 fmt::Display for Did<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl fmt::Debug for Did<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "at://{}", self.0)
}
}
impl<'d> From<Did<'d>> for String {
fn from(value: Did<'d>) -> Self {
value.0.to_string()
}
}
impl<'d> From<Did<'d>> for CowStr<'d> {
fn from(value: Did<'d>) -> Self {
value.0
}
}
impl From<String> for Did<'static> {
fn from(value: String) -> Self {
let value = if let Some(did) = value.strip_prefix("at://") {
CowStr::Borrowed(did)
} else {
value.into()
};
if value.len() > 2048 {
panic!("DID too long")
} else if !DID_REGEX.is_match(&value) {
panic!("Invalid DID")
} else {
Self(value.into_static())
}
}
}
impl<'d> From<CowStr<'d>> for Did<'d> {
fn from(value: CowStr<'d>) -> Self {
let value = if let Some(did) = value.strip_prefix("at://") {
CowStr::Borrowed(did)
} else {
value
};
if value.len() > 2048 {
panic!("DID too long")
} else if !DID_REGEX.is_match(&value) {
panic!("Invalid DID")
} else {
Self(value.into_static())
}
}
}
impl AsRef<str> for Did<'_> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Deref for Did<'_> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_dids() {
assert!(Did::new("did:plc:abc123").is_ok());
assert!(Did::new("did:web:example.com").is_ok());
assert!(Did::new("did:method:val_ue").is_ok());
assert!(Did::new("did:method:val-ue").is_ok());
assert!(Did::new("did:method:val.ue").is_ok());
assert!(Did::new("did:method:val%20ue").is_ok());
}
#[test]
fn prefix_stripping() {
assert_eq!(
Did::new("at://did:plc:foo").unwrap().as_str(),
"did:plc:foo"
);
assert_eq!(Did::new("did:plc:foo").unwrap().as_str(), "did:plc:foo");
}
#[test]
fn must_start_with_did() {
assert!(Did::new("DID:plc:foo").is_err());
assert!(Did::new("plc:foo").is_err());
assert!(Did::new("foo").is_err());
}
#[test]
fn method_must_be_lowercase() {
assert!(Did::new("did:plc:foo").is_ok());
assert!(Did::new("did:PLC:foo").is_err());
assert!(Did::new("did:Plc:foo").is_err());
}
#[test]
fn cannot_end_with_colon_or_percent() {
assert!(Did::new("did:plc:foo:").is_err());
assert!(Did::new("did:plc:foo%").is_err());
assert!(Did::new("did:plc:foo:bar").is_ok());
}
#[test]
fn max_length() {
let valid_2048 = format!("did:plc:{}", "a".repeat(2048 - 8));
assert_eq!(valid_2048.len(), 2048);
assert!(Did::new(&valid_2048).is_ok());
let too_long_2049 = format!("did:plc:{}", "a".repeat(2049 - 8));
assert_eq!(too_long_2049.len(), 2049);
assert!(Did::new(&too_long_2049).is_err());
}
#[test]
fn allowed_characters() {
assert!(Did::new("did:method:abc123").is_ok());
assert!(Did::new("did:method:ABC123").is_ok());
assert!(Did::new("did:method:a_b_c").is_ok());
assert!(Did::new("did:method:a-b-c").is_ok());
assert!(Did::new("did:method:a.b.c").is_ok());
assert!(Did::new("did:method:a:b:c").is_ok());
}
#[test]
fn disallowed_characters() {
assert!(Did::new("did:method:a b").is_err());
assert!(Did::new("did:method:a@b").is_err());
assert!(Did::new("did:method:a#b").is_err());
assert!(Did::new("did:method:a?b").is_err());
}
#[test]
fn percent_encoding() {
assert!(Did::new("did:method:foo%20bar").is_ok());
assert!(Did::new("did:method:foo%2Fbar").is_ok());
assert!(Did::new("did:method:foo%").is_err());
assert!(Did::new("did:method:foo%2x").is_ok());
assert!(Did::new("did:method:foo%ZZ").is_ok());
}
}