tsk/
task_storage.rs

1use crate::context::file_system::FileSystemOperations;
2use crate::storage::XdgDirectories;
3use crate::task::{Task, TaskStatus};
4use std::path::PathBuf;
5use std::sync::Arc;
6use tokio::sync::Mutex;
7
8// Trait for task storage abstraction
9#[async_trait::async_trait]
10pub trait TaskStorage: Send + Sync {
11    async fn add_task(&self, task: Task) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
12    #[allow(dead_code)] // Will be used in future functionality (e.g., task details view)
13    async fn get_task(
14        &self,
15        id: &str,
16    ) -> Result<Option<Task>, Box<dyn std::error::Error + Send + Sync>>;
17    async fn list_tasks(&self) -> Result<Vec<Task>, Box<dyn std::error::Error + Send + Sync>>;
18    async fn update_task(&self, task: Task)
19    -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
20    async fn delete_task(&self, id: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
21    async fn delete_tasks_by_status(
22        &self,
23        statuses: Vec<TaskStatus>,
24    ) -> Result<usize, Box<dyn std::error::Error + Send + Sync>>;
25}
26
27// JSON file-based implementation
28pub struct JsonTaskStorage {
29    file_path: PathBuf,
30    lock: Arc<Mutex<()>>,
31    file_system: Arc<dyn FileSystemOperations>,
32    #[allow(dead_code)]
33    xdg_directories: Arc<XdgDirectories>,
34}
35
36impl JsonTaskStorage {
37    pub fn new(
38        xdg_directories: Arc<XdgDirectories>,
39        file_system: Arc<dyn FileSystemOperations>,
40    ) -> Self {
41        let file_path = xdg_directories.tasks_file();
42
43        Self {
44            file_path,
45            lock: Arc::new(Mutex::new(())),
46            file_system,
47            xdg_directories,
48        }
49    }
50
51    async fn read_tasks(&self) -> Result<Vec<Task>, Box<dyn std::error::Error + Send + Sync>> {
52        if !self
53            .file_system
54            .exists(&self.file_path)
55            .await
56            .map_err(|e| e.to_string())?
57        {
58            return Ok(Vec::new());
59        }
60
61        let contents = self
62            .file_system
63            .read_file(&self.file_path)
64            .await
65            .map_err(|e| e.to_string())?;
66        let tasks: Vec<Task> = serde_json::from_str(&contents)?;
67        Ok(tasks)
68    }
69
70    async fn write_tasks(
71        &self,
72        tasks: &[Task],
73    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
74        let contents = serde_json::to_string_pretty(tasks)?;
75        self.file_system
76            .write_file(&self.file_path, &contents)
77            .await
78            .map_err(|e| e.to_string())?;
79        Ok(())
80    }
81}
82
83#[async_trait::async_trait]
84impl TaskStorage for JsonTaskStorage {
85    async fn add_task(&self, task: Task) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
86        let _lock = self.lock.lock().await;
87
88        let mut tasks = self.read_tasks().await?;
89        tasks.push(task);
90        self.write_tasks(&tasks).await?;
91
92        Ok(())
93    }
94
95    async fn get_task(
96        &self,
97        id: &str,
98    ) -> Result<Option<Task>, Box<dyn std::error::Error + Send + Sync>> {
99        let tasks = self.read_tasks().await?;
100        Ok(tasks.into_iter().find(|t| t.id == id))
101    }
102
103    async fn list_tasks(&self) -> Result<Vec<Task>, Box<dyn std::error::Error + Send + Sync>> {
104        self.read_tasks().await
105    }
106
107    async fn update_task(
108        &self,
109        task: Task,
110    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
111        let _lock = self.lock.lock().await;
112
113        let mut tasks = self.read_tasks().await?;
114        if let Some(index) = tasks.iter().position(|t| t.id == task.id) {
115            tasks[index] = task;
116            self.write_tasks(&tasks).await?;
117            Ok(())
118        } else {
119            Err("Task not found".into())
120        }
121    }
122
123    async fn delete_task(&self, id: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
124        let _lock = self.lock.lock().await;
125
126        let mut tasks = self.read_tasks().await?;
127        if let Some(index) = tasks.iter().position(|t| t.id == id) {
128            tasks.remove(index);
129            self.write_tasks(&tasks).await?;
130            Ok(())
131        } else {
132            Err("Task not found".into())
133        }
134    }
135
136    async fn delete_tasks_by_status(
137        &self,
138        statuses: Vec<TaskStatus>,
139    ) -> Result<usize, Box<dyn std::error::Error + Send + Sync>> {
140        let _lock = self.lock.lock().await;
141
142        let mut tasks = self.read_tasks().await?;
143        let original_count = tasks.len();
144        tasks.retain(|t| !statuses.contains(&t.status));
145        let deleted_count = original_count - tasks.len();
146        self.write_tasks(&tasks).await?;
147        Ok(deleted_count)
148    }
149}
150
151// Factory function for getting task storage
152pub fn get_task_storage(
153    xdg_directories: Arc<XdgDirectories>,
154    file_system: Arc<dyn FileSystemOperations>,
155) -> Box<dyn TaskStorage> {
156    let storage = JsonTaskStorage::new(xdg_directories, file_system);
157    Box::new(storage)
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use crate::context::file_system::DefaultFileSystem;
164    use crate::task::Task;
165    use tempfile::TempDir;
166
167    fn create_test_xdg_directories(temp_dir: &TempDir) -> Arc<XdgDirectories> {
168        unsafe {
169            std::env::set_var("XDG_DATA_HOME", temp_dir.path().join("data"));
170        }
171        unsafe {
172            std::env::set_var("XDG_RUNTIME_DIR", temp_dir.path().join("runtime"));
173        }
174        let xdg = XdgDirectories::new().unwrap();
175        xdg.ensure_directories().unwrap();
176        Arc::new(xdg)
177    }
178
179    #[tokio::test]
180    async fn test_json_task_storage() {
181        let temp_dir = TempDir::new().unwrap();
182        let file_system = Arc::new(DefaultFileSystem);
183        let xdg_directories = create_test_xdg_directories(&temp_dir);
184        let storage = JsonTaskStorage::new(xdg_directories.clone(), file_system);
185
186        // Test adding a task
187        let task = Task::new(
188            "2025-06-26-0900-feature-test-task".to_string(),
189            temp_dir.path().to_path_buf(),
190            "test-task".to_string(),
191            "feature".to_string(),
192            "instructions.md".to_string(),
193            "claude-code".to_string(),
194            30,
195            "tsk/test-task".to_string(),
196            "abc123".to_string(),
197            "default".to_string(),
198            "default".to_string(),
199            chrono::Local::now(),
200            temp_dir.path().to_path_buf(),
201        );
202
203        storage.add_task(task.clone()).await.unwrap();
204
205        // Test getting a task
206        let retrieved = storage.get_task(&task.id).await.unwrap();
207        assert!(retrieved.is_some());
208        assert_eq!(retrieved.unwrap().name, "test-task");
209
210        // Test listing tasks
211        let tasks = storage.list_tasks().await.unwrap();
212        assert_eq!(tasks.len(), 1);
213
214        // Test updating a task
215        let mut updated_task = task.clone();
216        updated_task.status = TaskStatus::Running;
217        storage.update_task(updated_task).await.unwrap();
218
219        let retrieved = storage.get_task(&task.id).await.unwrap().unwrap();
220        assert_eq!(retrieved.status, TaskStatus::Running);
221
222        // Test deleting a task
223        storage.delete_task(&task.id).await.unwrap();
224        let retrieved = storage.get_task(&task.id).await.unwrap();
225        assert!(retrieved.is_none());
226
227        // Test deleting tasks by status
228        let task1 = Task::new(
229            "2025-06-26-0901-feature-task1".to_string(),
230            temp_dir.path().to_path_buf(),
231            "task1".to_string(),
232            "feature".to_string(),
233            "instructions.md".to_string(),
234            "claude-code".to_string(),
235            30,
236            "tsk/task1".to_string(),
237            "abc123".to_string(),
238            "default".to_string(),
239            "default".to_string(),
240            chrono::Local::now(),
241            temp_dir.path().to_path_buf(),
242        );
243        let mut task2 = Task::new(
244            "2025-06-26-0902-bug-fix-task2".to_string(),
245            temp_dir.path().to_path_buf(),
246            "task2".to_string(),
247            "bug-fix".to_string(),
248            "instructions.md".to_string(),
249            "claude-code".to_string(),
250            30,
251            "tsk/task2".to_string(),
252            "abc123".to_string(),
253            "default".to_string(),
254            "default".to_string(),
255            chrono::Local::now(),
256            temp_dir.path().to_path_buf(),
257        );
258        task2.status = TaskStatus::Complete;
259        let mut task3 = Task::new(
260            "2025-06-26-0903-refactor-task3".to_string(),
261            temp_dir.path().to_path_buf(),
262            "task3".to_string(),
263            "refactor".to_string(),
264            "instructions.md".to_string(),
265            "claude-code".to_string(),
266            30,
267            "tsk/task3".to_string(),
268            "abc123".to_string(),
269            "default".to_string(),
270            "default".to_string(),
271            chrono::Local::now(),
272            temp_dir.path().to_path_buf(),
273        );
274        task3.status = TaskStatus::Failed;
275
276        storage.add_task(task1.clone()).await.unwrap();
277        storage.add_task(task2.clone()).await.unwrap();
278        storage.add_task(task3.clone()).await.unwrap();
279
280        // Delete completed and failed tasks
281        let deleted_count = storage
282            .delete_tasks_by_status(vec![TaskStatus::Complete, TaskStatus::Failed])
283            .await
284            .unwrap();
285        assert_eq!(deleted_count, 2);
286
287        // Verify only queued task remains
288        let remaining_tasks = storage.list_tasks().await.unwrap();
289        assert_eq!(remaining_tasks.len(), 1);
290        assert_eq!(remaining_tasks[0].status, TaskStatus::Queued);
291    }
292}