bamboo_infrastructure/storage/
jsonl.rs1use bamboo_domain::Session;
28use bamboo_domain::Storage;
29use std::path::{Path, PathBuf};
30use tokio::fs;
31
32#[derive(Debug, Clone)]
49pub struct JsonlStorage {
50 base_path: PathBuf,
52}
53
54impl JsonlStorage {
55 pub fn new(base_path: impl AsRef<Path>) -> Self {
67 Self {
68 base_path: base_path.as_ref().to_path_buf(),
69 }
70 }
71
72 pub async fn init(&self) -> std::io::Result<()> {
73 fs::create_dir_all(&self.base_path).await
74 }
75
76 pub async fn save_session(&self, session: &Session) -> std::io::Result<()> {
77 let path = self.session_path(&session.id);
78 let json = serde_json::to_string(session)?;
79 fs::write(path, json).await
80 }
81
82 pub async fn load_session(&self, session_id: &str) -> std::io::Result<Option<Session>> {
83 let path = self.session_path(session_id);
84 if !path.exists() {
85 return Ok(None);
86 }
87 let content = fs::read_to_string(path).await?;
88 let session = serde_json::from_str(&content)?;
89 Ok(Some(session))
90 }
91
92 pub async fn delete_session(&self, session_id: &str) -> std::io::Result<bool> {
93 let session_path = self.session_path(session_id);
94 let mut deleted_any = false;
95
96 for path in [session_path] {
97 match fs::remove_file(&path).await {
98 Ok(()) => {
99 deleted_any = true;
100 }
101 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
102 Err(error) => return Err(error),
103 }
104 }
105
106 Ok(deleted_any)
107 }
108
109 fn session_path(&self, session_id: &str) -> PathBuf {
110 self.base_path.join(format!("{}.json", session_id))
111 }
112}
113
114#[async_trait::async_trait]
115impl Storage for JsonlStorage {
116 async fn save_session(&self, session: &Session) -> std::io::Result<()> {
117 JsonlStorage::save_session(self, session).await
118 }
119
120 async fn load_session(&self, session_id: &str) -> std::io::Result<Option<Session>> {
121 JsonlStorage::load_session(self, session_id).await
122 }
123
124 async fn delete_session(&self, session_id: &str) -> std::io::Result<bool> {
125 JsonlStorage::delete_session(self, session_id).await
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use std::io;
133 use uuid::Uuid;
134
135 async fn create_temp_storage() -> io::Result<(JsonlStorage, PathBuf)> {
136 let temp_dir = std::env::temp_dir().join(format!("jsonl-storage-test-{}", Uuid::new_v4()));
137 let storage = JsonlStorage::new(&temp_dir);
138 storage.init().await?;
139 Ok((storage, temp_dir))
140 }
141
142 #[tokio::test]
143 async fn test_init_creates_directory() -> io::Result<()> {
144 let temp_dir = std::env::temp_dir().join(format!("jsonl-init-test-{}", Uuid::new_v4()));
145 let storage = JsonlStorage::new(&temp_dir);
146
147 assert!(!temp_dir.exists());
148 storage.init().await?;
149 assert!(temp_dir.exists());
150
151 fs::remove_dir_all(temp_dir).await?;
152 Ok(())
153 }
154
155 #[tokio::test]
156 async fn test_save_and_load_session() -> io::Result<()> {
157 let (storage, temp_dir) = create_temp_storage().await?;
158 let session = Session::new("session-1", "test-model");
159
160 storage.save_session(&session).await?;
161 let loaded = storage.load_session(&session.id).await?;
162
163 assert!(loaded.is_some());
164 let loaded = loaded.unwrap();
165 assert_eq!(loaded.id, session.id);
166 assert_eq!(loaded.model, session.model);
167
168 fs::remove_dir_all(temp_dir).await?;
169 Ok(())
170 }
171
172 #[tokio::test]
173 async fn test_load_session_returns_none_when_not_found() -> io::Result<()> {
174 let (storage, temp_dir) = create_temp_storage().await?;
175
176 let loaded = storage.load_session("nonexistent").await?;
177 assert!(loaded.is_none());
178
179 fs::remove_dir_all(temp_dir).await?;
180 Ok(())
181 }
182
183 #[tokio::test]
184 async fn delete_session_removes_metadata_file() -> io::Result<()> {
185 let (storage, temp_dir) = create_temp_storage().await?;
186 let session = Session::new("session-1", "test-model");
187
188 storage.save_session(&session).await?;
189
190 assert!(storage.session_path(&session.id).exists());
191
192 let deleted = storage.delete_session(&session.id).await?;
193
194 assert!(deleted);
195 assert!(!storage.session_path(&session.id).exists());
196
197 fs::remove_dir_all(temp_dir).await?;
198 Ok(())
199 }
200
201 #[tokio::test]
202 async fn delete_session_returns_false_when_files_do_not_exist() -> io::Result<()> {
203 let (storage, temp_dir) = create_temp_storage().await?;
204
205 let deleted = storage.delete_session("missing-session").await?;
206
207 assert!(!deleted);
208
209 fs::remove_dir_all(temp_dir).await?;
210 Ok(())
211 }
212
213 #[tokio::test]
214 async fn test_session_path_format() -> io::Result<()> {
215 let temp_dir = std::env::temp_dir().join(format!("jsonl-path-test-{}", Uuid::new_v4()));
216 fs::create_dir_all(&temp_dir).await?;
217 let storage = JsonlStorage::new(&temp_dir);
218
219 let session_path = storage.session_path("test-123");
220
221 assert_eq!(session_path.file_name().unwrap(), "test-123.json");
222
223 fs::remove_dir_all(temp_dir).await?;
224 Ok(())
225 }
226
227 #[tokio::test]
228 async fn test_overwrite_existing_session() -> io::Result<()> {
229 let (storage, temp_dir) = create_temp_storage().await?;
230
231 let session1 = Session::new("session-1", "model-1");
232 storage.save_session(&session1).await?;
233
234 let session2 = Session::new("session-1", "model-2");
235 storage.save_session(&session2).await?;
236
237 let loaded = storage.load_session("session-1").await?.unwrap();
238 assert_eq!(loaded.model, "model-2");
239
240 fs::remove_dir_all(temp_dir).await?;
241 Ok(())
242 }
243
244 #[tokio::test]
245 async fn test_storage_trait_implementation() -> io::Result<()> {
246 let (storage, temp_dir) = create_temp_storage().await?;
247 let trait_obj: &dyn Storage = &storage;
248
249 let session = Session::new("session-1", "test-model");
250 trait_obj.save_session(&session).await?;
251
252 let loaded = trait_obj.load_session(&session.id).await?;
253 assert!(loaded.is_some());
254
255 let deleted = trait_obj.delete_session(&session.id).await?;
256 assert!(deleted);
257
258 fs::remove_dir_all(temp_dir).await?;
259 Ok(())
260 }
261}