ndn_security/did/metadata.rs
1//! W3C DID Core resolution metadata types.
2//!
3//! Per the W3C DID Core specification §7.1, resolving a DID returns three
4//! components: the DID Document itself, document metadata, and resolution
5//! metadata. This module defines the latter two and the wrapper type that
6//! combines all three.
7//!
8//! References:
9//! - <https://www.w3.org/TR/did-core/#did-resolution>
10//! - <https://www.w3.org/TR/did-core/#did-document-metadata>
11//! - <https://www.w3.org/TR/did-core/#did-resolution-metadata>
12
13use serde::{Deserialize, Serialize};
14
15use super::document::DidDocument;
16
17// ── Resolution options ────────────────────────────────────────────────────────
18
19/// Input options for DID resolution, per W3C DID Core §7.1.
20#[derive(Debug, Clone, Default, Serialize, Deserialize)]
21pub struct DidResolutionOptions {
22 /// Desired media type for the DID Document representation.
23 /// Default: `application/did+ld+json`.
24 #[serde(rename = "accept", skip_serializing_if = "Option::is_none")]
25 pub accept: Option<String>,
26}
27
28// ── Document metadata ─────────────────────────────────────────────────────────
29
30/// Metadata about the DID Document itself (not the DID or resolution process).
31///
32/// Per W3C DID Core §7.1.3, this is returned alongside every resolved document.
33/// Values here reflect the *current state* of the document as of resolution.
34#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct DidDocumentMetadata {
37 /// When the DID was first created (RFC 3339 / ISO 8601 datetime string).
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub created: Option<String>,
40
41 /// When the DID Document was last updated (RFC 3339 datetime).
42 /// Absent if the document has never been updated since creation.
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub updated: Option<String>,
45
46 /// `true` if the DID has been deactivated (revoked / succeeded).
47 /// Resolvers MUST include this field when the DID is deactivated.
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub deactivated: Option<bool>,
50
51 /// When the next version of the document is expected (RFC 3339 datetime).
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub next_update: Option<String>,
54
55 /// Version identifier of this specific document revision.
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub version_id: Option<String>,
58
59 /// DIDs that are equivalent to this DID per the resolution method.
60 /// Used to express zone succession: the old DID's metadata lists the
61 /// new zone's DID here.
62 #[serde(skip_serializing_if = "Vec::is_empty", default)]
63 pub equivalent_id: Vec<String>,
64
65 /// The canonical DID for this subject if different from the requested DID.
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub canonical_id: Option<String>,
68}
69
70impl DidDocumentMetadata {
71 /// Construct metadata indicating a deactivated DID.
72 ///
73 /// Used for zone succession: the old zone is deactivated; `successor_did`
74 /// is listed in `equivalent_id` so resolvers can follow the chain.
75 pub fn deactivated_with_successor(successor_did: impl Into<String>) -> Self {
76 Self {
77 deactivated: Some(true),
78 equivalent_id: vec![successor_did.into()],
79 ..Default::default()
80 }
81 }
82}
83
84// ── Resolution error codes ────────────────────────────────────────────────────
85
86/// Standardized DID resolution error codes per W3C DID Core §7.1.2.
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub enum DidResolutionError {
90 /// The DID string is not a valid DID.
91 InvalidDid,
92 /// The DID was not found by the resolver.
93 NotFound,
94 /// The requested representation (content type) is not supported.
95 RepresentationNotSupported,
96 /// The resolver does not support the requested DID method.
97 MethodNotSupported,
98 /// The resolved DID Document is not valid.
99 InvalidDidDocument,
100 /// An unexpected error occurred during resolution.
101 InternalError,
102}
103
104impl std::fmt::Display for DidResolutionError {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 let s = match self {
107 Self::InvalidDid => "invalidDid",
108 Self::NotFound => "notFound",
109 Self::RepresentationNotSupported => "representationNotSupported",
110 Self::MethodNotSupported => "methodNotSupported",
111 Self::InvalidDidDocument => "invalidDidDocument",
112 Self::InternalError => "internalError",
113 };
114 f.write_str(s)
115 }
116}
117
118// ── Resolution metadata ───────────────────────────────────────────────────────
119
120/// Metadata about the DID resolution process itself.
121///
122/// Per W3C DID Core §7.1.2, this is returned alongside every resolution
123/// attempt — including failed ones (where `error` is set).
124#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct DidResolutionMetadata {
127 /// Media type of the returned representation (`application/did+ld+json`).
128 /// MUST be present when resolution succeeds.
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub content_type: Option<String>,
131
132 /// Error code if resolution failed. `None` when resolution succeeded.
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub error: Option<DidResolutionError>,
135
136 /// Human-readable error message (non-normative, for debugging).
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub error_message: Option<String>,
139}
140
141impl DidResolutionMetadata {
142 /// Build metadata for a successful resolution.
143 pub fn success() -> Self {
144 Self {
145 content_type: Some("application/did+ld+json".to_string()),
146 error: None,
147 error_message: None,
148 }
149 }
150
151 /// Build metadata for a failed resolution.
152 pub fn error(code: DidResolutionError, message: impl Into<String>) -> Self {
153 Self {
154 content_type: None,
155 error: Some(code),
156 error_message: Some(message.into()),
157 }
158 }
159}
160
161// ── Resolution result ─────────────────────────────────────────────────────────
162
163/// The complete output of a DID resolution operation.
164///
165/// Per W3C DID Core §7.1, `resolve(did, options)` returns a tuple of:
166/// 1. `didResolutionMetadata` — how the resolution went
167/// 2. `didDocument` — the resolved document (absent on error)
168/// 3. `didDocumentMetadata` — metadata about the document's current state
169///
170/// Use [`DidResolutionResult::into_document()`] to extract the document and
171/// convert resolution errors into a single `Result`.
172#[derive(Debug, Clone)]
173pub struct DidResolutionResult {
174 pub did_document: Option<DidDocument>,
175 pub did_document_metadata: DidDocumentMetadata,
176 pub did_resolution_metadata: DidResolutionMetadata,
177}
178
179impl DidResolutionResult {
180 /// Successful resolution result.
181 pub fn ok(document: DidDocument) -> Self {
182 Self {
183 did_document: Some(document),
184 did_document_metadata: DidDocumentMetadata::default(),
185 did_resolution_metadata: DidResolutionMetadata::success(),
186 }
187 }
188
189 /// Successful resolution with document metadata (e.g. deactivated flag).
190 pub fn ok_with_metadata(document: DidDocument, doc_meta: DidDocumentMetadata) -> Self {
191 Self {
192 did_document: Some(document),
193 did_document_metadata: doc_meta,
194 did_resolution_metadata: DidResolutionMetadata::success(),
195 }
196 }
197
198 /// Failed resolution.
199 pub fn err(code: DidResolutionError, message: impl Into<String>) -> Self {
200 Self {
201 did_document: None,
202 did_document_metadata: DidDocumentMetadata::default(),
203 did_resolution_metadata: DidResolutionMetadata::error(code, message),
204 }
205 }
206
207 /// Extract the DID Document, mapping resolution errors to a [`DidError`].
208 pub fn into_document(self) -> Result<DidDocument, super::resolver::DidError> {
209 if let Some(doc) = self.did_document {
210 return Ok(doc);
211 }
212 match self.did_resolution_metadata.error {
213 Some(DidResolutionError::NotFound) => Err(super::resolver::DidError::NotFound(
214 self.did_resolution_metadata
215 .error_message
216 .unwrap_or_default(),
217 )),
218 Some(DidResolutionError::InvalidDid) => Err(super::resolver::DidError::InvalidDid(
219 self.did_resolution_metadata
220 .error_message
221 .unwrap_or_default(),
222 )),
223 Some(DidResolutionError::MethodNotSupported) => {
224 Err(super::resolver::DidError::UnsupportedMethod(
225 self.did_resolution_metadata
226 .error_message
227 .unwrap_or_default(),
228 ))
229 }
230 _ => Err(super::resolver::DidError::Resolution(
231 self.did_resolution_metadata
232 .error_message
233 .unwrap_or_else(|| "resolution failed".to_string()),
234 )),
235 }
236 }
237
238 /// Whether this DID has been deactivated.
239 pub fn is_deactivated(&self) -> bool {
240 self.did_document_metadata.deactivated.unwrap_or(false)
241 }
242}