+12
src/config.rs
+12
src/config.rs
···
266
266
/// use quickdid::config::{Args, Config};
267
267
/// use clap::Parser;
268
268
///
269
+
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
269
270
/// let args = Args::parse();
270
271
/// let config = Config::from_args(args)?;
271
272
/// config.validate()?;
272
273
///
273
274
/// println!("Service running at: {}", config.http_external);
274
275
/// println!("Service DID: {}", config.service_did);
276
+
/// # Ok(())
277
+
/// # }
275
278
/// ```
276
279
#[derive(Clone)]
277
280
pub struct Config {
···
349
352
/// use quickdid::config::{Args, Config};
350
353
/// use clap::Parser;
351
354
///
355
+
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
352
356
/// // Parse from environment and command-line
353
357
/// let args = Args::parse();
354
358
/// let config = Config::from_args(args)?;
355
359
///
356
360
/// // The service DID is automatically generated from HTTP_EXTERNAL
357
361
/// assert!(config.service_did.starts_with("did:web:"));
362
+
/// # Ok(())
363
+
/// # }
358
364
/// ```
359
365
///
360
366
/// ## Errors
···
477
483
/// ## Example
478
484
///
479
485
/// ```rust,no_run
486
+
/// # use quickdid::config::{Args, Config};
487
+
/// # use clap::Parser;
488
+
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
489
+
/// # let args = Args::parse();
480
490
/// let config = Config::from_args(args)?;
481
491
/// config.validate()?; // Ensures all values are valid
492
+
/// # Ok(())
493
+
/// # }
482
494
/// ```
483
495
///
484
496
/// ## Errors
+57
-151
src/handle_resolver.rs
src/handle_resolver/redis.rs
+57
-151
src/handle_resolver.rs
src/handle_resolver/redis.rs
···
1
+
//! Redis-backed caching handle resolver.
2
+
//!
3
+
//! This module provides a handle resolver that caches resolution results in Redis
4
+
//! with configurable expiration times. Redis caching provides persistence across
5
+
//! service restarts and allows sharing of cached results across multiple instances.
6
+
7
+
use super::errors::HandleResolverError;
8
+
use super::traits::HandleResolver;
1
9
use crate::handle_resolution_result::HandleResolutionResult;
2
10
use async_trait::async_trait;
3
-
use atproto_identity::resolve::{DnsResolver, resolve_subject};
4
-
use chrono::Utc;
5
11
use deadpool_redis::{Pool as RedisPool, redis::AsyncCommands};
6
12
use metrohash::MetroHash64;
7
-
use reqwest::Client;
8
-
use std::collections::HashMap;
9
13
use std::hash::Hasher as _;
10
14
use std::sync::Arc;
11
-
use thiserror::Error;
12
-
use tokio::sync::RwLock;
13
-
14
-
/// Errors that can occur during handle resolution
15
-
#[derive(Error, Debug)]
16
-
pub enum HandleResolverError {
17
-
#[error("error-quickdid-resolve-1 Failed to resolve subject: {0}")]
18
-
ResolutionFailed(String),
19
-
20
-
#[error("error-quickdid-resolve-2 Handle not found (cached): {0}")]
21
-
HandleNotFoundCached(String),
22
-
23
-
#[error("error-quickdid-resolve-3 Handle not found (cached)")]
24
-
HandleNotFound,
25
15
26
-
#[error("error-quickdid-resolve-4 Mock resolution failure")]
27
-
MockResolutionFailure,
28
-
}
29
-
30
-
#[async_trait]
31
-
pub trait HandleResolver: Send + Sync {
32
-
async fn resolve(&self, s: &str) -> Result<String, HandleResolverError>;
33
-
}
34
-
35
-
pub struct BaseHandleResolver {
36
-
/// DNS resolver for handle-to-DID resolution via TXT records.
37
-
pub dns_resolver: Arc<dyn DnsResolver>,
38
-
/// HTTP client for DID document retrieval and well-known endpoint queries.
39
-
pub http_client: Client,
40
-
/// Hostname of the PLC directory server for `did:plc` resolution.
41
-
pub plc_hostname: String,
42
-
}
43
-
44
-
#[async_trait]
45
-
impl HandleResolver for BaseHandleResolver {
46
-
async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> {
47
-
resolve_subject(&self.http_client, &*self.dns_resolver, s)
48
-
.await
49
-
.map_err(|e| HandleResolverError::ResolutionFailed(e.to_string()))
50
-
}
51
-
}
52
-
53
-
#[derive(Clone, Debug)]
54
-
pub enum ResolveHandleResult {
55
-
Found(u64, String),
56
-
NotFound(u64, String),
57
-
}
58
-
59
-
pub struct CachingHandleResolver {
60
-
inner: Arc<dyn HandleResolver>,
61
-
cache: Arc<RwLock<HashMap<String, ResolveHandleResult>>>,
62
-
ttl_seconds: u64,
63
-
}
64
-
65
-
impl CachingHandleResolver {
66
-
pub fn new(inner: Arc<dyn HandleResolver>, ttl_seconds: u64) -> Self {
67
-
Self {
68
-
inner,
69
-
cache: Arc::new(RwLock::new(HashMap::new())),
70
-
ttl_seconds,
71
-
}
72
-
}
73
-
74
-
fn current_timestamp() -> u64 {
75
-
Utc::now().timestamp() as u64
76
-
}
77
-
78
-
fn is_expired(&self, timestamp: u64) -> bool {
79
-
let current = Self::current_timestamp();
80
-
current > timestamp && (current - timestamp) > self.ttl_seconds
81
-
}
82
-
}
83
-
84
-
#[async_trait]
85
-
impl HandleResolver for CachingHandleResolver {
86
-
async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> {
87
-
let handle = s.to_string();
88
-
89
-
// Check cache first
90
-
{
91
-
let cache = self.cache.read().await;
92
-
if let Some(cached) = cache.get(&handle) {
93
-
match cached {
94
-
ResolveHandleResult::Found(timestamp, did) => {
95
-
if !self.is_expired(*timestamp) {
96
-
tracing::debug!("Cache hit for handle {}: {}", handle, did);
97
-
return Ok(did.clone());
98
-
}
99
-
tracing::debug!("Cache entry expired for handle {}", handle);
100
-
}
101
-
ResolveHandleResult::NotFound(timestamp, error) => {
102
-
if !self.is_expired(*timestamp) {
103
-
tracing::debug!(
104
-
"Cache hit (not found) for handle {}: {}",
105
-
handle,
106
-
error
107
-
);
108
-
return Err(HandleResolverError::HandleNotFoundCached(error.clone()));
109
-
}
110
-
tracing::debug!("Cache entry expired for handle {}", handle);
111
-
}
112
-
}
113
-
}
114
-
}
115
-
116
-
// Not in cache or expired, resolve through inner resolver
117
-
tracing::debug!("Cache miss for handle {}, resolving...", handle);
118
-
let result = self.inner.resolve(s).await;
119
-
let timestamp = Self::current_timestamp();
120
-
121
-
// Store in cache
122
-
{
123
-
let mut cache = self.cache.write().await;
124
-
match &result {
125
-
Ok(did) => {
126
-
cache.insert(
127
-
handle.clone(),
128
-
ResolveHandleResult::Found(timestamp, did.clone()),
129
-
);
130
-
tracing::debug!(
131
-
"Cached successful resolution for handle {}: {}",
132
-
handle,
133
-
did
134
-
);
135
-
}
136
-
Err(e) => {
137
-
cache.insert(
138
-
handle.clone(),
139
-
ResolveHandleResult::NotFound(timestamp, e.to_string()),
140
-
);
141
-
tracing::debug!("Cached failed resolution for handle {}: {}", handle, e);
142
-
}
143
-
}
144
-
}
145
-
146
-
result
147
-
}
148
-
}
149
-
150
-
/// Redis-backed caching handle resolver that caches resolution results in Redis
151
-
/// with a configurable expiration time.
16
+
/// Redis-backed caching handle resolver.
17
+
///
18
+
/// This resolver caches handle resolution results in Redis with a configurable TTL.
19
+
/// Results are stored in a compact binary format using bincode serialization
20
+
/// to minimize storage overhead.
21
+
///
22
+
/// # Features
23
+
///
24
+
/// - Persistent caching across service restarts
25
+
/// - Shared cache across multiple service instances
26
+
/// - Configurable TTL (default: 90 days)
27
+
/// - Compact binary storage format
28
+
/// - Graceful fallback if Redis is unavailable
29
+
///
30
+
/// # Example
31
+
///
32
+
/// ```no_run
33
+
/// use std::sync::Arc;
34
+
/// use deadpool_redis::Pool;
35
+
/// use quickdid::handle_resolver::{RedisHandleResolver, BaseHandleResolver};
36
+
///
37
+
/// # async fn example() {
38
+
/// # let base_resolver: BaseHandleResolver = todo!();
39
+
/// # let redis_pool: Pool = todo!();
40
+
/// // Create with default 90-day TTL
41
+
/// let resolver = RedisHandleResolver::new(
42
+
/// Arc::new(base_resolver),
43
+
/// redis_pool.clone()
44
+
/// );
45
+
///
46
+
/// // Or with custom TTL
47
+
/// let resolver_with_ttl = RedisHandleResolver::with_ttl(
48
+
/// Arc::new(base_resolver),
49
+
/// redis_pool,
50
+
/// 86400 // 1 day in seconds
51
+
/// );
52
+
/// # }
53
+
/// ```
152
54
pub struct RedisHandleResolver {
153
55
/// Base handle resolver to perform actual resolution
154
56
inner: Arc<dyn HandleResolver>,
···
161
63
}
162
64
163
65
impl RedisHandleResolver {
164
-
/// Create a new Redis-backed handle resolver with default 90-day TTL
66
+
/// Create a new Redis-backed handle resolver with default 90-day TTL.
165
67
pub fn new(inner: Arc<dyn HandleResolver>, pool: RedisPool) -> Self {
166
68
Self::with_ttl(inner, pool, 90 * 24 * 60 * 60) // 90 days default
167
69
}
168
70
169
-
/// Create a new Redis-backed handle resolver with custom TTL
71
+
/// Create a new Redis-backed handle resolver with custom TTL.
170
72
pub fn with_ttl(inner: Arc<dyn HandleResolver>, pool: RedisPool, ttl_seconds: u64) -> Self {
171
73
Self::with_full_config(inner, pool, "handle:".to_string(), ttl_seconds)
172
74
}
173
75
174
-
/// Create a new Redis-backed handle resolver with a custom key prefix
76
+
/// Create a new Redis-backed handle resolver with a custom key prefix.
175
77
pub fn with_prefix(
176
78
inner: Arc<dyn HandleResolver>,
177
79
pool: RedisPool,
···
180
82
Self::with_full_config(inner, pool, key_prefix, 90 * 24 * 60 * 60)
181
83
}
182
84
183
-
/// Create a new Redis-backed handle resolver with full configuration
85
+
/// Create a new Redis-backed handle resolver with full configuration.
184
86
pub fn with_full_config(
185
87
inner: Arc<dyn HandleResolver>,
186
88
pool: RedisPool,
···
195
97
}
196
98
}
197
99
198
-
/// Generate the Redis key for a handle
100
+
/// Generate the Redis key for a handle.
101
+
///
102
+
/// Uses MetroHash64 to generate a consistent hash of the handle
103
+
/// for use as the Redis key. This provides better key distribution
104
+
/// and avoids issues with special characters in handles.
199
105
fn make_key(&self, handle: &str) -> String {
200
106
let mut h = MetroHash64::default();
201
107
h.write(handle.as_bytes());
202
108
format!("{}{}", self.key_prefix, h.finish())
203
109
}
204
110
205
-
/// Get the TTL in seconds
111
+
/// Get the TTL in seconds.
206
112
fn ttl_seconds(&self) -> u64 {
207
113
self.ttl_seconds
208
114
}
···
445
351
let _: Result<(), _> = conn.del(key).await;
446
352
}
447
353
}
448
-
}
354
+
}
+59
src/handle_resolver/base.rs
+59
src/handle_resolver/base.rs
···
1
+
//! Base handle resolver implementation.
2
+
//!
3
+
//! This module provides the fundamental handle resolution implementation that
4
+
//! performs actual DNS and HTTP lookups to resolve AT Protocol handles to DIDs.
5
+
6
+
use super::errors::HandleResolverError;
7
+
use super::traits::HandleResolver;
8
+
use async_trait::async_trait;
9
+
use atproto_identity::resolve::{DnsResolver, resolve_subject};
10
+
use reqwest::Client;
11
+
use std::sync::Arc;
12
+
13
+
/// Base handle resolver that performs actual resolution via DNS and HTTP.
14
+
///
15
+
/// This resolver implements the core AT Protocol handle resolution logic:
16
+
/// 1. DNS TXT record lookup for `_atproto.{handle}`
17
+
/// 2. HTTP well-known endpoint query at `https://{handle}/.well-known/atproto-did`
18
+
/// 3. DID document retrieval from PLC directory or web DIDs
19
+
///
20
+
/// # Example
21
+
///
22
+
/// ```no_run
23
+
/// use std::sync::Arc;
24
+
/// use reqwest::Client;
25
+
/// use atproto_identity::resolve::HickoryDnsResolver;
26
+
/// use quickdid::handle_resolver::{BaseHandleResolver, HandleResolver};
27
+
///
28
+
/// # async fn example() {
29
+
/// let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&[]));
30
+
/// let http_client = Client::new();
31
+
///
32
+
/// let resolver = BaseHandleResolver {
33
+
/// dns_resolver,
34
+
/// http_client,
35
+
/// plc_hostname: "plc.directory".to_string(),
36
+
/// };
37
+
///
38
+
/// let did = resolver.resolve("alice.bsky.social").await.unwrap();
39
+
/// # }
40
+
/// ```
41
+
pub struct BaseHandleResolver {
42
+
/// DNS resolver for handle-to-DID resolution via TXT records.
43
+
pub dns_resolver: Arc<dyn DnsResolver>,
44
+
45
+
/// HTTP client for DID document retrieval and well-known endpoint queries.
46
+
pub http_client: Client,
47
+
48
+
/// Hostname of the PLC directory server for `did:plc` resolution.
49
+
pub plc_hostname: String,
50
+
}
51
+
52
+
#[async_trait]
53
+
impl HandleResolver for BaseHandleResolver {
54
+
async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> {
55
+
resolve_subject(&self.http_client, &*self.dns_resolver, s)
56
+
.await
57
+
.map_err(|e| HandleResolverError::ResolutionFailed(e.to_string()))
58
+
}
59
+
}
+26
src/handle_resolver/errors.rs
+26
src/handle_resolver/errors.rs
···
1
+
//! Error types for handle resolution operations.
2
+
//!
3
+
//! This module defines all error types used throughout the handle resolver components,
4
+
//! following the QuickDID error format conventions.
5
+
6
+
use thiserror::Error;
7
+
8
+
/// Errors that can occur during handle resolution
9
+
#[derive(Error, Debug)]
10
+
pub enum HandleResolverError {
11
+
/// Failed to resolve subject through DNS or HTTP
12
+
#[error("error-quickdid-resolve-1 Failed to resolve subject: {0}")]
13
+
ResolutionFailed(String),
14
+
15
+
/// Handle not found in cache with specific error message
16
+
#[error("error-quickdid-resolve-2 Handle not found (cached): {0}")]
17
+
HandleNotFoundCached(String),
18
+
19
+
/// Handle not found in cache (generic)
20
+
#[error("error-quickdid-resolve-3 Handle not found (cached)")]
21
+
HandleNotFound,
22
+
23
+
/// Mock resolver failure for testing
24
+
#[error("error-quickdid-resolve-4 Mock resolution failure")]
25
+
MockResolutionFailure,
26
+
}
+145
src/handle_resolver/memory.rs
+145
src/handle_resolver/memory.rs
···
1
+
//! In-memory caching handle resolver.
2
+
//!
3
+
//! This module provides a handle resolver that caches resolution results in memory
4
+
//! with a configurable TTL. This is useful for reducing DNS/HTTP lookups and
5
+
//! improving performance when Redis is not available.
6
+
7
+
use super::errors::HandleResolverError;
8
+
use super::traits::HandleResolver;
9
+
use async_trait::async_trait;
10
+
use chrono::Utc;
11
+
use std::collections::HashMap;
12
+
use std::sync::Arc;
13
+
use tokio::sync::RwLock;
14
+
15
+
/// Result of a handle resolution cached in memory.
16
+
#[derive(Clone, Debug)]
17
+
pub enum ResolveHandleResult {
18
+
/// Handle was successfully resolved to a DID
19
+
Found(u64, String),
20
+
/// Handle resolution failed
21
+
NotFound(u64, String),
22
+
}
23
+
24
+
/// In-memory caching wrapper for handle resolvers.
25
+
///
26
+
/// This resolver wraps another resolver and caches results in memory with
27
+
/// a configurable TTL. Both successful and failed resolutions are cached
28
+
/// to avoid repeated lookups.
29
+
///
30
+
/// # Example
31
+
///
32
+
/// ```no_run
33
+
/// use std::sync::Arc;
34
+
/// use quickdid::handle_resolver::{CachingHandleResolver, BaseHandleResolver, HandleResolver};
35
+
///
36
+
/// # async fn example() {
37
+
/// # let base_resolver: BaseHandleResolver = todo!();
38
+
/// let caching_resolver = CachingHandleResolver::new(
39
+
/// Arc::new(base_resolver),
40
+
/// 300 // 5 minute TTL
41
+
/// );
42
+
///
43
+
/// // First call hits the underlying resolver
44
+
/// let did1 = caching_resolver.resolve("alice.bsky.social").await.unwrap();
45
+
///
46
+
/// // Second call returns cached result
47
+
/// let did2 = caching_resolver.resolve("alice.bsky.social").await.unwrap();
48
+
/// # }
49
+
/// ```
50
+
pub struct CachingHandleResolver {
51
+
inner: Arc<dyn HandleResolver>,
52
+
cache: Arc<RwLock<HashMap<String, ResolveHandleResult>>>,
53
+
ttl_seconds: u64,
54
+
}
55
+
56
+
impl CachingHandleResolver {
57
+
/// Create a new caching handle resolver.
58
+
///
59
+
/// # Arguments
60
+
///
61
+
/// * `inner` - The underlying resolver to use for actual resolution
62
+
/// * `ttl_seconds` - How long to cache results in seconds
63
+
pub fn new(inner: Arc<dyn HandleResolver>, ttl_seconds: u64) -> Self {
64
+
Self {
65
+
inner,
66
+
cache: Arc::new(RwLock::new(HashMap::new())),
67
+
ttl_seconds,
68
+
}
69
+
}
70
+
71
+
fn current_timestamp() -> u64 {
72
+
Utc::now().timestamp() as u64
73
+
}
74
+
75
+
fn is_expired(&self, timestamp: u64) -> bool {
76
+
let current = Self::current_timestamp();
77
+
current > timestamp && (current - timestamp) > self.ttl_seconds
78
+
}
79
+
}
80
+
81
+
#[async_trait]
82
+
impl HandleResolver for CachingHandleResolver {
83
+
async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> {
84
+
let handle = s.to_string();
85
+
86
+
// Check cache first
87
+
{
88
+
let cache = self.cache.read().await;
89
+
if let Some(cached) = cache.get(&handle) {
90
+
match cached {
91
+
ResolveHandleResult::Found(timestamp, did) => {
92
+
if !self.is_expired(*timestamp) {
93
+
tracing::debug!("Cache hit for handle {}: {}", handle, did);
94
+
return Ok(did.clone());
95
+
}
96
+
tracing::debug!("Cache entry expired for handle {}", handle);
97
+
}
98
+
ResolveHandleResult::NotFound(timestamp, error) => {
99
+
if !self.is_expired(*timestamp) {
100
+
tracing::debug!(
101
+
"Cache hit (not found) for handle {}: {}",
102
+
handle,
103
+
error
104
+
);
105
+
return Err(HandleResolverError::HandleNotFoundCached(error.clone()));
106
+
}
107
+
tracing::debug!("Cache entry expired for handle {}", handle);
108
+
}
109
+
}
110
+
}
111
+
}
112
+
113
+
// Not in cache or expired, resolve through inner resolver
114
+
tracing::debug!("Cache miss for handle {}, resolving...", handle);
115
+
let result = self.inner.resolve(s).await;
116
+
let timestamp = Self::current_timestamp();
117
+
118
+
// Store in cache
119
+
{
120
+
let mut cache = self.cache.write().await;
121
+
match &result {
122
+
Ok(did) => {
123
+
cache.insert(
124
+
handle.clone(),
125
+
ResolveHandleResult::Found(timestamp, did.clone()),
126
+
);
127
+
tracing::debug!(
128
+
"Cached successful resolution for handle {}: {}",
129
+
handle,
130
+
did
131
+
);
132
+
}
133
+
Err(e) => {
134
+
cache.insert(
135
+
handle.clone(),
136
+
ResolveHandleResult::NotFound(timestamp, e.to_string()),
137
+
);
138
+
tracing::debug!("Cached failed resolution for handle {}: {}", handle, e);
139
+
}
140
+
}
141
+
}
142
+
143
+
result
144
+
}
145
+
}
+54
src/handle_resolver/mod.rs
+54
src/handle_resolver/mod.rs
···
1
+
//! Handle resolution module for AT Protocol identity resolution.
2
+
//!
3
+
//! This module provides various implementations of handle-to-DID resolution
4
+
//! with different caching strategies.
5
+
//!
6
+
//! # Architecture
7
+
//!
8
+
//! The module is structured around the [`HandleResolver`] trait with multiple
9
+
//! implementations:
10
+
//!
11
+
//! - [`BaseHandleResolver`]: Core resolver that performs actual DNS/HTTP lookups
12
+
//! - [`CachingHandleResolver`]: In-memory caching wrapper with configurable TTL
13
+
//! - [`RedisHandleResolver`]: Redis-backed persistent caching with binary serialization
14
+
//!
15
+
//! # Example Usage
16
+
//!
17
+
//! ```no_run
18
+
//! use std::sync::Arc;
19
+
//! use quickdid::handle_resolver::{BaseHandleResolver, CachingHandleResolver, HandleResolver};
20
+
//!
21
+
//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
22
+
//! # use atproto_identity::resolve::HickoryDnsResolver;
23
+
//! # use reqwest::Client;
24
+
//! # let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&[]));
25
+
//! # let http_client = Client::new();
26
+
//! // Create base resolver
27
+
//! let base = Arc::new(BaseHandleResolver {
28
+
//! dns_resolver,
29
+
//! http_client,
30
+
//! plc_hostname: "plc.directory".to_string(),
31
+
//! });
32
+
//!
33
+
//! // Wrap with in-memory caching
34
+
//! let resolver = CachingHandleResolver::new(base, 300);
35
+
//!
36
+
//! // Resolve a handle
37
+
//! let did = resolver.resolve("alice.bsky.social").await?;
38
+
//! # Ok(())
39
+
//! # }
40
+
//! ```
41
+
42
+
// Module structure
43
+
mod errors;
44
+
mod traits;
45
+
mod base;
46
+
mod memory;
47
+
mod redis;
48
+
49
+
// Re-export public API
50
+
pub use errors::HandleResolverError;
51
+
pub use traits::HandleResolver;
52
+
pub use base::BaseHandleResolver;
53
+
pub use memory::{CachingHandleResolver, ResolveHandleResult};
54
+
pub use redis::RedisHandleResolver;
+50
src/handle_resolver/traits.rs
+50
src/handle_resolver/traits.rs
···
1
+
//! Core traits for handle resolution.
2
+
//!
3
+
//! This module defines the fundamental `HandleResolver` trait that all resolver
4
+
//! implementations must satisfy.
5
+
6
+
use super::errors::HandleResolverError;
7
+
use 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
+
///
21
+
/// struct MyResolver;
22
+
///
23
+
/// #[async_trait]
24
+
/// impl HandleResolver for MyResolver {
25
+
/// async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> {
26
+
/// // Custom resolution logic
27
+
/// Ok(format!("did:plc:{}", s.replace('.', "")))
28
+
/// }
29
+
/// }
30
+
/// ```
31
+
#[async_trait]
32
+
pub trait HandleResolver: Send + Sync {
33
+
/// Resolve a handle to its DID.
34
+
///
35
+
/// # Arguments
36
+
///
37
+
/// * `s` - The handle to resolve (e.g., "alice.bsky.social")
38
+
///
39
+
/// # Returns
40
+
///
41
+
/// The resolved DID on success, or an error if resolution fails.
42
+
///
43
+
/// # Errors
44
+
///
45
+
/// Returns [`HandleResolverError`] if:
46
+
/// - The handle cannot be resolved
47
+
/// - Network errors occur during resolution
48
+
/// - The handle is invalid or doesn't exist
49
+
async fn resolve(&self, s: &str) -> Result<String, HandleResolverError>;
50
+
}