Skip to main content

bamboo_infrastructure/storage/
jsonl.rs

1//! JSONL-based session storage implementation.
2//!
3//! This module provides persistent storage for sessions using JSON format.
4//!
5//! # Storage Layout
6//!
7//! ```text
8//! base_path/
9//! └── {session_id}.json    # Session metadata
10//! ```
11//!
12//! # Usage
13//!
14//! ```rust,ignore
15//! use bamboo_agent::agent::core::storage::jsonl::*;
16//!
17//! let storage = JsonlStorage::new("/path/to/bamboo-data-dir/sessions");
18//! storage.init().await?;
19//!
20//! // Save session
21//! storage.save_session(&session).await?;
22//!
23//! // Load session
24//! let session = storage.load_session(&session_id).await?;
25//! ```
26
27use bamboo_domain::Session;
28use bamboo_domain::Storage;
29use std::path::{Path, PathBuf};
30use tokio::fs;
31
32/// JSONL-based session storage.
33///
34/// Stores session metadata as JSON.
35///
36/// # Fields
37///
38/// * `base_path` - Base directory for storing session files
39///
40/// # Example
41///
42/// ```rust,ignore
43/// let storage = JsonlStorage::new("/path/to/bamboo-data-dir/sessions");
44/// storage.init().await?;
45///
46/// storage.save_session(&session).await?;
47/// ```
48#[derive(Debug, Clone)]
49pub struct JsonlStorage {
50    /// Base directory for session files
51    base_path: PathBuf,
52}
53
54impl JsonlStorage {
55    /// Create a new JSONL storage instance.
56    ///
57    /// # Arguments
58    ///
59    /// * `base_path` - Directory to store session files
60    ///
61    /// # Example
62    ///
63    /// ```rust,ignore
64    /// let storage = JsonlStorage::new("/path/to/bamboo-data-dir/sessions");
65    /// ```
66    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}