use crate::{DIDWebVHError, ensure_object_mut, url::WebVHURL};
use serde_json::{Value, json};
pub(crate) fn update_implicit_services(
services: Option<&Value>,
new_state: &mut Value,
did_id: &str,
) -> Result<(), DIDWebVHError> {
let url = WebVHURL::parse_did_url(did_id)?;
let Some(services) = services else {
ensure_object_mut(new_state)?.insert(
"service".to_string(),
Value::Array(vec![
get_service_files(did_id, &url)?,
get_service_whois(did_id, &url)?,
]),
);
return Ok(());
};
if let Some(services) = services.as_array() {
let absolute_whois = format!("{did_id}#whois");
let absolute_files = format!("{did_id}#files");
let mut has_whois = false;
let mut has_files = false;
for service in services {
if let Some(id) = service.get("id").and_then(|v| v.as_str()) {
if id == "#whois" || id == absolute_whois {
has_whois = true;
} else if id == "#files" || id == absolute_files {
has_files = true;
}
}
}
let mut new_services = services.clone();
if !has_files {
new_services.push(get_service_files(did_id, &url)?);
}
if !has_whois {
new_services.push(get_service_whois(did_id, &url)?);
}
ensure_object_mut(new_state)?.insert("service".to_string(), Value::Array(new_services));
} else {
return Err(DIDWebVHError::DIDError(
"services is not an array".to_string(),
));
}
Ok(())
}
fn get_service_whois(did_id: &str, url: &WebVHURL) -> Result<Value, DIDWebVHError> {
Ok(json!({
"@context": "https://identity.foundation/linked-vp/contexts/v1",
"id": format!("{did_id}#whois"),
"type": "LinkedVerifiablePresentation",
"serviceEndpoint": url.get_http_whois_url()?
}))
}
fn get_service_files(did_id: &str, url: &WebVHURL) -> Result<Value, DIDWebVHError> {
let endpoint = url.get_http_files_url()?.to_string();
let endpoint = endpoint.strip_suffix('/').unwrap_or(&endpoint);
Ok(json!({
"id": format!("{did_id}#files"),
"type": "relativeRef",
"serviceEndpoint": endpoint
}))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_no_services_adds_both() {
let mut state = json!({"id": "did:webvh:scid123:example.com"});
update_implicit_services(None, &mut state, "did:webvh:scid123:example.com").unwrap();
let services = state["service"].as_array().unwrap();
assert_eq!(services.len(), 2);
assert_eq!(
services[0]["id"].as_str(),
Some("did:webvh:scid123:example.com#files")
);
assert_eq!(
services[1]["id"].as_str(),
Some("did:webvh:scid123:example.com#whois")
);
}
#[test]
fn test_existing_services_adds_missing() {
let existing = json!([{"id": "did:webvh:scid123:example.com#custom", "type": "Custom", "serviceEndpoint": "https://example.com"}]);
let mut state = json!({"id": "did:webvh:scid123:example.com", "service": existing});
let services_ref = state.get("service").cloned();
update_implicit_services(
services_ref.as_ref(),
&mut state,
"did:webvh:scid123:example.com",
)
.unwrap();
let services = state["service"].as_array().unwrap();
assert_eq!(services.len(), 3); }
#[test]
fn test_both_services_exist_no_change() {
let existing = json!([
{"id": "did:webvh:scid123:example.com#whois", "type": "LinkedVerifiablePresentation", "serviceEndpoint": "https://example.com/whois.vp"},
{"id": "did:webvh:scid123:example.com#files", "type": "relativeRef", "serviceEndpoint": "https://example.com/"}
]);
let mut state = json!({"id": "did:webvh:scid123:example.com", "service": existing});
let services_ref = state.get("service").cloned();
update_implicit_services(
services_ref.as_ref(),
&mut state,
"did:webvh:scid123:example.com",
)
.unwrap();
let services = state["service"].as_array().unwrap();
assert_eq!(services.len(), 2); }
#[test]
fn test_services_not_array_error() {
let services = json!("not-an-array");
let mut state = json!({"id": "did:webvh:scid123:example.com"});
let result =
update_implicit_services(Some(&services), &mut state, "did:webvh:scid123:example.com");
assert!(result.is_err());
}
#[test]
fn test_only_whois_adds_files() {
let existing = json!([
{"id": "did:webvh:scid123:example.com#whois", "type": "LinkedVerifiablePresentation", "serviceEndpoint": "https://example.com/whois.vp"}
]);
let mut state = json!({"id": "did:webvh:scid123:example.com", "service": existing});
let services_ref = state.get("service").cloned();
update_implicit_services(
services_ref.as_ref(),
&mut state,
"did:webvh:scid123:example.com",
)
.unwrap();
let services = state["service"].as_array().unwrap();
assert_eq!(services.len(), 2);
let ids: Vec<&str> = services.iter().map(|s| s["id"].as_str().unwrap()).collect();
assert!(ids.contains(&"did:webvh:scid123:example.com#files"));
}
#[test]
fn test_only_files_adds_whois() {
let existing = json!([
{"id": "did:webvh:scid123:example.com#files", "type": "relativeRef", "serviceEndpoint": "https://example.com/"}
]);
let mut state = json!({"id": "did:webvh:scid123:example.com", "service": existing});
let services_ref = state.get("service").cloned();
update_implicit_services(
services_ref.as_ref(),
&mut state,
"did:webvh:scid123:example.com",
)
.unwrap();
let services = state["service"].as_array().unwrap();
assert_eq!(services.len(), 2);
let ids: Vec<&str> = services.iter().map(|s| s["id"].as_str().unwrap()).collect();
assert!(ids.contains(&"did:webvh:scid123:example.com#whois"));
}
#[test]
fn test_relative_and_absolute_forms_both_recognised() {
for whois_id in ["#whois", "did:webvh:scid123:example.com#whois"] {
for files_id in ["#files", "did:webvh:scid123:example.com#files"] {
let existing = json!([
{"id": whois_id, "type": "LinkedVerifiablePresentation", "serviceEndpoint": "x"},
{"id": files_id, "type": "relativeRef", "serviceEndpoint": "y"}
]);
let mut state = json!({"id": "did:webvh:scid123:example.com", "service": existing});
let services_ref = state.get("service").cloned();
update_implicit_services(
services_ref.as_ref(),
&mut state,
"did:webvh:scid123:example.com",
)
.unwrap();
let services = state["service"].as_array().unwrap();
assert_eq!(
services.len(),
2,
"no injection expected for ({whois_id}, {files_id})"
);
}
}
}
#[test]
fn test_unrelated_whois_suffix_does_not_suppress_injection() {
let existing = json!([
{"id": "did:webvh:OTHER:example.com#whois", "type": "LinkedVerifiablePresentation", "serviceEndpoint": "x"},
{"id": "https://elsewhere.example/#files", "type": "relativeRef", "serviceEndpoint": "y"}
]);
let mut state = json!({"id": "did:webvh:scid123:example.com", "service": existing});
let services_ref = state.get("service").cloned();
update_implicit_services(
services_ref.as_ref(),
&mut state,
"did:webvh:scid123:example.com",
)
.unwrap();
let services = state["service"].as_array().unwrap();
assert_eq!(services.len(), 4);
assert_eq!(
services[2]["id"].as_str(),
Some("did:webvh:scid123:example.com#files")
);
assert_eq!(
services[3]["id"].as_str(),
Some("did:webvh:scid123:example.com#whois")
);
}
#[test]
fn test_files_endpoint_has_no_trailing_slash() {
let mut state = json!({"id": "did:webvh:scid123:example.com"});
update_implicit_services(None, &mut state, "did:webvh:scid123:example.com").unwrap();
let files = &state["service"][0];
assert_eq!(
files["id"].as_str(),
Some("did:webvh:scid123:example.com#files")
);
assert_eq!(
files["serviceEndpoint"].as_str(),
Some("https://example.com")
);
}
#[test]
fn test_path_bearing_did_files_and_whois_endpoints() {
let did = "did:webvh:scid123:example.com:foo:bar";
let mut state = json!({"id": did});
update_implicit_services(None, &mut state, did).unwrap();
let services = state["service"].as_array().unwrap();
assert_eq!(
services[0]["id"].as_str(),
Some("did:webvh:scid123:example.com:foo:bar#files")
);
assert_eq!(
services[0]["serviceEndpoint"].as_str(),
Some("https://example.com/foo/bar")
);
assert_eq!(
services[1]["id"].as_str(),
Some("did:webvh:scid123:example.com:foo:bar#whois")
);
assert_eq!(
services[1]["serviceEndpoint"].as_str(),
Some("https://example.com/foo/bar/whois.vp")
);
}
#[test]
fn test_port_only_did_files_and_whois_endpoints() {
let did = "did:webvh:scid123:example.com%3A8080";
let mut state = json!({"id": did});
update_implicit_services(None, &mut state, did).unwrap();
let services = state["service"].as_array().unwrap();
assert_eq!(
services[0]["serviceEndpoint"].as_str(),
Some("https://example.com:8080")
);
assert_eq!(
services[1]["serviceEndpoint"].as_str(),
Some("https://example.com:8080/whois.vp")
);
}
#[test]
fn test_port_and_path_did_files_and_whois_endpoints() {
let did = "did:webvh:scid123:example.com%3A8080:foo:bar";
let mut state = json!({"id": did});
update_implicit_services(None, &mut state, did).unwrap();
let services = state["service"].as_array().unwrap();
assert_eq!(
services[0]["serviceEndpoint"].as_str(),
Some("https://example.com:8080/foo/bar")
);
assert_eq!(
services[1]["serviceEndpoint"].as_str(),
Some("https://example.com:8080/foo/bar/whois.vp")
);
}
#[test]
fn test_user_services_preserved_implicits_appended() {
let existing = json!([
{"id": "#linked-domain", "type": "LinkedDomains", "serviceEndpoint": "https://example.com"},
{"id": "#messaging", "type": "DIDCommMessaging", "serviceEndpoint": "https://example.com/dc"}
]);
let mut state = json!({"id": "did:webvh:scid123:example.com", "service": existing});
let services_ref = state.get("service").cloned();
update_implicit_services(
services_ref.as_ref(),
&mut state,
"did:webvh:scid123:example.com",
)
.unwrap();
let services = state["service"].as_array().unwrap();
assert_eq!(services.len(), 4);
assert_eq!(services[0]["id"].as_str(), Some("#linked-domain"));
assert_eq!(services[1]["id"].as_str(), Some("#messaging"));
assert_eq!(
services[2]["id"].as_str(),
Some("did:webvh:scid123:example.com#files")
);
assert_eq!(
services[3]["id"].as_str(),
Some("did:webvh:scid123:example.com#whois")
);
}
}