ito_core/
task_repository.rs1use std::path::Path;
4
5use ito_common::fs::{FileSystem, StdFs};
6use ito_domain::errors::{DomainError, DomainResult};
7use ito_domain::tasks::{
8 TaskRepository as DomainTaskRepository, TasksParseResult, parse_tasks_tracking_file,
9 tasks_path_checked,
10};
11
12pub struct FsTaskRepository<'a, F: FileSystem = StdFs> {
14 ito_path: &'a Path,
15 fs: F,
16}
17
18impl<'a> FsTaskRepository<'a, StdFs> {
19 pub fn new(ito_path: &'a Path) -> Self {
21 Self::with_fs(ito_path, StdFs)
22 }
23}
24
25impl<'a, F: FileSystem> FsTaskRepository<'a, F> {
26 pub fn with_fs(ito_path: &'a Path, fs: F) -> Self {
28 Self { ito_path, fs }
29 }
30}
31
32impl<F: FileSystem> DomainTaskRepository for FsTaskRepository<'_, F> {
33 fn load_tasks(&self, change_id: &str) -> DomainResult<TasksParseResult> {
34 let Some(path) = tasks_path_checked(self.ito_path, change_id) else {
35 return Ok(TasksParseResult::empty());
36 };
37 if !self.fs.is_file(&path) {
38 return Ok(TasksParseResult::empty());
39 }
40 let contents = self
41 .fs
42 .read_to_string(&path)
43 .map_err(|source| DomainError::io("reading tasks file", source))?;
44 Ok(parse_tasks_tracking_file(&contents))
45 }
46}
47
48pub type TaskRepository<'a> = FsTaskRepository<'a, StdFs>;
50
51#[cfg(test)]
52mod tests {
53 use std::fs;
54 use std::path::Path;
55
56 use tempfile::TempDir;
57
58 use super::TaskRepository;
59 use ito_domain::tasks::TaskRepository as DomainTaskRepository;
60
61 fn setup_test_change(ito_dir: &Path, change_id: &str, tasks_content: &str) {
62 let change_dir = ito_dir.join("changes").join(change_id);
63 fs::create_dir_all(&change_dir).unwrap();
64 fs::write(change_dir.join("tasks.md"), tasks_content).unwrap();
65 }
66
67 #[test]
68 fn test_get_task_counts_checkbox_format() {
69 let tmp = TempDir::new().unwrap();
70 let ito_path = tmp.path().join(".ito");
71 fs::create_dir_all(&ito_path).unwrap();
72
73 setup_test_change(
74 &ito_path,
75 "001-01_test",
76 r#"# Tasks
77
78- [x] Task 1
79- [x] Task 2
80- [ ] Task 3
81- [ ] Task 4
82"#,
83 );
84
85 let repo = TaskRepository::new(&ito_path);
86 let (completed, total) = repo.get_task_counts("001-01_test").unwrap();
87
88 assert_eq!(completed, 2);
89 assert_eq!(total, 4);
90 }
91
92 #[test]
93 fn test_get_task_counts_enhanced_format() {
94 let tmp = TempDir::new().unwrap();
95 let ito_path = tmp.path().join(".ito");
96 fs::create_dir_all(&ito_path).unwrap();
97
98 setup_test_change(
99 &ito_path,
100 "001-02_enhanced",
101 r#"# Tasks
102
103## Wave 1
104- **Depends On**: None
105
106### Task 1.1: First task
107- **Status**: [x] complete
108- **Updated At**: 2024-01-01
109
110### Task 1.2: Second task
111- **Status**: [ ] pending
112- **Updated At**: 2024-01-01
113
114### Task 1.3: Third task
115- **Status**: [x] complete
116- **Updated At**: 2024-01-01
117"#,
118 );
119
120 let repo = TaskRepository::new(&ito_path);
121 let (completed, total) = repo.get_task_counts("001-02_enhanced").unwrap();
122
123 assert_eq!(completed, 2);
124 assert_eq!(total, 3);
125 }
126
127 #[test]
128 fn test_missing_tasks_file_returns_zero() {
129 let tmp = TempDir::new().unwrap();
130 let ito_path = tmp.path().join(".ito");
131 fs::create_dir_all(&ito_path).unwrap();
132
133 let repo = TaskRepository::new(&ito_path);
134 let (completed, total) = repo.get_task_counts("nonexistent").unwrap();
135
136 assert_eq!(completed, 0);
137 assert_eq!(total, 0);
138 }
139
140 #[test]
141 fn test_has_tasks() {
142 let tmp = TempDir::new().unwrap();
143 let ito_path = tmp.path().join(".ito");
144 fs::create_dir_all(&ito_path).unwrap();
145
146 setup_test_change(&ito_path, "001-01_with-tasks", "# Tasks\n- [ ] Task 1\n");
147 setup_test_change(&ito_path, "001-02_no-tasks", "# Tasks\n\nNo tasks yet.\n");
148
149 let repo = TaskRepository::new(&ito_path);
150
151 assert!(repo.has_tasks("001-01_with-tasks").unwrap());
152 assert!(!repo.has_tasks("001-02_no-tasks").unwrap());
153 assert!(!repo.has_tasks("nonexistent").unwrap());
154 }
155}