Skip to main content

rustodo/storage/
json.rs

1//! JSON file-based storage implementation
2
3use anyhow::{Context, Result};
4use directories::ProjectDirs;
5use std::{fs, path::PathBuf};
6
7use super::Storage;
8use crate::models::Task;
9
10/// JSON file-based storage implementation
11pub struct JsonStorage {
12    file_path: PathBuf,
13}
14
15impl JsonStorage {
16    /// Create a new JSON storage using default OS-specific location
17    pub fn new() -> Result<Self> {
18        let file_path = get_data_file_path()?;
19        Ok(Self { file_path })
20    }
21
22    /// Create a JSON storage at a custom path (for testing)
23    #[cfg(test)]
24    pub fn with_path(file_path: PathBuf) -> Self {
25        Self { file_path }
26    }
27}
28
29impl Storage for JsonStorage {
30    fn load(&self) -> Result<Vec<Task>> {
31        match fs::read_to_string(&self.file_path) {
32            Ok(content) => serde_json::from_str(&content)
33                .context("Failed to parse todos.json - file may be corrupted"),
34            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
35            Err(e) => Err(e).context(format!(
36                "Failed to read todos.json from: {}",
37                self.file_path.display()
38            )),
39        }
40    }
41
42    fn save(&self, tasks: &[Task]) -> Result<()> {
43        let json =
44            serde_json::to_string_pretty(tasks).context("Failed to serialize tasks to JSON")?;
45
46        fs::write(&self.file_path, json).context(format!(
47            "Failed to write to {} - check file permissions",
48            self.file_path.display()
49        ))?;
50
51        Ok(())
52    }
53
54    fn location(&self) -> String {
55        self.file_path.display().to_string()
56    }
57}
58
59/// Returns the path to the todos.json file (re-exported for compatibility)
60pub fn get_data_file_path() -> Result<PathBuf> {
61    let project_dirs =
62        ProjectDirs::from("", "", "todo-cli").context("Failed to determine project directories")?;
63
64    let data_dir = project_dirs.data_dir();
65    fs::create_dir_all(data_dir).context(format!(
66        "Failed to create data directory: {}",
67        data_dir.display()
68    ))?;
69
70    let mut path = data_dir.to_path_buf();
71    path.push("todos.json");
72
73    Ok(path)
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::models::Priority;
80    use tempfile::TempDir;
81
82    #[test]
83    fn test_json_storage_save_and_load() {
84        let temp = TempDir::new().unwrap();
85        let path = temp.path().join("test.json");
86
87        let storage = JsonStorage::with_path(path.clone());
88
89        let tasks = vec![crate::models::Task::new(
90            "Test task".to_string(),
91            Priority::Medium,
92            vec![],
93            None,
94            None,
95            None,
96        )];
97        storage.save(&tasks).unwrap();
98
99        assert!(path.exists());
100
101        let loaded = storage.load().unwrap();
102        assert_eq!(loaded.len(), 1);
103        assert_eq!(loaded[0].text, "Test task");
104    }
105
106    #[test]
107    fn test_json_storage_empty_file() {
108        let temp = TempDir::new().unwrap();
109        let path = temp.path().join("empty.json");
110
111        let storage = JsonStorage::with_path(path);
112
113        let tasks = storage.load().unwrap();
114        assert_eq!(tasks.len(), 0);
115    }
116}