1use std::collections::HashMap;
9
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use crate::document::{Document, DocumentMetadata, Service, VerificationMethod};
14use crate::error::Error;
15use crate::{jwk, key, web, DidResolver};
16
17pub async fn resolve(
35 did: &str, opts: Option<Options>, resolver: impl DidResolver,
36) -> crate::Result<Resolved> {
37 let method = did.split(':').nth(1).unwrap_or_default();
39
40 let result = match method {
41 "key" => key::DidKey::resolve(did),
42 "jwk" => jwk::DidJwk::resolve(did, opts, resolver),
43 "web" => web::DidWeb::resolve(did, opts, resolver).await,
44 _ => Err(Error::MethodNotSupported(format!("{method} is not supported"))),
45 };
46
47 if let Err(e) = result {
48 return Ok(Resolved {
49 metadata: Metadata {
50 error: Some(e.to_string()),
51 error_message: Some(e.message()),
52 content_type: ContentType::DidLdJson,
53 ..Metadata::default()
54 },
55 ..Resolved::default()
56 });
57 }
58
59 result
60}
61
62pub async fn dereference(
66 did_url: &str, opts: Option<Options>, resolver: impl DidResolver,
67) -> crate::Result<Dereferenced> {
68 let url = url::Url::parse(did_url)
70 .map_err(|e| Error::InvalidDidUrl(format!("issue parsing URL: {e}")))?;
71 let did = format!("did:{}", url.path());
72
73 let method = did_url.split(':').nth(1).unwrap_or_default();
75 let resolution = match method {
76 "key" => key::DidKey::resolve(&did)?,
77 "web" => web::DidWeb::resolve(&did, opts, resolver).await?,
78 _ => return Err(Error::MethodNotSupported(format!("{method} is not supported"))),
79 };
80
81 let Some(document) = resolution.document else {
82 return Err(Error::InvalidDid("Unable to resolve DID document".into()));
83 };
84
85 let Some(verifcation_methods) = document.verification_method else {
87 return Err(Error::NotFound("verification method missing".into()));
88 };
89
90 let Some(vm) = verifcation_methods.iter().find(|vm| vm.id == did_url) else {
93 return Err(Error::NotFound("verification method not found".into()));
94 };
95
96 Ok(Dereferenced {
97 metadata: Metadata {
98 content_type: ContentType::DidLdJson,
99 ..Metadata::default()
100 },
101 content_stream: Some(Resource::VerificationMethod(vm.clone())),
102 content_metadata: Some(ContentMetadata {
103 document_metadata: resolution.document_metadata,
104 }),
105 })
106}
107
108#[derive(Clone, Debug, Default, Deserialize, Serialize)]
121#[serde(rename_all = "camelCase")]
122pub struct Options {
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub accept: Option<ContentType>,
126
127 #[serde(flatten)]
130 pub additional: Option<HashMap<String, Metadata>>,
131}
132
133#[derive(Clone, Debug, Default, Deserialize, Serialize)]
137#[serde(rename_all = "camelCase")]
138pub struct Parameters {
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub service: Option<String>,
142
143 #[serde(skip_serializing_if = "Option::is_none")]
147 #[serde(alias = "relative-ref")]
148 pub relative_ref: Option<String>,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
153 pub version_id: Option<String>,
154
155 #[serde(skip_serializing_if = "Option::is_none")]
160 pub version_time: Option<String>,
161
162 #[serde(skip_serializing_if = "Option::is_none")]
165 #[serde(rename = "hl")]
166 pub hashlink: Option<String>,
167
168 #[serde(flatten)]
170 pub additional: Option<HashMap<String, Value>>,
171}
172
173#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
175#[serde(rename_all = "camelCase")]
176pub struct Resolved {
177 #[serde(rename = "@context")]
179 pub context: String,
180
181 pub metadata: Metadata,
183
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub document: Option<Document>,
187
188 #[serde(skip_serializing_if = "Option::is_none")]
190 pub document_metadata: Option<DocumentMetadata>,
191}
192
193#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
195#[serde(rename_all = "camelCase")]
196pub struct Dereferenced {
197 pub metadata: Metadata,
201
202 pub content_stream: Option<Resource>,
206
207 pub content_metadata: Option<ContentMetadata>,
211}
212
213#[allow(clippy::large_enum_variant)]
216#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
217pub enum Resource {
218 Document(Document),
220
221 VerificationMethod(VerificationMethod),
223
224 Service(Service),
226}
227
228impl Default for Resource {
229 fn default() -> Self {
230 Self::VerificationMethod(VerificationMethod::default())
231 }
232}
233
234#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
236#[serde(rename_all = "camelCase")]
237pub struct Metadata {
238 pub content_type: ContentType,
240
241 #[serde(skip_serializing_if = "Option::is_none")]
245 pub error: Option<String>,
246
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub error_message: Option<String>,
250
251 #[serde(flatten)]
253 #[serde(skip_serializing_if = "Option::is_none")]
254 pub additional: Option<Value>,
255}
256
257#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
259pub enum ContentType {
260 #[default]
262 #[serde(rename = "application/did+ld+json")]
263 DidLdJson,
264 }
269
270#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
274#[serde(rename_all = "camelCase")]
275pub struct ContentMetadata {
276 #[serde(flatten)]
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub document_metadata: Option<DocumentMetadata>,
280}
281
282#[cfg(test)]
283mod test {
284 use anyhow::anyhow;
285 use insta::assert_json_snapshot as assert_snapshot;
286
287 use super::*;
288
289 #[derive(Clone)]
290 struct MockResolver;
291 impl DidResolver for MockResolver {
292 async fn resolve(&self, _url: &str) -> anyhow::Result<Document> {
293 serde_json::from_slice(include_bytes!("web/did-ecdsa.json"))
294 .map_err(|e| anyhow!("issue deserializing document: {e}"))
295 }
296 }
297
298 #[test]
299 fn error_code() {
300 let err = Error::MethodNotSupported("Method not supported".into());
301 assert_eq!(err.message(), "Method not supported");
302 }
303
304 #[tokio::test]
305 async fn deref_web() {
306 const DID_URL: &str = "did:web:demo.credibil.io#key-0";
307
308 let dereferenced =
309 dereference(DID_URL, None, MockResolver).await.expect("should dereference");
310 assert_snapshot!("deref_web", dereferenced);
311 }
312
313 #[tokio::test]
314 async fn deref_key() {
315 const DID_URL: &str = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
316
317 let dereferenced =
318 dereference(DID_URL, None, MockResolver).await.expect("should dereference");
319 assert_snapshot!("deref_key", dereferenced);
320 }
321}