QuickDID is a high-performance AT Protocol identity resolution service written in Rust. It provides handle-to-DID resolution with Redis-backed caching and queue processing.
1//! Core traits for handle resolution.
2//!
3//! This module defines the fundamental `HandleResolver` trait that all resolver
4//! implementations must satisfy.
5
6use super::errors::HandleResolverError;
7use async_trait::async_trait;
8
9/// Core trait for handle-to-DID resolution.
10///
11/// Implementations of this trait provide different strategies for resolving
12/// AT Protocol handles (like `alice.bsky.social`) to their corresponding
13/// DID identifiers (like `did:plc:xyz123`).
14///
15/// # Examples
16///
17/// ```no_run
18/// use async_trait::async_trait;
19/// use quickdid::handle_resolver::{HandleResolver, HandleResolverError};
20/// use std::time::{SystemTime, UNIX_EPOCH};
21///
22/// struct MyResolver;
23///
24/// #[async_trait]
25/// impl HandleResolver for MyResolver {
26/// async fn resolve(&self, s: &str) -> Result<(String, u64), HandleResolverError> {
27/// // Custom resolution logic
28/// let did = format!("did:plc:{}", s.replace('.', ""));
29/// let timestamp = SystemTime::now()
30/// .duration_since(UNIX_EPOCH)
31/// .unwrap()
32/// .as_secs();
33/// Ok((did, timestamp))
34/// }
35/// }
36/// ```
37#[async_trait]
38pub trait HandleResolver: Send + Sync {
39 /// Resolve a handle to its DID with timestamp.
40 ///
41 /// # Arguments
42 ///
43 /// * `s` - The handle to resolve (e.g., "alice.bsky.social")
44 ///
45 /// # Returns
46 ///
47 /// A tuple containing:
48 /// - The resolved DID string
49 /// - The resolution timestamp as UNIX epoch seconds
50 ///
51 /// # Errors
52 ///
53 /// Returns [`HandleResolverError`] if:
54 /// - The handle cannot be resolved
55 /// - Network errors occur during resolution
56 /// - The handle is invalid or doesn't exist
57 async fn resolve(&self, s: &str) -> Result<(String, u64), HandleResolverError>;
58
59 /// Purge a handle or DID from the cache.
60 ///
61 /// This method removes cached entries for the given identifier, which can be
62 /// either a handle (e.g., "alice.bsky.social") or a DID (e.g., "did:plc:xyz123").
63 /// Implementations should handle bidirectional purging where applicable.
64 ///
65 /// # Arguments
66 ///
67 /// * `identifier` - Either a handle or DID to purge from cache
68 ///
69 /// # Returns
70 ///
71 /// Ok(()) if the purge was successful or if the identifier wasn't cached.
72 /// Most implementations will simply return Ok(()) as a no-op.
73 ///
74 /// # Default Implementation
75 ///
76 /// The default implementation is a no-op that always returns Ok(()).
77 async fn purge(&self, _subject: &str) -> Result<(), HandleResolverError> {
78 Ok(())
79 }
80
81 /// Set a handle-to-DID mapping in the cache.
82 ///
83 /// This method allows manually setting or updating a cached mapping between
84 /// a handle and its corresponding DID. This is useful for pre-populating
85 /// caches or updating stale entries.
86 ///
87 /// # Arguments
88 ///
89 /// * `handle` - The handle to cache (e.g., "alice.bsky.social")
90 /// * `did` - The DID to associate with the handle (e.g., "did:plc:xyz123")
91 ///
92 /// # Returns
93 ///
94 /// Ok(()) if the mapping was successfully set or if the implementation
95 /// doesn't support manual cache updates. Most implementations will simply
96 /// return Ok(()) as a no-op.
97 ///
98 /// # Default Implementation
99 ///
100 /// The default implementation is a no-op that always returns Ok(()).
101 async fn set(&self, _handle: &str, _did: &str) -> Result<(), HandleResolverError> {
102 Ok(())
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 // Simple test resolver that doesn't cache anything
111 struct NoOpTestResolver;
112
113 #[async_trait]
114 impl HandleResolver for NoOpTestResolver {
115 async fn resolve(&self, _s: &str) -> Result<(String, u64), HandleResolverError> {
116 Ok(("did:test:123".to_string(), 1234567890))
117 }
118 // Uses default purge implementation
119 }
120
121 #[tokio::test]
122 async fn test_default_purge_implementation() {
123 let resolver = NoOpTestResolver;
124
125 // Default implementation should always return Ok(())
126 assert!(resolver.purge("alice.bsky.social").await.is_ok());
127 assert!(resolver.purge("did:plc:xyz123").await.is_ok());
128 assert!(resolver.purge("").await.is_ok());
129 }
130
131 #[tokio::test]
132 async fn test_default_set_implementation() {
133 let resolver = NoOpTestResolver;
134
135 // Default implementation should always return Ok(())
136 assert!(
137 resolver
138 .set("alice.bsky.social", "did:plc:xyz123")
139 .await
140 .is_ok()
141 );
142 assert!(
143 resolver
144 .set("bob.example.com", "did:web:example.com")
145 .await
146 .is_ok()
147 );
148 assert!(resolver.set("", "").await.is_ok());
149 }
150}