#![cfg(feature = "method-web")]
use super::{DidMethod, MethodError, ParsedDid};
pub const ROOT_MNEMONIC: &str = "__root";
pub struct Web;
impl DidMethod for Web {
fn name(&self) -> &'static str {
"web"
}
fn content_type(&self) -> &'static str {
"application/did+json"
}
fn data_ext(&self) -> &'static str {
"json"
}
fn parse_identifier(&self, did: &str) -> Result<ParsedDid, MethodError> {
let rest = did
.strip_prefix("did:web:")
.ok_or_else(|| MethodError::MethodMismatch {
expected: "web",
found: super::parse_did_method(did)
.map(|s| s.to_string())
.unwrap_or_else(|_| "<malformed>".into()),
})?;
let mut iter = rest.splitn(2, ':');
let domain = iter
.next()
.filter(|s| !s.is_empty())
.ok_or_else(|| MethodError::Malformed(did.to_string()))?
.to_string();
let path = iter.next().unwrap_or("").to_string();
Ok(ParsedDid {
method: "web",
scid: None, domain,
path,
})
}
fn resolution_url(&self, domain: &str, mnemonic: &str) -> String {
if mnemonic.is_empty() || mnemonic == ROOT_MNEMONIC {
return format!("https://{domain}/.well-known/did.json");
}
let path = mnemonic.replace(':', "/");
format!("https://{domain}/{path}/did.json")
}
fn validate(&self, data: &[u8]) -> Result<(), MethodError> {
let v: serde_json::Value = serde_json::from_slice(data)
.map_err(|e| MethodError::Validation(format!("did.json is not valid JSON: {e}")))?;
let id = v
.get("id")
.and_then(|x| x.as_str())
.ok_or_else(|| MethodError::Validation("did.json missing `id` field".into()))?;
if !id.starts_with("did:web:") {
return Err(MethodError::Validation(format!(
"did.json `id` is not a did:web identifier: '{id}'"
)));
}
Self.parse_identifier(id)?;
Ok(())
}
fn apply_update(
&self,
_existing: Option<&[u8]>,
new_data: &[u8],
) -> Result<Vec<u8>, MethodError> {
if new_data.is_empty() {
return Err(MethodError::Validation(
"web apply_update: new_data must not be empty".into(),
));
}
self.validate(new_data)?;
Ok(new_data.to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn name_and_metadata() {
let m = Web;
assert_eq!(m.name(), "web");
assert_eq!(m.content_type(), "application/did+json");
assert_eq!(m.data_ext(), "json");
}
#[test]
fn parse_identifier_simple() {
let p = Web.parse_identifier("did:web:example.com").unwrap();
assert_eq!(p.method, "web");
assert_eq!(p.scid, None);
assert_eq!(p.domain, "example.com");
assert_eq!(p.path, "");
}
#[test]
fn parse_identifier_with_path() {
let p = Web.parse_identifier("did:web:example.com:user1").unwrap();
assert_eq!(p.domain, "example.com");
assert_eq!(p.path, "user1");
}
#[test]
fn parse_identifier_deep_path() {
let p = Web
.parse_identifier("did:web:example.com:tenants:acme:alice")
.unwrap();
assert_eq!(p.domain, "example.com");
assert_eq!(p.path, "tenants:acme:alice");
}
#[test]
fn parse_identifier_host_with_port_encoded() {
let p = Web
.parse_identifier("did:web:example.com%3A8443:user1")
.unwrap();
assert_eq!(p.domain, "example.com%3A8443");
assert_eq!(p.path, "user1");
}
#[test]
fn parse_identifier_rejects_wrong_method() {
let err = Web
.parse_identifier("did:webvh:Q1:example.com:user1")
.expect_err("did:webvh must reject");
assert!(matches!(
err,
MethodError::MethodMismatch {
expected: "web",
..
}
));
}
#[test]
fn parse_identifier_rejects_missing_domain() {
assert!(Web.parse_identifier("did:web:").is_err());
}
#[test]
fn resolution_url_root_via_well_known_for_empty_mnemonic() {
let url = Web.resolution_url("example.com", "");
assert_eq!(url, "https://example.com/.well-known/did.json");
}
#[test]
fn resolution_url_root_via_well_known_for_root_sentinel() {
let url = Web.resolution_url("example.com", ROOT_MNEMONIC);
assert_eq!(url, "https://example.com/.well-known/did.json");
}
#[test]
fn resolution_url_simple_path() {
let url = Web.resolution_url("example.com", "user1");
assert_eq!(url, "https://example.com/user1/did.json");
}
#[test]
fn resolution_url_deep_path_converts_colons_to_slashes() {
let url = Web.resolution_url("example.com", "tenants:acme:alice");
assert_eq!(url, "https://example.com/tenants/acme/alice/did.json");
}
#[test]
fn validate_accepts_minimal_doc() {
let data = br#"{"id":"did:web:example.com"}"#;
assert!(Web.validate(data).is_ok());
}
#[test]
fn validate_accepts_realistic_doc() {
let data = br#"{
"@context": ["https://www.w3.org/ns/did/v1"],
"id": "did:web:example.com:tenants:acme",
"verificationMethod": [],
"service": []
}"#;
assert!(Web.validate(data).is_ok());
}
#[test]
fn validate_rejects_non_json() {
let err = Web.validate(b"not json").expect_err("must reject");
assert!(matches!(err, MethodError::Validation(_)));
}
#[test]
fn validate_rejects_missing_id() {
let err = Web.validate(br#"{"foo":"bar"}"#).expect_err("must reject");
assert!(matches!(err, MethodError::Validation(_)));
assert!(err.to_string().contains("missing `id` field"));
}
#[test]
fn validate_rejects_id_with_wrong_method() {
let err = Web
.validate(br#"{"id":"did:webvh:Q1:example.com"}"#)
.expect_err("must reject");
assert!(matches!(err, MethodError::Validation(_)));
}
#[test]
fn validate_rejects_id_with_non_did_value() {
let err = Web
.validate(br#"{"id":"https://example.com"}"#)
.expect_err("must reject");
assert!(matches!(err, MethodError::Validation(_)));
}
#[test]
fn apply_update_overwrites_returns_new_bytes() {
let existing = br#"{"id":"did:web:example.com","version":1}"#;
let new_data = br#"{"id":"did:web:example.com","version":2}"#;
let out = Web
.apply_update(Some(existing.as_slice()), new_data)
.unwrap();
assert_eq!(out, new_data);
}
#[test]
fn apply_update_ignores_existing_overwrite_semantics() {
let existing = b"GIBBERISH";
let new_data = br#"{"id":"did:web:example.com"}"#;
let out = Web
.apply_update(Some(existing.as_slice()), new_data)
.unwrap();
assert_eq!(out, new_data);
}
#[test]
fn apply_update_to_empty_existing() {
let new_data = br#"{"id":"did:web:example.com"}"#;
let out = Web.apply_update(None, new_data).unwrap();
assert_eq!(out, new_data);
}
#[test]
fn apply_update_rejects_empty_new_data() {
assert!(Web.apply_update(None, b"").is_err());
}
#[test]
fn apply_update_rejects_malformed_new_data() {
let err = Web
.apply_update(None, b"not json")
.expect_err("must reject");
assert!(matches!(err, MethodError::Validation(_)));
}
#[test]
fn apply_update_rejects_missing_id() {
let err = Web
.apply_update(None, br#"{"foo":"bar"}"#)
.expect_err("must reject");
assert!(matches!(err, MethodError::Validation(_)));
}
}