atproto_identity/storage.rs
1//! DID document storage abstraction.
2//!
3//! Storage trait for DID document CRUD operations supporting multiple
4//! backends (database, file system, memory) with consistent interface.
5
6use anyhow::Result;
7
8use crate::model::Document;
9
10/// Trait for implementing DID document CRUD operations across different storage backends.
11///
12/// This trait provides an abstraction layer for storing and retrieving DID documents,
13/// allowing different implementations for various storage systems such as databases, file systems,
14/// in-memory stores, or cloud storage services.
15///
16/// All methods return `anyhow::Result` to allow implementations to use their own error types
17/// while providing a consistent interface for callers. Implementations should handle their
18/// specific error conditions and convert them to appropriate error messages.
19///
20/// ## Thread Safety
21///
22/// This trait requires implementations to be thread-safe (`Send + Sync`), meaning:
23/// - `Send`: The storage implementation can be moved between threads
24/// - `Sync`: The storage implementation can be safely accessed from multiple threads simultaneously
25///
26/// This is essential for async applications where the storage might be accessed from different
27/// async tasks running on different threads. Implementations should use appropriate
28/// synchronization primitives (like `Arc<Mutex<>>`, `RwLock`, or database connection pools)
29/// to ensure thread safety.
30///
31/// ## Usage
32///
33/// Implementors of this trait can provide storage for AT Protocol DID documents in any backend:
34///
35/// ```rust,ignore
36/// use atproto_identity::storage::DidDocumentStorage;
37/// use atproto_identity::model::Document;
38/// use anyhow::Result;
39/// use std::sync::Arc;
40/// use tokio::sync::RwLock;
41/// use std::collections::HashMap;
42///
43/// // Thread-safe in-memory storage using Arc<RwLock<>>
44/// #[derive(Clone)]
45/// struct InMemoryStorage {
46/// data: Arc<RwLock<HashMap<String, Document>>>, // DID -> Document mapping
47/// }
48///
49/// #[async_trait::async_trait]
50/// impl DidDocumentStorage for InMemoryStorage {
51/// async fn get_document_by_did(&self, did: &str) -> Result<Option<Document>> {
52/// let data = self.data.read().await;
53/// Ok(data.get(did).cloned())
54/// }
55///
56/// async fn store_document(&self, document: Document) -> Result<()> {
57/// let mut data = self.data.write().await;
58/// data.insert(document.id.clone(), document);
59/// Ok(())
60/// }
61///
62/// async fn delete_document_by_did(&self, did: &str) -> Result<()> {
63/// let mut data = self.data.write().await;
64/// data.remove(did);
65/// Ok(())
66/// }
67/// }
68///
69/// // Database storage with thread-safe connection pool
70/// struct DatabaseStorage {
71/// pool: sqlx::Pool<sqlx::Postgres>, // Thread-safe connection pool
72/// }
73///
74/// #[async_trait::async_trait]
75/// impl DidDocumentStorage for DatabaseStorage {
76/// async fn get_document_by_did(&self, did: &str) -> Result<Option<Document>> {
77/// // Database connection pools are thread-safe
78/// let row: Option<(serde_json::Value,)> = sqlx::query_as(
79/// "SELECT document FROM did_documents WHERE did = $1"
80/// )
81/// .bind(did)
82/// .fetch_optional(&self.pool)
83/// .await?;
84///
85/// if let Some((doc_json,)) = row {
86/// let document: Document = serde_json::from_value(doc_json)?;
87/// Ok(Some(document))
88/// } else {
89/// Ok(None)
90/// }
91/// }
92///
93/// async fn store_document(&self, document: Document) -> Result<()> {
94/// let doc_json = serde_json::to_value(&document)?;
95/// sqlx::query("INSERT INTO did_documents (did, document) VALUES ($1, $2) ON CONFLICT (did) DO UPDATE SET document = $2")
96/// .bind(&document.id)
97/// .bind(doc_json)
98/// .execute(&self.pool)
99/// .await?;
100/// Ok(())
101/// }
102///
103/// async fn delete_document_by_did(&self, did: &str) -> Result<()> {
104/// sqlx::query("DELETE FROM did_documents WHERE did = $1")
105/// .bind(did)
106/// .execute(&self.pool)
107/// .await?;
108/// Ok(())
109/// }
110/// }
111/// ```
112#[async_trait::async_trait]
113pub trait DidDocumentStorage: Send + Sync {
114 /// Retrieves a DID document associated with the given DID.
115 ///
116 /// This method looks up the complete DID document that is currently stored for the provided
117 /// DID (Decentralized Identifier). The document contains services, verification methods,
118 /// and other identity information for the DID.
119 ///
120 /// # Arguments
121 /// * `did` - The DID (Decentralized Identifier) to look up. Should be in the format
122 /// `did:method:identifier` (e.g., "did:plc:bv6ggog3tya2z3vxsub7hnal")
123 ///
124 /// # Returns
125 /// * `Ok(Some(document))` - If a document is found for the given DID
126 /// * `Ok(None)` - If no document is currently stored for the DID
127 /// * `Err(error)` - If an error occurs during retrieval (storage failure, invalid DID format, etc.)
128 ///
129 /// # Examples
130 ///
131 /// ```rust,ignore
132 /// let storage = MyStorage::new();
133 /// let document = storage.get_document_by_did("did:plc:bv6ggog3tya2z3vxsub7hnal").await?;
134 /// match document {
135 /// Some(doc) => {
136 /// println!("Found document for DID: {}", doc.id);
137 /// if let Some(handle) = doc.handles() {
138 /// println!("Primary handle: {}", handle);
139 /// }
140 /// },
141 /// None => println!("No document found for this DID"),
142 /// }
143 /// ```
144 async fn get_document_by_did(&self, did: &str) -> Result<Option<Document>>;
145
146 /// Stores or updates a DID document.
147 ///
148 /// This method creates a new DID document entry or updates an existing one.
149 /// In the AT Protocol ecosystem, this operation typically occurs when a DID document
150 /// is resolved from the network, updated by the identity owner, or cached for performance.
151 ///
152 /// Implementations should ensure that:
153 /// - The document's DID (`document.id`) is used as the key for storage
154 /// - The operation is atomic (either fully succeeds or fully fails)
155 /// - Any existing document for the same DID is properly replaced
156 /// - The complete document structure is preserved
157 ///
158 /// # Arguments
159 /// * `document` - The complete DID document to store. The document's `id` field
160 /// will be used as the storage key.
161 ///
162 /// # Returns
163 /// * `Ok(())` - If the document was successfully stored or updated
164 /// * `Err(error)` - If an error occurs during the operation (storage failure,
165 /// serialization failure, constraint violation, etc.)
166 ///
167 /// # Examples
168 ///
169 /// ```rust,ignore
170 /// let storage = MyStorage::new();
171 /// let document = Document {
172 /// id: "did:plc:bv6ggog3tya2z3vxsub7hnal".to_string(),
173 /// also_known_as: vec!["at://alice.bsky.social".to_string()],
174 /// service: vec![/* services */],
175 /// verification_method: vec![/* verification methods */],
176 /// extra: HashMap::new(),
177 /// };
178 /// storage.store_document(document).await?;
179 /// println!("Document successfully stored");
180 /// ```
181 async fn store_document(&self, document: Document) -> Result<()>;
182
183 /// Deletes a DID document by its DID.
184 ///
185 /// This method removes a DID document from storage using the DID as the identifier.
186 /// This operation is typically used when cleaning up expired cache entries, removing
187 /// invalid documents, or when an identity is deactivated.
188 ///
189 /// Implementations should:
190 /// - Handle the case where the DID doesn't exist gracefully (return Ok(()))
191 /// - Ensure the deletion is atomic
192 /// - Clean up any related data or indexes
193 /// - Preserve referential integrity if applicable
194 ///
195 /// # Arguments
196 /// * `did` - The DID identifying the document to delete.
197 /// Should be in the format `did:method:identifier`
198 /// (e.g., "did:plc:bv6ggog3tya2z3vxsub7hnal")
199 ///
200 /// # Returns
201 /// * `Ok(())` - If the document was successfully deleted or didn't exist
202 /// * `Err(error)` - If an error occurs during deletion (storage failure, etc.)
203 ///
204 /// # Examples
205 ///
206 /// ```rust,ignore
207 /// let storage = MyStorage::new();
208 /// storage.delete_document_by_did("did:plc:bv6ggog3tya2z3vxsub7hnal").await?;
209 /// println!("Document deleted");
210 /// ```
211 async fn delete_document_by_did(&self, did: &str) -> Result<()>;
212}