Skip to main content

atproto_client/
record_resolver.rs

1//! Helpers for resolving AT Protocol records referenced by URI.
2
3use std::str::FromStr;
4use std::sync::Arc;
5
6use anyhow::{Result, anyhow, bail};
7use async_trait::async_trait;
8use atproto_identity::traits::IdentityResolver;
9use atproto_record::aturi::ATURI;
10
11use crate::{
12    client::Auth,
13    com::atproto::repo::{GetRecordResponse, get_record},
14};
15
16/// Trait for resolving AT Protocol records by `at://` URI.
17///
18/// Implementations perform the network lookup and deserialize the response into
19/// the requested type.
20#[async_trait]
21pub trait RecordResolver: Send + Sync {
22    /// Resolve an AT URI to a typed record.
23    async fn resolve<T>(&self, aturi: &str) -> Result<T>
24    where
25        T: serde::de::DeserializeOwned + Send;
26}
27
28/// Resolver that fetches records using public XRPC endpoints.
29///
30/// Uses an identity resolver to dynamically determine the PDS endpoint for each record.
31#[derive(Clone)]
32pub struct HttpRecordResolver {
33    http_client: reqwest::Client,
34    identity_resolver: Arc<dyn IdentityResolver>,
35}
36
37impl HttpRecordResolver {
38    /// Create a new resolver using the provided HTTP client and identity resolver.
39    ///
40    /// The identity resolver is used to dynamically determine the PDS endpoint for each record
41    /// based on the authority (DID or handle) in the AT URI.
42    pub fn new(http_client: reqwest::Client, identity_resolver: Arc<dyn IdentityResolver>) -> Self {
43        Self {
44            http_client,
45            identity_resolver,
46        }
47    }
48}
49
50#[async_trait]
51impl RecordResolver for HttpRecordResolver {
52    async fn resolve<T>(&self, aturi: &str) -> Result<T>
53    where
54        T: serde::de::DeserializeOwned + Send,
55    {
56        let parsed = ATURI::from_str(aturi).map_err(|error| anyhow!(error))?;
57
58        // Resolve the authority (DID or handle) to get the DID document
59        let document = self
60            .identity_resolver
61            .resolve(&parsed.authority)
62            .await
63            .map_err(|error| {
64                anyhow!(
65                    "Failed to resolve identity for {}: {}",
66                    parsed.authority,
67                    error
68                )
69            })?;
70
71        // Extract PDS endpoint from the DID document
72        let pds_endpoints = document.pds_endpoints();
73        let base_url = pds_endpoints
74            .first()
75            .ok_or_else(|| anyhow!("No PDS endpoint found for {}", parsed.authority))?;
76
77        let auth = Auth::None;
78
79        let response = get_record(
80            &self.http_client,
81            &auth,
82            base_url,
83            &parsed.authority,
84            &parsed.collection,
85            &parsed.record_key,
86            None,
87        )
88        .await?;
89
90        match response {
91            GetRecordResponse::Record { value, .. } => {
92                serde_json::from_value(value).map_err(|error| anyhow!(error))
93            }
94            GetRecordResponse::Error(error) => {
95                let message = error.error_message();
96                if message.is_empty() {
97                    bail!("Record resolution failed without additional error details");
98                }
99
100                bail!(message);
101            }
102        }
103    }
104}