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}