pub mod parse;
#[cfg(feature = "method-web")]
pub mod web;
#[cfg(feature = "method-webplus")]
pub mod webplus;
#[cfg(feature = "method-webs")]
pub mod webs;
#[cfg(feature = "method-webvh")]
pub mod webvh;
pub use parse::parse_did_method;
pub trait DidMethod: Send + Sync + 'static {
fn name(&self) -> &'static str;
fn content_type(&self) -> &'static str;
fn data_ext(&self) -> &'static str;
fn parse_identifier(&self, did: &str) -> Result<ParsedDid, MethodError>;
fn resolution_url(&self, domain: &str, mnemonic: &str) -> String;
fn validate(&self, data: &[u8]) -> Result<(), MethodError>;
fn apply_update(
&self,
existing: Option<&[u8]>,
new_data: &[u8],
) -> Result<Vec<u8>, MethodError>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedDid {
pub method: &'static str,
pub scid: Option<String>,
pub domain: String,
pub path: String,
}
#[derive(Debug, thiserror::Error)]
pub enum MethodError {
#[error("malformed DID identifier: {0}")]
Malformed(String),
#[error("DID method mismatch: expected {expected}, found {found}")]
MethodMismatch {
expected: &'static str,
found: String,
},
#[error("validation failed: {0}")]
Validation(String),
}
#[allow(clippy::needless_return)] pub fn method_by_name(name: &str) -> Option<&'static dyn DidMethod> {
#[cfg(feature = "method-webvh")]
static WEBVH: webvh::Webvh = webvh::Webvh;
#[cfg(feature = "method-web")]
static WEB: web::Web = web::Web;
match name {
#[cfg(feature = "method-webvh")]
"webvh" => Some(&WEBVH),
#[cfg(feature = "method-web")]
"web" => Some(&WEB),
_ => None,
}
}
pub fn enabled_methods() -> &'static [&'static str] {
&[
#[cfg(feature = "method-webvh")]
"webvh",
#[cfg(feature = "method-web")]
"web",
]
}
#[cfg(test)]
mod tests {
use super::*;
struct NoopMethod;
impl DidMethod for NoopMethod {
fn name(&self) -> &'static str {
"noop"
}
fn content_type(&self) -> &'static str {
"application/octet-stream"
}
fn data_ext(&self) -> &'static str {
"bin"
}
fn parse_identifier(&self, _did: &str) -> Result<ParsedDid, MethodError> {
Err(MethodError::Malformed("noop test impl".into()))
}
fn resolution_url(&self, domain: &str, mnemonic: &str) -> String {
format!("https://{domain}/{mnemonic}/data.bin")
}
fn validate(&self, _data: &[u8]) -> Result<(), MethodError> {
Ok(())
}
fn apply_update(
&self,
_existing: Option<&[u8]>,
new_data: &[u8],
) -> Result<Vec<u8>, MethodError> {
Ok(new_data.to_vec())
}
}
static NOOP: NoopMethod = NoopMethod;
#[test]
fn trait_is_object_safe() {
let m: &'static dyn DidMethod = &NOOP;
assert_eq!(m.name(), "noop");
assert_eq!(m.content_type(), "application/octet-stream");
assert_eq!(m.data_ext(), "bin");
}
#[test]
fn resolution_url_composes_method_specific_path() {
let url = NOOP.resolution_url("example.com", "tenant/user1");
assert_eq!(url, "https://example.com/tenant/user1/data.bin");
}
#[test]
fn parse_identifier_can_surface_malformed() {
let err = NOOP
.parse_identifier("not-a-did")
.expect_err("noop impl rejects everything");
assert!(matches!(err, MethodError::Malformed(_)));
}
#[cfg(feature = "method-webvh")]
#[test]
fn dispatcher_routes_webvh() {
let m = method_by_name("webvh").expect("webvh feature enabled in default build");
assert_eq!(m.name(), "webvh");
}
#[test]
fn dispatcher_returns_none_for_unknown() {
assert!(method_by_name("anything-not-a-method").is_none());
}
#[cfg(feature = "method-web")]
#[test]
fn dispatcher_routes_web() {
let m = method_by_name("web").expect("web feature enabled in default build");
assert_eq!(m.name(), "web");
assert_eq!(m.content_type(), "application/did+json");
}
#[cfg(not(feature = "method-web"))]
#[test]
fn dispatcher_returns_none_for_web_when_feature_off() {
assert!(method_by_name("web").is_none());
}
#[cfg(feature = "method-webvh")]
#[test]
fn enabled_methods_contains_webvh() {
assert!(enabled_methods().contains(&"webvh"));
}
#[cfg(feature = "method-web")]
#[test]
fn enabled_methods_contains_web() {
assert!(enabled_methods().contains(&"web"));
}
#[cfg(not(any(feature = "method-webvh", feature = "method-web")))]
#[test]
fn enabled_methods_is_empty_when_no_method_feature() {
assert!(enabled_methods().is_empty());
}
#[test]
fn parsed_did_round_trips_through_debug() {
let p = ParsedDid {
method: "noop",
scid: Some("Q1Hh3jBb2".into()),
domain: "example.com".into(),
path: "tenant:user1".into(),
};
let dbg = format!("{p:?}");
assert!(dbg.contains("noop"));
assert!(dbg.contains("example.com"));
assert!(dbg.contains("tenant:user1"));
}
#[test]
fn method_error_method_mismatch_carries_both_sides() {
let err = MethodError::MethodMismatch {
expected: "web",
found: "webvh".to_string(),
};
let s = err.to_string();
assert!(s.contains("web"));
assert!(s.contains("webvh"));
}
}