use serde::{Deserialize, Serialize};
use std::{fmt, str::FromStr};
use crate::Document;
use crate::did_method::peer::{PeerCreateKey, PeerCreatedKey, PeerService};
use crate::did_method::{DIDMethod, parse::parse_method};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DID {
method: DIDMethod,
path: Option<String>,
query: Option<String>,
fragment: Option<String>,
url: url::Url,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DIDError {
MissingPrefix,
InvalidMethod(String),
InvalidMethodSpecificId(String),
InvalidPath(String),
InvalidQuery(String),
InvalidFragment(String),
InvalidUrl(String),
ResolutionError(String),
}
impl std::error::Error for DIDError {}
impl fmt::Display for DIDError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DIDError::MissingPrefix => write!(f, "DID must start with 'did:'"),
DIDError::InvalidMethod(m) => write!(f, "Invalid DID method: {m}"),
DIDError::InvalidMethodSpecificId(id) => {
write!(f, "Invalid method-specific ID: {id}")
}
DIDError::InvalidPath(msg) => write!(f, "Invalid path: {msg}"),
DIDError::InvalidQuery(msg) => write!(f, "Invalid query: {msg}"),
DIDError::InvalidFragment(msg) => write!(f, "Invalid fragment: {msg}"),
DIDError::InvalidUrl(msg) => write!(f, "Invalid URL: {msg}"),
DIDError::ResolutionError(msg) => write!(f, "Resolution error: {msg}"),
}
}
}
impl FromStr for DID {
type Err = DIDError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let rest = s.strip_prefix("did:").ok_or(DIDError::MissingPrefix)?;
let (method_name, rest) = rest
.split_once(':')
.ok_or_else(|| DIDError::InvalidMethod("missing method".into()))?;
if method_name.is_empty()
|| !method_name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
{
return Err(DIDError::InvalidMethod(method_name.into()));
}
let components = parse_did_url_components(rest)?;
let method = parse_method(method_name, &components.method_specific_id)?;
DID::build(
method,
components.path,
components.query,
components.fragment,
)
}
}
struct DIDUrlComponents {
method_specific_id: String,
path: Option<String>,
query: Option<String>,
fragment: Option<String>,
}
fn is_unreserved(c: char) -> bool {
c.is_ascii_alphanumeric() || matches!(c, '-' | '.' | '_' | '~')
}
fn is_sub_delims(c: char) -> bool {
matches!(
c,
'!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ';' | '='
)
}
fn is_pchar(c: char) -> bool {
is_unreserved(c) || is_sub_delims(c) || matches!(c, ':' | '@')
}
fn validate_pchar_sequence(s: &str, allow_slash_question: bool) -> Result<(), String> {
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if is_pchar(c) {
continue;
}
if allow_slash_question && matches!(c, '/' | '?') {
continue;
}
if c == '%' {
match (chars.next(), chars.next()) {
(Some(h1), Some(h2)) if h1.is_ascii_hexdigit() && h2.is_ascii_hexdigit() => {
continue;
}
_ => return Err("invalid percent-encoding".into()),
}
}
return Err(format!("invalid character '{c}'"));
}
Ok(())
}
fn validate_path(s: &str) -> Result<(), DIDError> {
for segment in s.split('/') {
validate_pchar_sequence(segment, false).map_err(DIDError::InvalidPath)?;
}
Ok(())
}
fn validate_query(s: &str) -> Result<(), DIDError> {
validate_pchar_sequence(s, true).map_err(DIDError::InvalidQuery)
}
fn validate_fragment(s: &str) -> Result<(), DIDError> {
validate_pchar_sequence(s, true).map_err(DIDError::InvalidFragment)
}
fn none_if_empty(s: String) -> Option<String> {
if s.is_empty() { None } else { Some(s) }
}
fn parse_did_url_components(s: &str) -> Result<DIDUrlComponents, DIDError> {
let path_start = s.find('/');
let query_start = s.find('?');
let fragment_start = s.find('#');
let id_end = [path_start, query_start, fragment_start]
.into_iter()
.flatten()
.min()
.unwrap_or(s.len());
let method_specific_id = s[..id_end].to_string();
let remainder = &s[id_end..];
if remainder.is_empty() {
return Ok(DIDUrlComponents {
method_specific_id,
path: None,
query: None,
fragment: None,
});
}
let mut path = None;
let mut query = None;
let mut fragment = None;
if let Some(stripped) = remainder.strip_prefix('/') {
let end = stripped.find(['?', '#']).unwrap_or(stripped.len());
path = none_if_empty(stripped[..end].to_string());
let remainder = &stripped[end..];
if let Some(stripped) = remainder.strip_prefix('?') {
let end = stripped.find('#').unwrap_or(stripped.len());
query = none_if_empty(stripped[..end].to_string());
if let Some(stripped) = stripped[end..].strip_prefix('#') {
fragment = none_if_empty(stripped.to_string());
}
} else if let Some(stripped) = remainder.strip_prefix('#') {
fragment = none_if_empty(stripped.to_string());
}
} else if let Some(stripped) = remainder.strip_prefix('?') {
let end = stripped.find('#').unwrap_or(stripped.len());
query = none_if_empty(stripped[..end].to_string());
if let Some(frag) = stripped[end..].strip_prefix('#') {
fragment = none_if_empty(frag.to_string());
}
} else if let Some(stripped) = remainder.strip_prefix('#') {
fragment = none_if_empty(stripped.to_string());
}
if let Some(ref p) = path {
validate_path(p)?;
}
if let Some(ref q) = query {
validate_query(q)?;
}
if let Some(ref f) = fragment {
validate_fragment(f)?;
}
Ok(DIDUrlComponents {
method_specific_id,
path,
query,
fragment,
})
}
impl fmt::Display for DID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "did:{}:{}", self.method.name(), self.method.identifier())?;
if let Some(ref path) = self.path {
write!(f, "/{path}")?;
}
if let Some(ref query) = self.query {
write!(f, "?{query}")?;
}
if let Some(ref fragment) = self.fragment {
write!(f, "#{fragment}")?;
}
Ok(())
}
}
impl DID {
pub(crate) fn build(
method: DIDMethod,
path: Option<String>,
query: Option<String>,
fragment: Option<String>,
) -> Result<Self, DIDError> {
let mut did_string = format!("did:{}:{}", method.name(), method.identifier());
if let Some(ref p) = path {
did_string.push('/');
did_string.push_str(p);
}
if let Some(ref q) = query {
did_string.push('?');
did_string.push_str(q);
}
if let Some(ref f) = fragment {
did_string.push('#');
did_string.push_str(f);
}
let url = url::Url::parse(&did_string).map_err(|e| DIDError::InvalidUrl(e.to_string()))?;
Ok(DID {
method,
path,
query,
fragment,
url,
})
}
pub fn new(method: DIDMethod) -> Result<Self, DIDError> {
Self::build(method, None, None, None)
}
pub fn new_key(id: impl Into<String>) -> Result<Self, DIDError> {
let method = parse_method("key", &id.into())?;
Self::build(method, None, None, None)
}
pub fn new_peer(id: impl Into<String>) -> Result<Self, DIDError> {
let method = parse_method("peer", &id.into())?;
Self::build(method, None, None, None)
}
pub fn new_web(id: impl Into<String>) -> Result<Self, DIDError> {
let method = parse_method("web", &id.into())?;
Self::build(method, None, None, None)
}
pub fn new_jwk(id: impl Into<String>) -> Result<Self, DIDError> {
let method = parse_method("jwk", &id.into())?;
Self::build(method, None, None, None)
}
pub fn parse(s: &str) -> Result<Self, DIDError> {
s.parse()
}
pub fn generate_key(
key_type: affinidi_crypto::KeyType,
) -> Result<(Self, crate::KeyMaterial), DIDError> {
use crate::did_method::key::KeyMaterial;
let mut key = KeyMaterial::generate(key_type)
.map_err(|e| DIDError::InvalidMethodSpecificId(e.to_string()))?;
let multibase = key
.public_multibase()
.map_err(|e| DIDError::InvalidMethodSpecificId(e.to_string()))?;
let did_string = format!("did:key:{multibase}");
let did: DID = did_string.parse()?;
key.id = format!("{did_string}#{multibase}");
Ok((did, key))
}
pub fn generate_peer(
keys: &[PeerCreateKey],
services: Option<&[PeerService]>,
) -> Result<(Self, Vec<PeerCreatedKey>), DIDError> {
use crate::did_method::key::KeyMaterial;
use affinidi_crypto::Params;
let mut did_string = String::from("did:peer:2");
let mut created_keys: Vec<PeerCreatedKey> = Vec::new();
for key_spec in keys {
let multibase = if let Some(ref existing) = key_spec.public_key_multibase {
existing.clone()
} else {
let key_type = key_spec.key_type.ok_or_else(|| {
DIDError::InvalidMethodSpecificId(
"Must provide either public_key_multibase or key_type".to_string(),
)
})?;
let key = KeyMaterial::generate(key_type.to_crypto_key_type())
.map_err(|e| DIDError::InvalidMethodSpecificId(e.to_string()))?;
let multibase = key
.public_multibase()
.map_err(|e| DIDError::InvalidMethodSpecificId(e.to_string()))?;
if let crate::did_method::key::KeyMaterialFormat::JWK(jwk) = &key.format {
let (curve, d, x, y) = match &jwk.params {
Params::OKP(params) => (
params.curve.clone(),
params.d.clone().unwrap_or_default(),
params.x.clone(),
None,
),
Params::EC(params) => (
params.curve.clone(),
params.d.clone().unwrap_or_default(),
params.x.clone(),
Some(params.y.clone()),
),
};
created_keys.push(PeerCreatedKey {
key_multibase: multibase.clone(),
curve,
d,
x,
y,
});
}
multibase
};
let purpose_char = key_spec.purpose.to_char();
did_string.push_str(&format!(".{purpose_char}{multibase}"));
}
if let Some(svcs) = services {
for service in svcs {
let encoded = service
.encode()
.map_err(|e| DIDError::InvalidMethodSpecificId(e.to_string()))?;
did_string.push('.');
did_string.push_str(&encoded);
}
}
let did: DID = did_string.parse()?;
Ok((did, created_keys))
}
pub fn resolve(&self) -> Result<Document, DIDError> {
self.method.resolve(self)
}
}
impl DID {
pub fn method(&self) -> DIDMethod {
self.method.clone()
}
pub fn method_specific_id(&self) -> String {
self.method.identifier().to_string()
}
pub fn path(&self) -> Option<String> {
self.path.clone()
}
pub fn query(&self) -> Option<String> {
self.query.clone()
}
pub fn fragment(&self) -> Option<String> {
self.fragment.clone()
}
pub fn is_url(&self) -> bool {
self.path.is_some() || self.query.is_some() || self.fragment.is_some()
}
pub fn url(&self) -> url::Url {
self.url.clone()
}
}
impl DID {
pub fn with_path(self, path: impl Into<String>) -> Result<Self, DIDError> {
let path = path.into();
validate_path(&path)?;
DID::build(self.method, Some(path), self.query, self.fragment)
}
pub fn with_query(self, query: impl Into<String>) -> Result<Self, DIDError> {
let query = query.into();
validate_query(&query)?;
DID::build(self.method, self.path, Some(query), self.fragment)
}
pub fn with_fragment(self, fragment: impl Into<String>) -> Result<Self, DIDError> {
let fragment = fragment.into();
validate_fragment(&fragment)?;
DID::build(self.method, self.path, self.query, Some(fragment))
}
}
impl From<DID> for String {
fn from(did: DID) -> Self {
did.to_string()
}
}
impl TryFrom<String> for DID {
type Error = DIDError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
impl TryFrom<&str> for DID {
type Error = DIDError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
s.parse()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_basic_did() {
let did: DID = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
.parse()
.unwrap();
assert!(matches!(did.method(), DIDMethod::Key { .. }));
assert_eq!(
did.method_specific_id(),
"z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
);
assert!(!did.is_url());
}
#[test]
fn parse_did_peer() {
let did: DID = "did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc"
.parse()
.unwrap();
assert!(matches!(did.method(), DIDMethod::Peer { .. }));
assert!(
did.method_specific_id()
.starts_with("2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc")
);
}
#[test]
fn parse_did_web() {
let did: DID = "did:web:example.com".parse().unwrap();
assert!(matches!(did.method(), DIDMethod::Web { .. }));
assert_eq!(did.method_specific_id(), "example.com");
}
#[test]
fn parse_did_with_fragment() {
let did: DID = "did:example:123#key-1".parse().unwrap();
assert!(matches!(did.method(), DIDMethod::Other { .. }));
assert_eq!(did.method_specific_id(), "123");
assert_eq!(did.fragment(), Some("key-1".to_string()));
assert!(did.is_url());
}
#[test]
fn parse_did_with_path() {
let did: DID = "did:example:123/path/to/resource".parse().unwrap();
assert_eq!(did.path(), Some("path/to/resource".to_string()));
}
#[test]
fn parse_did_with_query() {
let did: DID = "did:example:123?service=files".parse().unwrap();
assert_eq!(did.query(), Some("service=files".to_string()));
}
#[test]
fn parse_full_did_url() {
let did: DID = "did:example:123/path?query=value#fragment".parse().unwrap();
assert_eq!(did.method_specific_id(), "123");
assert_eq!(did.path(), Some("path".to_string()));
assert_eq!(did.query(), Some("query=value".to_string()));
assert_eq!(did.fragment(), Some("fragment".to_string()));
}
#[test]
fn display_roundtrip() {
let original = "did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc";
let did: DID = original.parse().unwrap();
assert_eq!(did.to_string(), original);
}
#[test]
fn display_roundtrip_with_fragment() {
let original = "did:example:123#key-1";
let did: DID = original.parse().unwrap();
assert_eq!(did.to_string(), original);
}
#[test]
fn display_roundtrip_full_url() {
let original = "did:example:123/path?query=value#fragment";
let did: DID = original.parse().unwrap();
assert_eq!(did.to_string(), original);
}
#[test]
fn new_did() {
let did = DID::new_key("z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap();
assert_eq!(
did.to_string(),
"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
);
}
#[test]
fn builder_methods() {
let did = DID::new_peer("2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc")
.unwrap()
.with_fragment("key-1")
.unwrap();
assert_eq!(
did.to_string(),
"did:peer:2.Ez6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc#key-1"
);
}
#[test]
fn error_missing_prefix() {
let result: Result<DID, _> = "not-a-did".parse();
assert_eq!(result.unwrap_err(), DIDError::MissingPrefix);
}
#[test]
fn error_invalid_method() {
let result: Result<DID, _> = "did:UPPER:123".parse();
assert!(matches!(result.unwrap_err(), DIDError::InvalidMethod(_)));
}
#[test]
fn error_empty_method_specific_id() {
let result: Result<DID, _> = "did:example:".parse();
assert!(matches!(
result.unwrap_err(),
DIDError::InvalidMethodSpecificId(_)
));
}
#[test]
fn method_ethr() {
let did: DID = "did:ethr:0x1234567890abcdef".parse().unwrap();
assert!(matches!(did.method(), DIDMethod::Ethr { .. }));
assert_eq!(did.method_specific_id(), "0x1234567890abcdef");
}
#[test]
fn method_pkh() {
let did: DID = "did:pkh:solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ:CKg5d12Jhpej1JqtmxLJgaFqqeYjxgPqToJ4LBdvG9Ev"
.parse()
.unwrap();
match did.method() {
DIDMethod::Pkh {
chain_namespace,
chain_reference,
account_address,
..
} => {
assert_eq!(chain_namespace, "solana");
assert_eq!(chain_reference, "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ");
assert_eq!(
account_address,
"CKg5d12Jhpej1JqtmxLJgaFqqeYjxgPqToJ4LBdvG9Ev"
);
}
_ => panic!("Expected DIDMethod::Pkh"),
}
}
#[test]
fn method_webvh() {
let did: DID = "did:webvh:Qmd1FCL9Vj2vJ433UDfC9MBstK6W6QWSQvYyeNn8va2fai:identity.foundation:didwebvh-implementations:implementations:affinidi-didwebvh-rs"
.parse()
.unwrap();
match did.method() {
DIDMethod::Webvh {
scid,
domain,
path_segments,
..
} => {
assert_eq!(scid, "Qmd1FCL9Vj2vJ433UDfC9MBstK6W6QWSQvYyeNn8va2fai");
assert_eq!(domain, "identity.foundation");
assert_eq!(
path_segments,
vec![
"didwebvh-implementations",
"implementations",
"affinidi-didwebvh-rs"
]
);
}
_ => panic!("Expected DIDMethod::Webvh"),
}
}
#[test]
fn method_cheqd() {
let did: DID = "did:cheqd:testnet:cad53e1d-71e0-48d2-9352-39cc3d0fac99"
.parse()
.unwrap();
match did.method() {
DIDMethod::Cheqd { network, uuid, .. } => {
assert_eq!(network, "testnet");
assert_eq!(uuid, "cad53e1d-71e0-48d2-9352-39cc3d0fac99");
}
_ => panic!("Expected DIDMethod::Cheqd"),
}
}
#[test]
fn method_scid() {
let did: DID = "did:scid:vh:1:Qmd1FCL9Vj2vJ433UDfC9MBstK6W6QWSQvYyeNn8va2fai"
.parse()
.unwrap();
match did.method() {
DIDMethod::Scid {
underlying_method,
version,
scid,
..
} => {
assert_eq!(underlying_method, "vh");
assert_eq!(version, "1");
assert_eq!(scid, "Qmd1FCL9Vj2vJ433UDfC9MBstK6W6QWSQvYyeNn8va2fai");
}
_ => panic!("Expected DIDMethod::Scid"),
}
}
#[test]
fn method_other() {
let did: DID = "did:example:123".parse().unwrap();
assert!(matches!(did.method(), DIDMethod::Other { method, .. } if method == "example"));
}
#[test]
fn colons_in_method_specific_id() {
let did: DID = "did:web:example.com:user:alice".parse().unwrap();
assert_eq!(did.method_specific_id(), "example.com:user:alice");
}
#[test]
fn valid_percent_encoding() {
let did: DID = "did:web:example.com%3A8080".parse().unwrap();
assert_eq!(did.method_specific_id(), "example.com%3A8080");
}
#[test]
fn valid_minimal_did() {
let did: DID = "did:a:b".parse().unwrap();
assert_eq!(did.method().to_string(), "a");
assert_eq!(did.method_specific_id(), "b");
}
#[test]
fn valid_idchars() {
let did: DID = "did:example:ABC-123_test.value".parse().unwrap();
assert_eq!(did.method_specific_id(), "ABC-123_test.value");
}
#[test]
fn error_invalid_character_space() {
let result: Result<DID, _> = "did:example:has space".parse();
assert!(matches!(
result.unwrap_err(),
DIDError::InvalidMethodSpecificId(_)
));
}
#[test]
fn error_invalid_character_at() {
let result: Result<DID, _> = "did:example:user@domain".parse();
assert!(matches!(
result.unwrap_err(),
DIDError::InvalidMethodSpecificId(_)
));
}
#[test]
fn error_trailing_colon() {
let result: Result<DID, _> = "did:example:123:".parse();
assert!(matches!(
result.unwrap_err(),
DIDError::InvalidMethodSpecificId(_)
));
}
#[test]
fn error_invalid_percent_encoding() {
let result: Result<DID, _> = "did:example:test%2".parse();
assert!(matches!(
result.unwrap_err(),
DIDError::InvalidMethodSpecificId(_)
));
}
#[test]
fn error_invalid_percent_encoding_non_hex() {
let result: Result<DID, _> = "did:example:test%GH".parse();
assert!(matches!(
result.unwrap_err(),
DIDError::InvalidMethodSpecificId(_)
));
}
#[test]
fn normalize_empty_fragment() {
let did: DID = "did:example:123#".parse().unwrap();
assert_eq!(did.fragment(), None);
assert!(!did.is_url());
}
#[test]
fn normalize_empty_query() {
let did: DID = "did:example:123?".parse().unwrap();
assert_eq!(did.query(), None);
assert!(!did.is_url());
}
#[test]
fn normalize_empty_path() {
let did: DID = "did:example:123/".parse().unwrap();
assert_eq!(did.path(), None);
assert!(!did.is_url());
}
#[test]
fn normalize_empty_all() {
let did: DID = "did:example:123/?#".parse().unwrap();
assert_eq!(did.path(), None);
assert_eq!(did.query(), None);
assert_eq!(did.fragment(), None);
assert!(!did.is_url());
}
#[test]
fn normalize_mixed_empty_and_present() {
let did: DID = "did:example:123/?query#".parse().unwrap();
assert_eq!(did.path(), None);
assert_eq!(did.query(), Some("query".to_string()));
assert_eq!(did.fragment(), None);
assert!(did.is_url()); }
#[test]
fn valid_method_with_digits() {
let did: DID = "did:web3:0x123".parse().unwrap();
assert!(matches!(did.method(), DIDMethod::Other { method, .. } if method == "web3"));
assert_eq!(did.method_specific_id(), "0x123");
}
#[test]
fn didmethod_serde_roundtrip() {
let method = DIDMethod::Web {
identifier: "example.com".to_string(),
domain: "example.com".to_string(),
path_segments: vec![],
};
let json = serde_json::to_string(&method).unwrap();
let parsed: DIDMethod = serde_json::from_str(&json).unwrap();
assert_eq!(method, parsed);
}
#[test]
fn didmethod_other_serde() {
let method = DIDMethod::Other {
method: "ethr".to_string(),
identifier: "0x123".to_string(),
};
let json = serde_json::to_string(&method).unwrap();
let parsed: DIDMethod = serde_json::from_str(&json).unwrap();
assert_eq!(method, parsed);
}
#[test]
fn valid_path_with_pchar() {
let did: DID = "did:example:123/path-to_resource.txt".parse().unwrap();
assert_eq!(did.path(), Some("path-to_resource.txt".to_string()));
}
#[test]
fn valid_query_with_special_chars() {
let did: DID = "did:example:123?key=value&other=123".parse().unwrap();
assert_eq!(did.query(), Some("key=value&other=123".to_string()));
}
#[test]
fn valid_fragment_with_slash() {
let did: DID = "did:example:123#section/subsection".parse().unwrap();
assert_eq!(did.fragment(), Some("section/subsection".to_string()));
}
#[test]
fn error_invalid_path_char() {
let result: Result<DID, _> = "did:example:123/path<script>".parse();
assert!(matches!(result.unwrap_err(), DIDError::InvalidPath(_)));
}
#[test]
fn error_invalid_query_char() {
let result: Result<DID, _> = "did:example:123?query<script>".parse();
assert!(matches!(result.unwrap_err(), DIDError::InvalidQuery(_)));
}
#[test]
fn error_invalid_fragment_char() {
let result: Result<DID, _> = "did:example:123#frag<script>".parse();
assert!(matches!(result.unwrap_err(), DIDError::InvalidFragment(_)));
}
#[test]
fn error_invalid_path_space() {
let result: Result<DID, _> = "did:example:123/has space".parse();
assert!(matches!(result.unwrap_err(), DIDError::InvalidPath(_)));
}
#[test]
fn builder_validates_path() {
let result = DID::new_key("z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK")
.unwrap()
.with_path("invalid<path>");
assert!(matches!(result.unwrap_err(), DIDError::InvalidPath(_)));
}
#[test]
fn builder_validates_fragment() {
let result = DID::new_key("z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK")
.unwrap()
.with_fragment("invalid<frag>");
assert!(matches!(result.unwrap_err(), DIDError::InvalidFragment(_)));
}
}