1use anyhow::{Context, Result};
4use directories::ProjectDirs;
5use std::{fs, path::PathBuf};
6
7use super::Storage;
8use crate::models::Task;
9
10pub struct JsonStorage {
12 file_path: PathBuf,
13}
14
15impl JsonStorage {
16 pub fn new() -> Result<Self> {
18 let file_path = get_data_file_path()?;
19 Ok(Self { file_path })
20 }
21
22 #[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
59pub 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}