Skip to main content

identity_kit/did/
resolution.rs

1//! DID resolution types (DID Core §7.1, W3C DID Resolution): the result of resolving a DID, its
2//! two metadata structures, the resolution options and the error vocabulary.
3
4use core::fmt;
5use core::hash::{Hash, Hasher};
6use core::str::FromStr;
7use std::error::Error;
8use std::sync::Arc;
9
10use indexmap::IndexMap;
11use serde::de::Error as _;
12use serde::{Deserialize, Deserializer, Serialize, Serializer};
13use serde_json::Value;
14
15use super::{Did, DidDocument, DidError};
16
17/// Media type of the plain JSON representation of a DID document (DID Core §6.2).
18pub const APPLICATION_DID_JSON: &str = "application/did+json";
19/// Media type of the JSON-LD representation of a DID document (DID Core §6.3).
20pub const APPLICATION_DID_LD_JSON: &str = "application/did+ld+json";
21
22/// The error keywords of DID resolution (DID Core §7.1.2, DID Resolution).
23///
24/// [`ResolutionError::kind`] returns it; it is what the error serializes as and what callers
25/// match on. A keyword this crate does not know is kept in [`Other`](Self::Other).
26#[derive(Debug, Clone, PartialEq, Eq, Hash)]
27#[non_exhaustive]
28pub enum ResolutionErrorKind {
29    /// `invalidDid`: the DID does not conform to the DID syntax.
30    InvalidDid,
31    /// `notFound`: no DID document exists for the DID.
32    NotFound,
33    /// `methodNotSupported`: the resolver has no implementation of the DID method.
34    MethodNotSupported,
35    /// `representationNotSupported`: the resolver cannot produce the requested media type.
36    RepresentationNotSupported,
37    /// `invalidDidDocument`: the method returned a document that is malformed or not the
38    /// document of the DID (DID Resolution "Errors").
39    InvalidDidDocument,
40    /// `internalError`: the resolver failed unexpectedly (DID Resolution "Errors").
41    InternalError,
42    /// Any other keyword.
43    Other(String),
44}
45
46impl ResolutionErrorKind {
47    /// The registered keyword, for example `notFound`.
48    pub fn as_str(&self) -> &str {
49        match self {
50            Self::InvalidDid => "invalidDid",
51            Self::NotFound => "notFound",
52            Self::MethodNotSupported => "methodNotSupported",
53            Self::RepresentationNotSupported => "representationNotSupported",
54            Self::InvalidDidDocument => "invalidDidDocument",
55            Self::InternalError => "internalError",
56            Self::Other(keyword) => keyword,
57        }
58    }
59}
60
61impl fmt::Display for ResolutionErrorKind {
62    /// A short English description, for example "DID not found".
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        match self {
65            Self::InvalidDid => f.write_str("invalid DID"),
66            Self::NotFound => f.write_str("DID not found"),
67            Self::MethodNotSupported => f.write_str("DID method not supported"),
68            Self::RepresentationNotSupported => f.write_str("representation not supported"),
69            Self::InvalidDidDocument => f.write_str("invalid DID document"),
70            Self::InternalError => f.write_str("internal resolver error"),
71            Self::Other(keyword) => write!(f, "resolution error `{keyword}`"),
72        }
73    }
74}
75
76impl FromStr for ResolutionErrorKind {
77    type Err = core::convert::Infallible;
78
79    /// Never fails: an unknown keyword becomes [`ResolutionErrorKind::Other`].
80    fn from_str(s: &str) -> Result<Self, Self::Err> {
81        Ok(match s {
82            "invalidDid" => Self::InvalidDid,
83            "notFound" => Self::NotFound,
84            "methodNotSupported" => Self::MethodNotSupported,
85            "representationNotSupported" => Self::RepresentationNotSupported,
86            "invalidDidDocument" => Self::InvalidDidDocument,
87            "internalError" => Self::InternalError,
88            other => Self::Other(other.to_owned()),
89        })
90    }
91}
92
93/// Why a DID could not be resolved: a [`ResolutionErrorKind`] keyword, and optionally a
94/// human-readable detail and the underlying error.
95///
96/// The keyword is the error as DID Core defines it: it is what the error serializes as, what
97/// [`ResolutionMetadata::error`] reports, and the only part two errors are compared on. The
98/// detail (the HTTP status of a failed `did:web` fetch, the ids of a mismatched document) and
99/// the source (the HTTP client's error, the JSON or decoding error)
100/// are diagnostics: they appear in [`Display`](fmt::Display) and [`Error::source`], and are
101/// lost when the error is serialized.
102///
103/// # Examples
104///
105/// ```
106/// use std::error::Error as _;
107///
108/// use identity_kit::did::{ResolutionError, ResolutionErrorKind};
109///
110/// let cause = std::io::Error::other("connection refused");
111/// let error = ResolutionError::new(ResolutionErrorKind::InternalError)
112///     .with_detail("fetching `https://example.com/.well-known/did.json` failed")
113///     .with_source(cause);
114///
115/// assert_eq!(error.kind(), &ResolutionErrorKind::InternalError);
116/// assert_eq!(error, ResolutionErrorKind::InternalError);
117/// assert_eq!(error.as_str(), "internalError");
118/// assert_eq!(
119///     error.to_string(),
120///     "internal resolver error: fetching `https://example.com/.well-known/did.json` failed: \
121///      connection refused"
122/// );
123/// assert!(error.source().unwrap().downcast_ref::<std::io::Error>().is_some());
124/// assert_eq!(serde_json::to_string(&error)?, r#""internalError""#);
125/// # Ok::<(), serde_json::Error>(())
126/// ```
127#[derive(Debug, Clone)]
128pub struct ResolutionError {
129    kind: ResolutionErrorKind,
130    detail: Option<String>,
131    source: Option<Arc<dyn Error + Send + Sync + 'static>>,
132}
133
134impl ResolutionError {
135    /// An error reporting `kind`, without detail or source.
136    pub const fn new(kind: ResolutionErrorKind) -> Self {
137        Self {
138            kind,
139            detail: None,
140            source: None,
141        }
142    }
143
144    /// This error with a human-readable `detail`, replacing any previous one.
145    #[must_use]
146    pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
147        self.detail = Some(detail.into());
148        self
149    }
150
151    /// This error caused by `source`, replacing any previous source.
152    #[must_use]
153    pub fn with_source<E: Error + Send + Sync + 'static>(mut self, source: E) -> Self {
154        self.source = Some(Arc::new(source));
155        self
156    }
157
158    /// The keyword.
159    pub const fn kind(&self) -> &ResolutionErrorKind {
160        &self.kind
161    }
162
163    /// The registered keyword, for example `notFound`.
164    pub fn as_str(&self) -> &str {
165        self.kind.as_str()
166    }
167
168    /// The human-readable detail, if any.
169    pub fn detail(&self) -> Option<&str> {
170        self.detail.as_deref()
171    }
172}
173
174impl fmt::Display for ResolutionError {
175    /// The keyword's description, then the detail and the source's message when present, each
176    /// after a colon.
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        fmt::Display::fmt(&self.kind, f)?;
179        if let Some(detail) = &self.detail {
180            write!(f, ": {detail}")?;
181        }
182        if let Some(source) = &self.source {
183            write!(f, ": {source}")?;
184        }
185        Ok(())
186    }
187}
188
189impl Error for ResolutionError {
190    fn source(&self) -> Option<&(dyn Error + 'static)> {
191        self.source
192            .as_deref()
193            .map(|source| source as &(dyn Error + 'static))
194    }
195}
196
197/// Errors are equal when their keywords are; the detail and the source are not compared, so an
198/// error equals its own serialization round trip.
199impl PartialEq for ResolutionError {
200    fn eq(&self, other: &Self) -> bool {
201        self.kind == other.kind
202    }
203}
204
205impl Eq for ResolutionError {}
206
207impl PartialEq<ResolutionErrorKind> for ResolutionError {
208    fn eq(&self, other: &ResolutionErrorKind) -> bool {
209        self.kind == *other
210    }
211}
212
213impl Hash for ResolutionError {
214    fn hash<H: Hasher>(&self, state: &mut H) {
215        self.kind.hash(state);
216    }
217}
218
219impl From<ResolutionErrorKind> for ResolutionError {
220    fn from(kind: ResolutionErrorKind) -> Self {
221        Self::new(kind)
222    }
223}
224
225impl FromStr for ResolutionError {
226    type Err = core::convert::Infallible;
227
228    /// Never fails: an unknown keyword becomes [`ResolutionErrorKind::Other`].
229    fn from_str(s: &str) -> Result<Self, Self::Err> {
230        s.parse().map(Self::new)
231    }
232}
233
234impl From<DidError> for ResolutionError {
235    /// [`ResolutionErrorKind::InvalidDid`], caused by `error`.
236    fn from(error: DidError) -> Self {
237        Self::new(ResolutionErrorKind::InvalidDid).with_source(error)
238    }
239}
240
241impl Serialize for ResolutionError {
242    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
243        serializer.serialize_str(self.as_str())
244    }
245}
246
247impl<'de> Deserialize<'de> for ResolutionError {
248    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
249        let keyword = String::deserialize(deserializer)?;
250        if keyword.is_empty() || !keyword.is_ascii() {
251            return Err(D::Error::custom("error must be an ASCII keyword"));
252        }
253        keyword.parse().map_err(|never| match never {})
254    }
255}
256
257/// Options passed to a resolver (DID Core §7.1.1 "DID resolution options").
258#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
259#[non_exhaustive]
260pub struct ResolutionOptions {
261    /// The media type the caller wants the document in.
262    #[serde(default, skip_serializing_if = "Option::is_none")]
263    pub accept: Option<String>,
264    /// Every other option, in insertion order.
265    #[serde(flatten)]
266    pub properties: IndexMap<String, Value>,
267}
268
269impl ResolutionOptions {
270    /// Options asking for `accept`.
271    pub fn accept(media_type: impl Into<String>) -> Self {
272        Self {
273            accept: Some(media_type.into()),
274            properties: IndexMap::new(),
275        }
276    }
277}
278
279/// Metadata about the resolution process (DID Core §7.1.3 "DID resolution metadata").
280#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
281#[non_exhaustive]
282pub struct ResolutionMetadata {
283    /// The media type of the returned document.
284    #[serde(
285        rename = "contentType",
286        default,
287        skip_serializing_if = "Option::is_none"
288    )]
289    pub content_type: Option<String>,
290    /// Why resolution failed. Only the keyword is serialized; the detail and source of the
291    /// error are kept in memory, for the caller of the resolver.
292    #[serde(default, skip_serializing_if = "Option::is_none")]
293    pub error: Option<ResolutionError>,
294    /// Every other property, in insertion order.
295    #[serde(flatten)]
296    pub properties: IndexMap<String, Value>,
297}
298
299impl ResolutionMetadata {
300    /// Metadata reporting `error`, a [`ResolutionError`] or a bare [`ResolutionErrorKind`].
301    pub fn error(error: impl Into<ResolutionError>) -> Self {
302        Self {
303            content_type: None,
304            error: Some(error.into()),
305            properties: IndexMap::new(),
306        }
307    }
308
309    /// Metadata reporting `content_type`.
310    pub fn content_type(media_type: impl Into<String>) -> Self {
311        Self {
312            content_type: Some(media_type.into()),
313            error: None,
314            properties: IndexMap::new(),
315        }
316    }
317}
318
319/// Metadata about the DID document (DID Core §7.1.3 "DID document metadata").
320///
321/// Timestamps are XML Datetime strings in UTC without fractional seconds, as DID Core requires,
322/// for example `2020-12-20T19:17:47Z`; this layer carries them as strings.
323#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
324#[non_exhaustive]
325pub struct DocumentMetadata {
326    /// When the DID was created.
327    #[serde(default, skip_serializing_if = "Option::is_none")]
328    pub created: Option<String>,
329    /// When the resolved version was written.
330    #[serde(default, skip_serializing_if = "Option::is_none")]
331    pub updated: Option<String>,
332    /// Whether the DID has been deactivated.
333    #[serde(default, skip_serializing_if = "Option::is_none")]
334    pub deactivated: Option<bool>,
335    /// When the next version was written, if the resolved version is not the latest.
336    #[serde(
337        rename = "nextUpdate",
338        default,
339        skip_serializing_if = "Option::is_none"
340    )]
341    pub next_update: Option<String>,
342    /// The version of the resolved document.
343    #[serde(rename = "versionId", default, skip_serializing_if = "Option::is_none")]
344    pub version_id: Option<String>,
345    /// The version after the resolved one, if any.
346    #[serde(
347        rename = "nextVersionId",
348        default,
349        skip_serializing_if = "Option::is_none"
350    )]
351    pub next_version_id: Option<String>,
352    /// DIDs the method defines as logically equivalent to the resolved one.
353    #[serde(
354        rename = "equivalentId",
355        default,
356        skip_serializing_if = "Vec::is_empty"
357    )]
358    pub equivalent_id: Vec<Did>,
359    /// The DID the method defines as canonical for the resolved one.
360    #[serde(
361        rename = "canonicalId",
362        default,
363        skip_serializing_if = "Option::is_none"
364    )]
365    pub canonical_id: Option<Did>,
366    /// Every other property, in insertion order.
367    #[serde(flatten)]
368    pub properties: IndexMap<String, Value>,
369}
370
371impl DocumentMetadata {
372    /// Whether the DID is deactivated.
373    pub fn is_deactivated(&self) -> bool {
374        self.deactivated == Some(true)
375    }
376}
377
378/// What a resolver returns: the document when resolution succeeded, and the two metadata
379/// structures (DID Core §7.1.1).
380///
381/// # Examples
382///
383/// ```
384/// use identity_kit::did::{ResolutionErrorKind, ResolutionResult};
385///
386/// let failed = ResolutionResult::error(ResolutionErrorKind::NotFound);
387/// assert_eq!(
388///     serde_json::to_string(&failed)?,
389///     r#"{"didResolutionMetadata":{"error":"notFound"},"didDocumentMetadata":{}}"#
390/// );
391/// assert_eq!(failed.into_document().unwrap_err(), ResolutionErrorKind::NotFound);
392/// # Ok::<(), serde_json::Error>(())
393/// ```
394#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
395#[non_exhaustive]
396pub struct ResolutionResult {
397    /// The DID document, absent when resolution failed.
398    #[serde(
399        rename = "didDocument",
400        default,
401        skip_serializing_if = "Option::is_none"
402    )]
403    pub document: Option<DidDocument>,
404    /// Metadata about the resolution process.
405    #[serde(rename = "didResolutionMetadata", default)]
406    pub resolution_metadata: ResolutionMetadata,
407    /// Metadata about the document.
408    #[serde(rename = "didDocumentMetadata", default)]
409    pub document_metadata: DocumentMetadata,
410}
411
412impl ResolutionResult {
413    /// A successful result holding `document`.
414    pub fn new(document: DidDocument) -> Self {
415        Self {
416            document: Some(document),
417            resolution_metadata: ResolutionMetadata::default(),
418            document_metadata: DocumentMetadata::default(),
419        }
420    }
421
422    /// A failed result reporting `error`, a [`ResolutionError`] or a bare
423    /// [`ResolutionErrorKind`].
424    pub fn error(error: impl Into<ResolutionError>) -> Self {
425        Self {
426            document: None,
427            resolution_metadata: ResolutionMetadata::error(error),
428            document_metadata: DocumentMetadata::default(),
429        }
430    }
431
432    /// This result with `document_metadata`.
433    #[must_use]
434    pub fn with_document_metadata(mut self, metadata: DocumentMetadata) -> Self {
435        self.document_metadata = metadata;
436        self
437    }
438
439    /// This result with `resolution_metadata`.
440    #[must_use]
441    pub fn with_resolution_metadata(mut self, metadata: ResolutionMetadata) -> Self {
442        self.resolution_metadata = metadata;
443        self
444    }
445
446    /// The keyword of the reported error, if any; the error itself, with its detail and source,
447    /// is in [`resolution_metadata`](Self::resolution_metadata).
448    pub fn error_kind(&self) -> Option<&ResolutionErrorKind> {
449        self.resolution_metadata
450            .error
451            .as_ref()
452            .map(ResolutionError::kind)
453    }
454
455    /// The document, or the reported error; a result with neither counts as
456    /// [`ResolutionErrorKind::NotFound`].
457    ///
458    /// # Errors
459    ///
460    /// The [`ResolutionError`] of the resolution metadata.
461    pub fn into_document(self) -> Result<DidDocument, ResolutionError> {
462        match (self.document, self.resolution_metadata.error) {
463            (_, Some(error)) => Err(error),
464            (Some(document), None) => Ok(document),
465            (None, None) => Err(ResolutionErrorKind::NotFound.into()),
466        }
467    }
468}
469
470impl fmt::Display for ResolutionResult {
471    /// The result as compact JSON, or a placeholder naming the reason when it cannot be
472    /// serialized (a verification method holding a private JWK).
473    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
474        match serde_json::to_string(self) {
475            Ok(json) => f.write_str(&json),
476            Err(e) => write!(f, "<unserializable resolution result: {e}>"),
477        }
478    }
479}