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