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}