use std::collections::HashMap;
use serde_json::Value;
use url::Url;
use crate::{
Document, DocumentError,
service::{Endpoint, Service},
verification_method::{VerificationMethod, VerificationRelationship},
};
pub struct DocumentBuilder {
id: Url,
verification_method: Vec<VerificationMethod>,
authentication: Vec<VerificationRelationship>,
assertion_method: Vec<VerificationRelationship>,
key_agreement: Vec<VerificationRelationship>,
capability_invocation: Vec<VerificationRelationship>,
capability_delegation: Vec<VerificationRelationship>,
service: Vec<Service>,
parameters_set: HashMap<String, Value>,
}
impl DocumentBuilder {
pub fn new(id: &str) -> Result<Self, DocumentError> {
Ok(Self::from_url(Url::parse(id)?))
}
pub fn from_url(id: Url) -> Self {
Self {
id,
verification_method: Vec::new(),
authentication: Vec::new(),
assertion_method: Vec::new(),
key_agreement: Vec::new(),
capability_invocation: Vec::new(),
capability_delegation: Vec::new(),
service: Vec::new(),
parameters_set: HashMap::new(),
}
}
pub fn context(mut self, value: Value) -> Self {
self.parameters_set
.insert("@context".to_string(), value);
self
}
fn append_context(mut self, ctx: &str) -> Self {
let entry = self
.parameters_set
.entry("@context".to_string())
.or_insert_with(|| Value::Array(Vec::new()));
if let Value::Array(arr) = entry {
let val = Value::String(ctx.to_string());
if !arr.contains(&val) {
arr.push(val);
}
}
self
}
pub fn context_did_v1(self) -> Self {
self.append_context("https://www.w3.org/ns/did/v1")
}
pub fn context_multikey_v1(self) -> Self {
self.append_context("https://w3id.org/security/multikey/v1")
}
pub fn context_did_v1_1(self) -> Self {
self.append_context("https://www.w3.org/ns/did/v1.1")
}
pub fn verification_method(mut self, vm: VerificationMethod) -> Self {
self.verification_method.push(vm);
self
}
pub fn verification_methods(mut self, vms: Vec<VerificationMethod>) -> Self {
self.verification_method.extend(vms);
self
}
pub fn authentication_reference(mut self, url: &str) -> Result<Self, DocumentError> {
self.authentication
.push(VerificationRelationship::Reference(Url::parse(url)?));
Ok(self)
}
pub fn authentication_embedded(mut self, vm: VerificationMethod) -> Self {
self.authentication
.push(VerificationRelationship::VerificationMethod(Box::new(vm)));
self
}
pub fn authentication(mut self, rel: VerificationRelationship) -> Self {
self.authentication.push(rel);
self
}
pub fn assertion_method_reference(mut self, url: &str) -> Result<Self, DocumentError> {
self.assertion_method
.push(VerificationRelationship::Reference(Url::parse(url)?));
Ok(self)
}
pub fn assertion_method_embedded(mut self, vm: VerificationMethod) -> Self {
self.assertion_method
.push(VerificationRelationship::VerificationMethod(Box::new(vm)));
self
}
pub fn assertion_method(mut self, rel: VerificationRelationship) -> Self {
self.assertion_method.push(rel);
self
}
pub fn key_agreement_reference(mut self, url: &str) -> Result<Self, DocumentError> {
self.key_agreement
.push(VerificationRelationship::Reference(Url::parse(url)?));
Ok(self)
}
pub fn key_agreement_embedded(mut self, vm: VerificationMethod) -> Self {
self.key_agreement
.push(VerificationRelationship::VerificationMethod(Box::new(vm)));
self
}
pub fn key_agreement(mut self, rel: VerificationRelationship) -> Self {
self.key_agreement.push(rel);
self
}
pub fn capability_invocation_reference(mut self, url: &str) -> Result<Self, DocumentError> {
self.capability_invocation
.push(VerificationRelationship::Reference(Url::parse(url)?));
Ok(self)
}
pub fn capability_invocation_embedded(mut self, vm: VerificationMethod) -> Self {
self.capability_invocation
.push(VerificationRelationship::VerificationMethod(Box::new(vm)));
self
}
pub fn capability_invocation(mut self, rel: VerificationRelationship) -> Self {
self.capability_invocation.push(rel);
self
}
pub fn capability_delegation_reference(mut self, url: &str) -> Result<Self, DocumentError> {
self.capability_delegation
.push(VerificationRelationship::Reference(Url::parse(url)?));
Ok(self)
}
pub fn capability_delegation_embedded(mut self, vm: VerificationMethod) -> Self {
self.capability_delegation
.push(VerificationRelationship::VerificationMethod(Box::new(vm)));
self
}
pub fn capability_delegation(mut self, rel: VerificationRelationship) -> Self {
self.capability_delegation.push(rel);
self
}
pub fn service(mut self, svc: Service) -> Self {
self.service.push(svc);
self
}
pub fn services(mut self, svcs: Vec<Service>) -> Self {
self.service.extend(svcs);
self
}
pub fn parameter(mut self, key: impl Into<String>, value: Value) -> Self {
self.parameters_set.insert(key.into(), value);
self
}
pub fn build(self) -> Document {
Document {
id: self.id,
verification_method: self.verification_method,
authentication: self.authentication,
assertion_method: self.assertion_method,
key_agreement: self.key_agreement,
capability_invocation: self.capability_invocation,
capability_delegation: self.capability_delegation,
service: self.service,
parameters_set: self.parameters_set,
}
}
}
pub struct VerificationMethodBuilder {
id: Url,
type_: String,
controller: Url,
expires: Option<String>,
revoked: Option<String>,
property_set: HashMap<String, Value>,
}
impl VerificationMethodBuilder {
pub fn new(id: &str, type_: &str, controller: &str) -> Result<Self, DocumentError> {
Ok(Self::from_urls(
Url::parse(id)?,
type_.to_string(),
Url::parse(controller)?,
))
}
pub fn from_urls(id: Url, type_: String, controller: Url) -> Self {
Self {
id,
type_,
controller,
expires: None,
revoked: None,
property_set: HashMap::new(),
}
}
pub fn expires(mut self, s: impl Into<String>) -> Self {
self.expires = Some(s.into());
self
}
pub fn revoked(mut self, s: impl Into<String>) -> Self {
self.revoked = Some(s.into());
self
}
pub fn property(mut self, key: impl Into<String>, value: Value) -> Self {
self.property_set.insert(key.into(), value);
self
}
pub fn properties(mut self, map: HashMap<String, Value>) -> Self {
self.property_set.extend(map);
self
}
pub fn public_key_multibase(self, s: impl Into<String>) -> Self {
self.property("publicKeyMultibase", Value::String(s.into()))
}
pub fn public_key_jwk(self, value: Value) -> Self {
self.property("publicKeyJwk", value)
}
pub fn build(self) -> VerificationMethod {
VerificationMethod {
id: self.id,
type_: self.type_,
controller: self.controller,
expires: self.expires,
revoked: self.revoked,
property_set: self.property_set,
}
}
}
pub struct ServiceBuilder {
id: Option<Url>,
type_: Vec<String>,
service_endpoint: Endpoint,
property_set: HashMap<String, Value>,
}
impl ServiceBuilder {
pub fn new(type_: impl Into<String>, endpoint: Endpoint) -> Self {
Self {
id: None,
type_: vec![type_.into()],
service_endpoint: endpoint,
property_set: HashMap::new(),
}
}
pub fn new_with_url(
type_: impl Into<String>,
endpoint_url: &str,
) -> Result<Self, DocumentError> {
Ok(Self::new(type_, Endpoint::Url(Url::parse(endpoint_url)?)))
}
pub fn new_with_map(type_: impl Into<String>, endpoint_map: Value) -> Self {
Self::new(type_, Endpoint::Map(endpoint_map))
}
pub fn id(mut self, url: &str) -> Result<Self, DocumentError> {
self.id = Some(Url::parse(url)?);
Ok(self)
}
pub fn id_url(mut self, url: Url) -> Self {
self.id = Some(url);
self
}
pub fn add_type(mut self, type_: impl Into<String>) -> Self {
self.type_.push(type_.into());
self
}
pub fn types(mut self, types: Vec<String>) -> Self {
self.type_ = types;
self
}
pub fn property(mut self, key: impl Into<String>, value: Value) -> Self {
self.property_set.insert(key.into(), value);
self
}
pub fn properties(mut self, map: HashMap<String, Value>) -> Self {
self.property_set.extend(map);
self
}
pub fn build(self) -> Service {
Service {
id: self.id,
type_: self.type_,
service_endpoint: self.service_endpoint,
property_set: self.property_set,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn minimal_document_build() {
let doc = DocumentBuilder::new("did:example:123").unwrap().build();
assert_eq!(doc.id.as_str(), "did:example:123");
assert!(doc.verification_method.is_empty());
assert!(doc.authentication.is_empty());
assert!(doc.assertion_method.is_empty());
assert!(doc.key_agreement.is_empty());
assert!(doc.capability_invocation.is_empty());
assert!(doc.capability_delegation.is_empty());
assert!(doc.service.is_empty());
assert!(doc.parameters_set.is_empty());
}
#[test]
fn invalid_id_returns_error() {
assert!(DocumentBuilder::new("not a url").is_err());
}
#[test]
fn context_convenience_methods() {
let doc = DocumentBuilder::new("did:example:123")
.unwrap()
.context_did_v1()
.context_multikey_v1()
.build();
let ctx = doc.parameters_set.get("@context").unwrap();
let arr = ctx.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0], "https://www.w3.org/ns/did/v1");
assert_eq!(arr[1], "https://w3id.org/security/multikey/v1");
}
#[test]
fn context_deduplication() {
let doc = DocumentBuilder::new("did:example:123")
.unwrap()
.context_did_v1()
.context_did_v1()
.build();
let ctx = doc.parameters_set.get("@context").unwrap();
let arr = ctx.as_array().unwrap();
assert_eq!(arr.len(), 1);
}
#[test]
fn adding_verification_methods() {
let vm1 = VerificationMethodBuilder::new(
"did:example:123#key-1",
"Multikey",
"did:example:123",
)
.unwrap()
.build();
let vm2 = VerificationMethodBuilder::new(
"did:example:123#key-2",
"Multikey",
"did:example:123",
)
.unwrap()
.build();
let vm3 = VerificationMethodBuilder::new(
"did:example:123#key-3",
"Multikey",
"did:example:123",
)
.unwrap()
.build();
let doc = DocumentBuilder::new("did:example:123")
.unwrap()
.verification_method(vm1)
.verification_methods(vec![vm2, vm3])
.build();
assert_eq!(doc.verification_method.len(), 3);
}
#[test]
fn authentication_reference_and_embedded() {
let vm = VerificationMethodBuilder::new(
"did:example:123#key-1",
"Multikey",
"did:example:123",
)
.unwrap()
.build();
let doc = DocumentBuilder::new("did:example:123")
.unwrap()
.authentication_reference("did:example:123#key-1")
.unwrap()
.authentication_embedded(vm)
.build();
assert_eq!(doc.authentication.len(), 2);
assert!(matches!(
doc.authentication[0],
VerificationRelationship::Reference(_)
));
assert!(matches!(
doc.authentication[1],
VerificationRelationship::VerificationMethod(_)
));
}
#[test]
fn assertion_method_reference_and_embedded() {
let vm = VerificationMethodBuilder::new(
"did:example:123#key-1",
"Multikey",
"did:example:123",
)
.unwrap()
.build();
let doc = DocumentBuilder::new("did:example:123")
.unwrap()
.assertion_method_reference("did:example:123#key-1")
.unwrap()
.assertion_method_embedded(vm)
.build();
assert_eq!(doc.assertion_method.len(), 2);
}
#[test]
fn key_agreement_reference_and_embedded() {
let vm = VerificationMethodBuilder::new(
"did:example:123#key-1",
"Multikey",
"did:example:123",
)
.unwrap()
.build();
let doc = DocumentBuilder::new("did:example:123")
.unwrap()
.key_agreement_reference("did:example:123#key-1")
.unwrap()
.key_agreement_embedded(vm)
.build();
assert_eq!(doc.key_agreement.len(), 2);
}
#[test]
fn full_chained_build() {
let vm = VerificationMethodBuilder::new(
"did:example:123#key-1",
"Multikey",
"did:example:123",
)
.unwrap()
.public_key_multibase("z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK")
.build();
let doc = DocumentBuilder::new("did:example:123")
.unwrap()
.context_did_v1()
.context_multikey_v1()
.verification_method(vm)
.authentication_reference("did:example:123#key-1")
.unwrap()
.assertion_method_reference("did:example:123#key-1")
.unwrap()
.key_agreement_reference("did:example:123#key-1")
.unwrap()
.capability_invocation_reference("did:example:123#key-1")
.unwrap()
.capability_delegation_reference("did:example:123#key-1")
.unwrap()
.parameter("custom", json!("value"))
.build();
assert_eq!(doc.id.as_str(), "did:example:123");
assert_eq!(doc.verification_method.len(), 1);
assert_eq!(doc.authentication.len(), 1);
assert_eq!(doc.assertion_method.len(), 1);
assert_eq!(doc.key_agreement.len(), 1);
assert_eq!(doc.capability_invocation.len(), 1);
assert_eq!(doc.capability_delegation.len(), 1);
assert_eq!(
doc.parameters_set.get("custom").unwrap(),
&json!("value")
);
}
#[test]
fn verification_method_builder_minimal() {
let vm = VerificationMethodBuilder::new(
"did:example:123#key-1",
"Multikey",
"did:example:123",
)
.unwrap()
.build();
assert_eq!(vm.id.as_str(), "did:example:123#key-1");
assert_eq!(vm.type_, "Multikey");
assert_eq!(vm.controller.as_str(), "did:example:123");
assert!(vm.expires.is_none());
assert!(vm.revoked.is_none());
assert!(vm.property_set.is_empty());
}
#[test]
fn verification_method_builder_with_properties() {
let mut extra = HashMap::new();
extra.insert("extra".to_string(), json!("data"));
let vm = VerificationMethodBuilder::new(
"did:example:123#key-1",
"Multikey",
"did:example:123",
)
.unwrap()
.public_key_multibase("z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK")
.public_key_jwk(json!({"kty": "OKP"}))
.expires("2025-12-31T00:00:00Z")
.revoked("2025-06-01T00:00:00Z")
.properties(extra)
.build();
assert!(vm.property_set.contains_key("publicKeyMultibase"));
assert!(vm.property_set.contains_key("publicKeyJwk"));
assert!(vm.property_set.contains_key("extra"));
assert_eq!(vm.expires.as_deref(), Some("2025-12-31T00:00:00Z"));
assert_eq!(vm.revoked.as_deref(), Some("2025-06-01T00:00:00Z"));
}
#[test]
fn verification_method_builder_invalid_id() {
assert!(VerificationMethodBuilder::new("not a url", "Multikey", "did:example:123").is_err());
}
#[test]
fn service_builder_minimal_with_url_endpoint() {
let svc = ServiceBuilder::new_with_url(
"LinkedDomains",
"https://example.com",
)
.unwrap()
.build();
assert!(svc.id.is_none());
assert_eq!(svc.type_, vec!["LinkedDomains"]);
assert_eq!(
svc.service_endpoint,
Endpoint::Url(Url::parse("https://example.com").unwrap())
);
assert!(svc.property_set.is_empty());
}
#[test]
fn service_builder_with_map_endpoint() {
let map = json!({"uri": "https://example.com", "accept": ["didcomm/v2"]});
let svc = ServiceBuilder::new_with_map("DIDCommMessaging", map.clone()).build();
assert_eq!(svc.service_endpoint, Endpoint::Map(map));
}
#[test]
fn service_builder_with_id() {
let svc = ServiceBuilder::new_with_url(
"LinkedDomains",
"https://example.com",
)
.unwrap()
.id("did:example:123#linked-domain")
.unwrap()
.build();
assert_eq!(
svc.id.as_ref().unwrap().as_str(),
"did:example:123#linked-domain"
);
}
#[test]
fn service_builder_invalid_id_returns_error() {
let result = ServiceBuilder::new_with_url(
"LinkedDomains",
"https://example.com",
)
.unwrap()
.id("not a url");
assert!(result.is_err());
}
#[test]
fn service_builder_invalid_endpoint_url_returns_error() {
assert!(ServiceBuilder::new_with_url("LinkedDomains", "not a url").is_err());
}
#[test]
fn service_builder_multiple_types() {
let svc = ServiceBuilder::new_with_url(
"LinkedDomains",
"https://example.com",
)
.unwrap()
.add_type("CredentialRepository")
.build();
assert_eq!(svc.type_.len(), 2);
assert_eq!(svc.type_[0], "LinkedDomains");
assert_eq!(svc.type_[1], "CredentialRepository");
}
#[test]
fn service_builder_replace_types() {
let svc = ServiceBuilder::new_with_url(
"LinkedDomains",
"https://example.com",
)
.unwrap()
.types(vec!["TypeA".to_string(), "TypeB".to_string()])
.build();
assert_eq!(svc.type_, vec!["TypeA", "TypeB"]);
}
#[test]
fn service_builder_with_properties() {
let mut extra = HashMap::new();
extra.insert("routingKeys".to_string(), json!(["did:example:123#key-1"]));
let svc = ServiceBuilder::new_with_url(
"DIDCommMessaging",
"https://example.com/didcomm",
)
.unwrap()
.id("did:example:123#didcomm")
.unwrap()
.property("accept", json!(["didcomm/v2"]))
.properties(extra)
.build();
assert!(svc.property_set.contains_key("accept"));
assert!(svc.property_set.contains_key("routingKeys"));
}
#[test]
fn service_builder_integrates_with_document_builder() {
let svc = ServiceBuilder::new_with_url(
"LinkedDomains",
"https://example.com",
)
.unwrap()
.id("did:example:123#linked-domain")
.unwrap()
.build();
let doc = DocumentBuilder::new("did:example:123")
.unwrap()
.service(svc)
.build();
assert_eq!(doc.service.len(), 1);
assert_eq!(doc.service[0].type_, vec!["LinkedDomains"]);
}
}