identity_resolver/resolution/resolver.rs
1// Copyright 2020-2025 IOTA Stiftung, Fondazione LINKS
2// SPDX-License-Identifier: Apache-2.0
3
4use core::future::Future;
5use futures::stream::FuturesUnordered;
6use futures::TryStreamExt;
7use identity_did::DIDCompositeJwk;
8use identity_did::DIDJwk;
9use identity_did::DID;
10use std::collections::HashSet;
11
12use identity_document::document::CoreDocument;
13use std::collections::HashMap;
14use std::marker::PhantomData;
15
16use crate::Error;
17use crate::ErrorCause;
18use crate::Result;
19
20use super::commands::Command;
21use super::commands::SendSyncCommand;
22use super::commands::SingleThreadedCommand;
23
24/// Convenience type for resolving DID documents from different DID methods.
25///
26/// # Configuration
27///
28/// The resolver will only be able to resolve DID documents for methods it has been configured for. This is done by
29/// attaching method specific handlers with [`Self::attach_handler`](Self::attach_handler()).
30pub struct Resolver<DOC = CoreDocument, CMD = SendSyncCommand<DOC>>
31where
32 CMD: for<'r> Command<'r, Result<DOC>>,
33{
34 command_map: HashMap<String, CMD>,
35 _required: PhantomData<DOC>,
36}
37
38impl<M, DOC> Resolver<DOC, M>
39where
40 M: for<'r> Command<'r, Result<DOC>>,
41{
42 /// Constructs a new [`Resolver`].
43 ///
44 /// # Example
45 ///
46 /// Construct a `Resolver` that resolves DID documents of type
47 /// [`CoreDocument`](::identity_document::document::CoreDocument).
48 /// ```
49 /// # use identity_resolver::Resolver;
50 /// # use identity_document::document::CoreDocument;
51 ///
52 /// let mut resolver = Resolver::<CoreDocument>::new();
53 /// // Now attach some handlers whose output can be converted to a `CoreDocument`.
54 /// ```
55 pub fn new() -> Self {
56 Self {
57 command_map: HashMap::new(),
58 _required: PhantomData::<DOC>,
59 }
60 }
61
62 /// Fetches the DID Document of the given DID.
63 ///
64 /// # Errors
65 ///
66 /// Errors if the resolver has not been configured to handle the method corresponding to the given DID or the
67 /// resolution process itself fails.
68 ///
69 /// ## Example
70 ///
71 /// ```
72 /// # use identity_resolver::Resolver;
73 /// # use identity_did::CoreDID;
74 /// # use identity_document::document::CoreDocument;
75 ///
76 /// async fn configure_and_resolve(
77 /// did: CoreDID,
78 /// ) -> std::result::Result<CoreDocument, Box<dyn std::error::Error>> {
79 /// let resolver: Resolver = configure_resolver(Resolver::new());
80 /// let resolved_doc: CoreDocument = resolver.resolve(&did).await?;
81 /// Ok(resolved_doc)
82 /// }
83 ///
84 /// fn configure_resolver(mut resolver: Resolver) -> Resolver {
85 /// resolver.attach_handler("foo".to_owned(), resolve_foo);
86 /// // Attach handlers for other DID methods we are interested in.
87 /// resolver
88 /// }
89 ///
90 /// async fn resolve_foo(did: CoreDID) -> std::result::Result<CoreDocument, std::io::Error> {
91 /// todo!()
92 /// }
93 /// ```
94 pub async fn resolve<D: DID>(&self, did: &D) -> Result<DOC> {
95 let method: &str = did.method();
96 let delegate: &M = self
97 .command_map
98 .get(method)
99 .ok_or_else(|| ErrorCause::UnsupportedMethodError {
100 method: method.to_owned(),
101 })
102 .map_err(Error::new)?;
103
104 delegate.apply(did.as_str()).await
105 }
106
107 /// Concurrently fetches the DID Documents of the multiple given DIDs.
108 ///
109 /// # Errors
110 /// * If the resolver has not been configured to handle the method of any of the given DIDs.
111 /// * If the resolution process of any DID fails.
112 ///
113 /// ## Note
114 /// * If `dids` contains duplicates, these will be resolved only once.
115 pub async fn resolve_multiple<D: DID>(&self, dids: &[D]) -> Result<HashMap<D, DOC>> {
116 let futures = FuturesUnordered::new();
117
118 // Create set to remove duplicates to avoid unnecessary resolution.
119 let dids_set: HashSet<D> = dids.iter().cloned().collect();
120 for did in dids_set {
121 futures.push(async move {
122 let doc = self.resolve(&did).await;
123 doc.map(|doc| (did, doc))
124 });
125 }
126
127 let documents: HashMap<D, DOC> = futures.try_collect().await?;
128
129 Ok(documents)
130 }
131}
132
133impl<DOC: 'static> Resolver<DOC, SendSyncCommand<DOC>> {
134 /// Attach a new handler responsible for resolving DIDs of the given DID method.
135 ///
136 /// The `handler` is expected to be a closure taking an owned DID and asynchronously returning a DID Document
137 /// which can be converted to the type this [`Resolver`] is parametrized over. The `handler` is required to be
138 /// [`Clone`], [`Send`], [`Sync`] and `'static` hence all captured variables must satisfy these bounds. In this regard
139 /// the `move` keyword and (possibly) wrapping values in an [`Arc`](std::sync::Arc) may come in handy (see the example
140 /// below).
141 ///
142 /// NOTE: If there already exists a handler for this method then it will be replaced with the new handler.
143 /// In the case where one would like to have a "backup handler" for the same DID method, one can achieve this with
144 /// composition.
145 ///
146 /// # Example
147 /// ```
148 /// # use identity_resolver::Resolver;
149 /// # use identity_did::CoreDID;
150 /// # use identity_document::document::CoreDocument;
151 ///
152 /// // A client that can resolve DIDs of our invented "foo" method.
153 /// struct Client;
154 ///
155 /// impl Client {
156 /// // Resolves some of the DIDs we are interested in.
157 /// async fn resolve(&self, _did: &CoreDID) -> std::result::Result<CoreDocument, std::io::Error> {
158 /// todo!()
159 /// }
160 /// }
161 ///
162 /// // This way we can essentially produce (cheap) clones of our client.
163 /// let client = std::sync::Arc::new(Client {});
164 ///
165 /// // Get a clone we can move into a handler.
166 /// let client_clone = client.clone();
167 ///
168 /// // Construct a resolver that resolves documents of type `CoreDocument`.
169 /// let mut resolver = Resolver::<CoreDocument>::new();
170 ///
171 /// // Now we want to attach a handler that uses the client to resolve DIDs whose method is "foo".
172 /// resolver.attach_handler("foo".to_owned(), move |did: CoreDID| {
173 /// // We want to resolve the did asynchronously, but since we do not know when it will be awaited we
174 /// // let the future take ownership of the client by moving a clone into the asynchronous block.
175 /// let future_client = client_clone.clone();
176 /// async move { future_client.resolve(&did).await }
177 /// });
178 /// ```
179 pub fn attach_handler<D, F, Fut, DOCUMENT, E, DIDERR>(&mut self, method: String, handler: F)
180 where
181 D: DID + Send + for<'r> TryFrom<&'r str, Error = DIDERR> + 'static,
182 DOCUMENT: 'static + Into<DOC>,
183 F: Fn(D) -> Fut + 'static + Clone + Send + Sync,
184 Fut: Future<Output = std::result::Result<DOCUMENT, E>> + Send,
185 E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
186 DIDERR: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
187 {
188 let command = SendSyncCommand::new(handler);
189 self.command_map.insert(method, command);
190 }
191}
192
193impl<DOC: 'static> Resolver<DOC, SingleThreadedCommand<DOC>> {
194 /// Attach a new handler responsible for resolving DIDs of the given DID method.
195 ///
196 /// The `handler` is expected to be a closure taking an owned DID and asynchronously returning a DID Document
197 /// which can be converted to the type this [`Resolver`] is parametrized over. The `handler` is required to be
198 /// [`Clone`] and `'static` hence all captured variables must satisfy these bounds. In this regard the
199 /// `move` keyword and (possibly) wrapping values in an [`std::rc::Rc`] may come in handy (see the example below).
200 ///
201 /// NOTE: If there already exists a handler for this method then it will be replaced with the new handler.
202 /// In the case where one would like to have a "backup handler" for the same DID method, one can achieve this with
203 /// composition.
204 ///
205 /// # Example
206 /// ```
207 /// # use identity_resolver::SingleThreadedResolver;
208 /// # use identity_did::CoreDID;
209 /// # use identity_document::document::CoreDocument;
210 ///
211 /// // A client that can resolve DIDs of our invented "foo" method.
212 /// struct Client;
213 ///
214 /// impl Client {
215 /// // Resolves some of the DIDs we are interested in.
216 /// async fn resolve(&self, _did: &CoreDID) -> std::result::Result<CoreDocument, std::io::Error> {
217 /// todo!()
218 /// }
219 /// }
220 ///
221 /// // This way we can essentially produce (cheap) clones of our client.
222 /// let client = std::rc::Rc::new(Client {});
223 ///
224 /// // Get a clone we can move into a handler.
225 /// let client_clone = client.clone();
226 ///
227 /// // Construct a resolver that resolves documents of type `CoreDocument`.
228 /// let mut resolver = SingleThreadedResolver::<CoreDocument>::new();
229 ///
230 /// // Now we want to attach a handler that uses the client to resolve DIDs whose method is "foo".
231 /// resolver.attach_handler("foo".to_owned(), move |did: CoreDID| {
232 /// // We want to resolve the did asynchronously, but since we do not know when it will be awaited we
233 /// // let the future take ownership of the client by moving a clone into the asynchronous block.
234 /// let future_client = client_clone.clone();
235 /// async move { future_client.resolve(&did).await }
236 /// });
237 /// ```
238 pub fn attach_handler<D, F, Fut, DOCUMENT, E, DIDERR>(&mut self, method: String, handler: F)
239 where
240 D: DID + for<'r> TryFrom<&'r str, Error = DIDERR> + 'static,
241 DOCUMENT: 'static + Into<DOC>,
242 F: Fn(D) -> Fut + 'static + Clone,
243 Fut: Future<Output = std::result::Result<DOCUMENT, E>>,
244 E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
245 DIDERR: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
246 {
247 let command = SingleThreadedCommand::new(handler);
248 self.command_map.insert(method, command);
249 }
250}
251
252impl<DOC: From<CoreDocument> + 'static> Resolver<DOC, SingleThreadedCommand<DOC>> {
253 /// Attaches a handler capable of resolving `did:jwk` DIDs.
254 pub fn attach_did_jwk_handler(&mut self) {
255 let handler = |did_jwk: DIDJwk| async move { CoreDocument::expand_did_jwk(did_jwk) };
256 self.attach_handler(DIDJwk::METHOD.to_string(), handler)
257 }
258}
259
260impl<DOC: From<CoreDocument> + 'static> Resolver<DOC, SendSyncCommand<DOC>> {
261 /// Attaches a handler capable of resolving `did:jwk` DIDs.
262 pub fn attach_did_jwk_handler(&mut self) {
263 let handler = |did_jwk: DIDJwk| async move { CoreDocument::expand_did_jwk(did_jwk) };
264 self.attach_handler(DIDJwk::METHOD.to_string(), handler)
265 }
266}
267
268impl<DOC: From<CoreDocument> + 'static> Resolver<DOC, SingleThreadedCommand<DOC>> {
269 /// Attaches a handler capable of resolving `did:compositejwk` DIDs.
270 pub fn attach_did_compositejwk_handler(&mut self) {
271 let handler =
272 |did_compositejwk: DIDCompositeJwk| async move { CoreDocument::expand_did_compositejwk(did_compositejwk) };
273 self.attach_handler(DIDCompositeJwk::METHOD.to_string(), handler)
274 }
275}
276
277impl<DOC: From<CoreDocument> + 'static> Resolver<DOC, SendSyncCommand<DOC>> {
278 /// Attaches a handler capable of resolving `did:compositejwk` DIDs.
279 pub fn attach_did_compositejwk_handler(&mut self) {
280 let handler =
281 |did_compositejwk: DIDCompositeJwk| async move { CoreDocument::expand_did_compositejwk(did_compositejwk) };
282 self.attach_handler(DIDCompositeJwk::METHOD.to_string(), handler)
283 }
284}
285
286#[cfg(all(feature = "iota", not(target_arch = "wasm32")))]
287mod iota_handler {
288 use crate::ErrorCause;
289
290 use super::Resolver;
291 use identity_document::document::CoreDocument;
292 use identity_iota_core::IotaDID;
293 use identity_iota_core::IotaDocument;
294 use std::sync::Arc;
295
296 mod iota_specific {
297 use identity_iota_core::DidResolutionHandler;
298 use std::collections::HashMap;
299
300 use super::*;
301
302 impl<DOC> Resolver<DOC>
303 where
304 DOC: From<IotaDocument> + AsRef<CoreDocument> + 'static,
305 {
306 /// Convenience method for attaching a new handler responsible for resolving IOTA DIDs.
307 ///
308 /// See also [`attach_handler`](Self::attach_handler).
309 pub fn attach_iota_handler<CLI>(&mut self, client: CLI)
310 where
311 CLI: DidResolutionHandler + Send + Sync + 'static,
312 {
313 let arc_client: Arc<CLI> = Arc::new(client);
314
315 let handler = move |did: IotaDID| {
316 let future_client = arc_client.clone();
317 async move { future_client.resolve_did(&did).await }
318 };
319
320 self.attach_handler(IotaDID::METHOD.to_owned(), handler);
321 }
322
323 /// Convenience method for attaching multiple handlers responsible for resolving IOTA DIDs
324 /// on multiple networks.
325 ///
326 ///
327 /// # Arguments
328 ///
329 /// * `clients` - A collection of tuples where each tuple contains the name of the network name and its
330 /// corresponding client.
331 ///
332 /// # Examples
333 ///
334 /// ```ignore
335 /// // Assume `client1` and `client2` are instances of identity clients `IdentityClientReadOnly`.
336 /// attach_multiple_iota_handlers(vec![("client1", client1), ("client2", client2)]);
337 /// ```
338 ///
339 /// # See Also
340 /// - [`attach_handler`](Self::attach_handler).
341 ///
342 /// # Note
343 ///
344 /// - Using `attach_iota_handler` or `attach_handler` for the IOTA method would override all previously added
345 /// clients.
346 /// - This function does not validate the provided configuration. Ensure that the provided network name
347 /// corresponds with the client, possibly by using `client.network_name()`.
348 pub fn attach_multiple_iota_handlers<CLI, I>(&mut self, clients: I)
349 where
350 CLI: DidResolutionHandler + Send + Sync + 'static,
351 I: IntoIterator<Item = (&'static str, CLI)>,
352 {
353 let arc_clients = Arc::new(clients.into_iter().collect::<HashMap<&'static str, CLI>>());
354
355 let handler = move |did: IotaDID| {
356 let future_client = arc_clients.clone();
357 async move {
358 let did_network = did.network_str();
359 let client: &CLI =
360 future_client
361 .get(did_network)
362 .ok_or(crate::Error::new(ErrorCause::UnsupportedNetwork(
363 did_network.to_string(),
364 )))?;
365 client
366 .resolve_did(&did)
367 .await
368 .map_err(|err| crate::Error::new(ErrorCause::HandlerError { source: Box::new(err) }))
369 }
370 };
371
372 self.attach_handler(IotaDID::METHOD.to_owned(), handler);
373 }
374 }
375 }
376}
377
378impl<CMD, DOC> Default for Resolver<DOC, CMD>
379where
380 CMD: for<'r> Command<'r, Result<DOC>>,
381 DOC: AsRef<CoreDocument>,
382{
383 fn default() -> Self {
384 Self::new()
385 }
386}
387
388impl<CMD, DOC> std::fmt::Debug for Resolver<DOC, CMD>
389where
390 CMD: for<'r> Command<'r, Result<DOC>>,
391 DOC: AsRef<CoreDocument>,
392{
393 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
394 f.debug_struct("Resolver")
395 .field("command_map", &self.command_map)
396 .finish()
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use identity_did::CoreDID;
403 use identity_iota_core::DidResolutionHandler;
404 use identity_iota_core::IotaDID;
405 use identity_iota_core::IotaDocument;
406
407 use super::*;
408
409 struct DummyClient(IotaDocument);
410
411 #[async_trait::async_trait]
412 impl DidResolutionHandler for DummyClient {
413 async fn resolve_did(&self, did: &IotaDID) -> identity_iota_core::Result<IotaDocument> {
414 if self.0.id().as_str() == did.as_str() {
415 Ok(self.0.clone())
416 } else {
417 Err(identity_iota_core::Error::DIDResolutionError(
418 "DID not found".to_string(),
419 ))
420 }
421 }
422 }
423
424 #[cfg(feature = "iota")]
425 #[tokio::test]
426 async fn test_multiple_handlers() {
427 let did1 =
428 IotaDID::parse("did:iota:smr:0x0101010101010101010101010101010101010101010101010101010101010101").unwrap();
429 let document = IotaDocument::new_with_id(did1.clone());
430 let dummy_smr_client = DummyClient(document);
431
432 let did2 = IotaDID::parse("did:iota:0x0101010101010101010101010101010101010101010101010101010101010101").unwrap();
433 let document = IotaDocument::new_with_id(did2.clone());
434 let dummy_iota_client = DummyClient(document);
435
436 let mut resolver = Resolver::<IotaDocument>::new();
437 resolver.attach_multiple_iota_handlers(vec![("iota", dummy_iota_client), ("smr", dummy_smr_client)]);
438
439 let doc = resolver.resolve(&did1).await.unwrap();
440 assert_eq!(doc.id(), &did1);
441
442 let doc = resolver.resolve(&did2).await.unwrap();
443 assert_eq!(doc.id(), &did2);
444 }
445
446 #[tokio::test]
447 async fn test_did_jwk_resolution() {
448 let mut resolver = Resolver::<CoreDocument>::new();
449 resolver.attach_did_jwk_handler();
450
451 let did_jwk = "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9".parse::<DIDJwk>().unwrap();
452 let expected_did: &CoreDID = did_jwk.as_ref();
453
454 let doc = resolver.resolve(&did_jwk).await.unwrap();
455 assert_eq!(doc.id(), expected_did);
456 }
457}