use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Did {
canonical: String,
method: DidMethod,
identifier: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum DidMethod {
Plc,
Web,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum DidError {
#[error("not a DID: {0}")]
NotADid(String),
#[error("unsupported DID method: {0}")]
UnsupportedMethod(String),
#[error("invalid DID identifier: {0}")]
InvalidIdentifier(String),
}
impl Did {
pub fn parse(raw: &str) -> Result<Self, DidError> {
let rest = raw
.strip_prefix("did:")
.ok_or_else(|| DidError::NotADid(raw.to_owned()))?;
let (method_str, identifier) = rest
.split_once(':')
.ok_or_else(|| DidError::InvalidIdentifier(raw.to_owned()))?;
if identifier.is_empty() {
return Err(DidError::InvalidIdentifier(raw.to_owned()));
}
let method = match method_str {
"plc" => DidMethod::Plc,
"web" => DidMethod::Web,
other => return Err(DidError::UnsupportedMethod(other.to_owned())),
};
Ok(Self {
canonical: raw.to_owned(),
method,
identifier: identifier.to_owned(),
})
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.canonical
}
#[must_use]
pub const fn method(&self) -> DidMethod {
self.method
}
#[must_use]
pub fn identifier(&self) -> &str {
&self.identifier
}
}
impl fmt::Display for Did {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.canonical)
}
}
impl FromStr for Did {
type Err = DidError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl AsRef<str> for Did {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl std::ops::Deref for Did {
type Target = str;
fn deref(&self) -> &str {
&self.canonical
}
}
impl std::borrow::Borrow<str> for Did {
fn borrow(&self) -> &str {
&self.canonical
}
}
impl Serialize for Did {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.canonical.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Did {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::parse(&s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_plc() {
let d = Did::parse("did:plc:abc123").unwrap();
assert_eq!(d.method(), DidMethod::Plc);
assert_eq!(d.identifier(), "abc123");
assert_eq!(d.as_str(), "did:plc:abc123");
}
#[test]
fn parse_web() {
let d = Did::parse("did:web:example.com").unwrap();
assert_eq!(d.method(), DidMethod::Web);
assert_eq!(d.identifier(), "example.com");
}
#[test]
fn parse_web_with_path() {
let d = Did::parse("did:web:example.com:users:alice").unwrap();
assert_eq!(d.method(), DidMethod::Web);
assert_eq!(d.identifier(), "example.com:users:alice");
}
#[test]
fn reject_non_did() {
assert!(matches!(
Did::parse("https://example"),
Err(DidError::NotADid(_))
));
}
#[test]
fn reject_unsupported_method() {
assert!(matches!(
Did::parse("did:key:abc"),
Err(DidError::UnsupportedMethod(_))
));
}
#[test]
fn reject_empty_identifier() {
assert!(matches!(
Did::parse("did:plc:"),
Err(DidError::InvalidIdentifier(_))
));
}
#[test]
fn fromstr_and_serde_roundtrip() {
let d: Did = "did:plc:abc123".parse().unwrap();
let s = serde_json::to_string(&d).unwrap();
let d2: Did = serde_json::from_str(&s).unwrap();
assert_eq!(d, d2);
}
}