A better Rust ATProto crate
at oauth 4.7 kB view raw
1//! Generic session storage traits and utilities. 2 3use async_trait::async_trait; 4use miette::Diagnostic; 5use serde::Serialize; 6use serde::de::DeserializeOwned; 7use serde_json::Value; 8use std::collections::HashMap; 9use std::error::Error as StdError; 10use std::fmt::Display; 11use std::hash::Hash; 12use std::path::{Path, PathBuf}; 13use std::sync::Arc; 14use tokio::sync::RwLock; 15 16/// Errors emitted by session stores. 17#[derive(Debug, thiserror::Error, Diagnostic)] 18pub enum SessionStoreError { 19 /// Filesystem or I/O error 20 #[error("I/O error: {0}")] 21 #[diagnostic(code(jacquard::session_store::io))] 22 Io(#[from] std::io::Error), 23 /// Serialization error (e.g., JSON) 24 #[error("serialization error: {0}")] 25 #[diagnostic(code(jacquard::session_store::serde))] 26 Serde(#[from] serde_json::Error), 27 /// Any other error from a backend implementation 28 #[error(transparent)] 29 #[diagnostic(code(jacquard::session_store::other))] 30 Other(#[from] Box<dyn StdError + Send + Sync>), 31} 32 33/// Pluggable storage for arbitrary session records. 34#[async_trait] 35pub trait SessionStore<K, T>: Send + Sync 36where 37 K: Eq + Hash, 38 T: Clone, 39{ 40 /// Get the current session if present. 41 async fn get(&self, key: &K) -> Option<T>; 42 /// Persist the given session. 43 async fn set(&self, key: K, session: T) -> Result<(), SessionStoreError>; 44 /// Delete the given session. 45 async fn del(&self, key: &K) -> Result<(), SessionStoreError>; 46} 47 48/// In-memory session store suitable for short-lived sessions and tests. 49#[derive(Clone)] 50pub struct MemorySessionStore<K, T>(Arc<RwLock<HashMap<K, T>>>); 51 52impl<K, T> Default for MemorySessionStore<K, T> { 53 fn default() -> Self { 54 Self(Arc::new(RwLock::new(HashMap::new()))) 55 } 56} 57 58#[async_trait] 59impl<K, T> SessionStore<K, T> for MemorySessionStore<K, T> 60where 61 K: Eq + Hash + Send + Sync, 62 T: Clone + Send + Sync + 'static, 63{ 64 async fn get(&self, key: &K) -> Option<T> { 65 self.0.read().await.get(key).cloned() 66 } 67 async fn set(&self, key: K, session: T) -> Result<(), SessionStoreError> { 68 self.0.write().await.insert(key, session); 69 Ok(()) 70 } 71 async fn del(&self, key: &K) -> Result<(), SessionStoreError> { 72 self.0.write().await.remove(key); 73 Ok(()) 74 } 75} 76 77/// File-backed token store using a JSON file. 78/// 79/// NOT secure, only suitable for development. 80/// 81/// Example 82/// ```ignore 83/// use jacquard::client::{AtClient, FileTokenStore}; 84/// let base = url::Url::parse("https://bsky.social").unwrap(); 85/// let store = FileTokenStore::new("/tmp/jacquard-session.json"); 86/// let client = AtClient::new(reqwest::Client::new(), base, store); 87/// ``` 88#[derive(Clone, Debug)] 89pub struct FileTokenStore { 90 /// Path to the JSON file. 91 pub path: PathBuf, 92} 93 94impl FileTokenStore { 95 /// Create a new file token store at the given path. 96 pub fn new(path: impl AsRef<Path>) -> Self { 97 Self { 98 path: path.as_ref().to_path_buf(), 99 } 100 } 101} 102 103#[async_trait::async_trait] 104impl< 105 K: Eq + Hash + Display + Send + Sync + 'static, 106 T: Clone + Serialize + DeserializeOwned + Send + Sync + 'static, 107> SessionStore<K, T> for FileTokenStore 108{ 109 /// Get the current session if present. 110 async fn get(&self, key: &K) -> Option<T> { 111 let file = std::fs::read_to_string(&self.path).ok()?; 112 let store: Value = serde_json::from_str(&file).ok()?; 113 114 let session = store.get(key.to_string())?; 115 serde_json::from_value(session.clone()).ok() 116 } 117 /// Persist the given session. 118 async fn set(&self, key: K, session: T) -> Result<(), SessionStoreError> { 119 let file = std::fs::read_to_string(&self.path)?; 120 let mut store: Value = serde_json::from_str(&file)?; 121 let key_string = key.to_string(); 122 if let Some(store) = store.as_object_mut() { 123 store.insert(key_string, serde_json::to_value(session.clone())?); 124 125 std::fs::write(&self.path, serde_json::to_string_pretty(&store)?)?; 126 Ok(()) 127 } else { 128 Err(SessionStoreError::Other("invalid store".into())) 129 } 130 } 131 /// Delete the given session. 132 async fn del(&self, key: &K) -> Result<(), SessionStoreError> { 133 let file = std::fs::read_to_string(&self.path)?; 134 let mut store: Value = serde_json::from_str(&file)?; 135 let key_string = key.to_string(); 136 if let Some(store) = store.as_object_mut() { 137 store.remove(&key_string); 138 139 std::fs::write(&self.path, serde_json::to_string_pretty(&store)?)?; 140 Ok(()) 141 } else { 142 Err(SessionStoreError::Other("invalid store".into())) 143 } 144 } 145}