Skip to main content

ryo_app/
storage.rs

1//! Storage trait for dependency injection.
2//!
3//! This module defines the `Storage` trait that abstracts persistent storage
4//! operations. Implementations are provided in `ryo-storage` crate.
5//!
6//! # Design
7//!
8//! The storage trait enables:
9//! - **DI**: Different backends (Local, Network, Cloud) can be injected
10//! - **Testing**: Mock storage for unit tests
11//! - **Future flexibility**: Easy to add new storage backends
12//!
13//! # Example
14//!
15//! ```ignore
16//! use ryo_app::{Api, Storage};
17//! use ryo_storage::RyoStorage;
18//!
19//! // Production: use real storage
20//! let storage = Box::new(RyoStorage::global()?);
21//! let api = Api::new(storage);
22//!
23//! // Testing: use mock storage
24//! let mock = Box::new(MockStorage::new());
25//! let api = Api::new(mock);
26//! ```
27
28use ryo_storage::{StorageResult, TxLog};
29use std::path::Path;
30
31/// Abstract storage trait for session persistence.
32///
33/// This trait is implemented by `ryo-storage` crate and injected into `Api`.
34pub trait Storage: Send + Sync {
35    /// Initialize storage (create directories, etc.)
36    fn init(&mut self) -> StorageResult<()>;
37
38    /// Save a transaction log, returning the session ID.
39    fn save(&mut self, log: &TxLog) -> StorageResult<String>;
40
41    /// Load a transaction log by session ID.
42    fn load(&self, session_id: &str) -> StorageResult<TxLog>;
43
44    /// List all session IDs.
45    fn list_sessions(&self) -> StorageResult<Vec<String>>;
46
47    /// List sessions for a specific project.
48    fn sessions_for_project(&self, project_path: &Path) -> StorageResult<Vec<String>>;
49
50    /// Delete a session by ID.
51    fn delete(&mut self, session_id: &str) -> StorageResult<()>;
52}
53
54/// In-memory storage for testing.
55#[derive(Default)]
56pub struct InMemoryStorage {
57    sessions: std::collections::HashMap<String, TxLog>,
58}
59
60impl InMemoryStorage {
61    pub fn new() -> Self {
62        Self::default()
63    }
64}
65
66impl Storage for InMemoryStorage {
67    fn init(&mut self) -> StorageResult<()> {
68        Ok(())
69    }
70
71    fn save(&mut self, log: &TxLog) -> StorageResult<String> {
72        let id = uuid::Uuid::new_v4().to_string();
73        self.sessions.insert(id.clone(), log.clone());
74        Ok(id)
75    }
76
77    fn load(&self, session_id: &str) -> StorageResult<TxLog> {
78        self.sessions
79            .get(session_id)
80            .cloned()
81            .ok_or_else(|| ryo_storage::StorageError::SessionNotFound(session_id.to_string()))
82    }
83
84    fn list_sessions(&self) -> StorageResult<Vec<String>> {
85        Ok(self.sessions.keys().cloned().collect())
86    }
87
88    fn sessions_for_project(&self, _project_path: &Path) -> StorageResult<Vec<String>> {
89        // Simple implementation: return all sessions
90        self.list_sessions()
91    }
92
93    fn delete(&mut self, session_id: &str) -> StorageResult<()> {
94        self.sessions
95            .remove(session_id)
96            .map(|_| ())
97            .ok_or_else(|| ryo_storage::StorageError::SessionNotFound(session_id.to_string()))
98    }
99}
100
101// ============================================================================
102// Tests
103// ============================================================================
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    // ------------------------------------------------------------------------
110    // InMemoryStorage Tests
111    // ------------------------------------------------------------------------
112
113    #[test]
114    fn test_new() {
115        let storage = InMemoryStorage::new();
116        assert!(storage.sessions.is_empty());
117    }
118
119    #[test]
120    fn test_default() {
121        let storage = InMemoryStorage::default();
122        assert!(storage.sessions.is_empty());
123    }
124
125    #[test]
126    fn test_init() {
127        let mut storage = InMemoryStorage::new();
128        assert!(storage.init().is_ok());
129    }
130
131    #[test]
132    fn test_save_and_load() {
133        let mut storage = InMemoryStorage::new();
134        let log = TxLog::new();
135
136        let id = storage.save(&log).unwrap();
137        assert!(!id.is_empty());
138
139        let loaded = storage.load(&id).unwrap();
140        assert_eq!(loaded.entries().len(), log.entries().len());
141    }
142
143    #[test]
144    fn test_load_nonexistent() {
145        let storage = InMemoryStorage::new();
146        let result = storage.load("nonexistent-id");
147        assert!(result.is_err());
148    }
149
150    #[test]
151    fn test_list_sessions_empty() {
152        let storage = InMemoryStorage::new();
153        let sessions = storage.list_sessions().unwrap();
154        assert!(sessions.is_empty());
155    }
156
157    #[test]
158    fn test_list_sessions_with_data() {
159        let mut storage = InMemoryStorage::new();
160        let log1 = TxLog::new();
161        let log2 = TxLog::new();
162
163        let id1 = storage.save(&log1).unwrap();
164        let id2 = storage.save(&log2).unwrap();
165
166        let sessions = storage.list_sessions().unwrap();
167        assert_eq!(sessions.len(), 2);
168        assert!(sessions.contains(&id1));
169        assert!(sessions.contains(&id2));
170    }
171
172    #[test]
173    fn test_sessions_for_project() {
174        let mut storage = InMemoryStorage::new();
175        let log = TxLog::new();
176        storage.save(&log).unwrap();
177
178        // InMemoryStorage returns all sessions regardless of project
179        let sessions = storage
180            .sessions_for_project(Path::new("/some/project"))
181            .unwrap();
182        assert_eq!(sessions.len(), 1);
183    }
184
185    #[test]
186    fn test_delete() {
187        let mut storage = InMemoryStorage::new();
188        let log = TxLog::new();
189        let id = storage.save(&log).unwrap();
190
191        assert!(storage.load(&id).is_ok());
192        assert!(storage.delete(&id).is_ok());
193        assert!(storage.load(&id).is_err());
194    }
195
196    #[test]
197    fn test_delete_nonexistent() {
198        let mut storage = InMemoryStorage::new();
199        let result = storage.delete("nonexistent-id");
200        assert!(result.is_err());
201    }
202
203    #[test]
204    fn test_multiple_saves() {
205        let mut storage = InMemoryStorage::new();
206
207        for i in 0..5 {
208            let log = TxLog::with_project(format!("project-{}", i));
209            storage.save(&log).unwrap();
210        }
211
212        assert_eq!(storage.list_sessions().unwrap().len(), 5);
213    }
214
215    // ------------------------------------------------------------------------
216    // Storage Trait Object Tests
217    // ------------------------------------------------------------------------
218
219    #[test]
220    fn test_as_dyn_storage() {
221        let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
222        let sessions = storage.list_sessions().unwrap();
223        assert!(sessions.is_empty());
224    }
225
226    #[test]
227    fn test_dyn_storage_save_load() {
228        let mut storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
229        let log = TxLog::new();
230
231        let id = storage.save(&log).unwrap();
232        let loaded = storage.load(&id).unwrap();
233        assert_eq!(loaded.entries().len(), log.entries().len());
234    }
235}