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}