1use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use std::path::Path;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum TaskStatus {
11 Pending,
13 InProgress,
15 Completed,
17}
18
19impl std::fmt::Display for TaskStatus {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 match self {
22 TaskStatus::Pending => write!(f, "pending"),
23 TaskStatus::InProgress => write!(f, "in_progress"),
24 TaskStatus::Completed => write!(f, "completed"),
25 }
26 }
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct TeamTask {
32 #[serde(default)]
34 pub id: String,
35 #[serde(default)]
37 pub subject: String,
38 #[serde(default)]
40 pub description: String,
41 #[serde(default, rename = "activeForm")]
43 pub active_form: Option<String>,
44 #[serde(default = "default_task_status")]
46 pub status: TaskStatus,
47 #[serde(default)]
49 pub owner: Option<String>,
50 #[serde(default)]
52 pub blocks: Vec<String>,
53 #[serde(default, rename = "blockedBy")]
55 pub blocked_by: Vec<String>,
56}
57
58fn default_task_status() -> TaskStatus {
60 TaskStatus::Pending
61}
62
63pub fn read_task(task_path: &Path) -> Result<TeamTask> {
68 let content = std::fs::read_to_string(task_path)
69 .with_context(|| format!("Failed to read task file: {:?}", task_path))?;
70
71 let task: TeamTask = serde_json::from_str(&content)
72 .with_context(|| format!("Failed to parse task file: {:?}", task_path))?;
73
74 Ok(task)
75}
76
77pub fn read_all_tasks(tasks_dir: &Path) -> Result<Vec<TeamTask>> {
84 if !tasks_dir.exists() {
85 return Ok(Vec::new());
86 }
87
88 let mut tasks = Vec::new();
89 let entries = std::fs::read_dir(tasks_dir)
90 .with_context(|| format!("Failed to read tasks directory: {:?}", tasks_dir))?;
91
92 for entry in entries {
93 let entry = entry?;
94 let path = entry.path();
95
96 if path.extension().and_then(|s| s.to_str()) != Some("json") {
98 continue;
99 }
100
101 let is_numeric = path
103 .file_stem()
104 .and_then(|s| s.to_str())
105 .map(|s| s.chars().all(|c| c.is_ascii_digit()))
106 .unwrap_or(false);
107
108 if !is_numeric {
109 continue;
110 }
111
112 match read_task(&path) {
113 Ok(mut task) => {
114 if task.id.is_empty() {
116 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
117 task.id = stem.to_string();
118 }
119 }
120 tasks.push(task);
121 }
122 Err(e) => {
123 eprintln!("Warning: Failed to read task file {:?}: {}", path, e);
124 }
125 }
126 }
127
128 tasks.sort_by(|a, b| {
130 let id_a: u64 = a.id.parse().unwrap_or(u64::MAX);
131 let id_b: u64 = b.id.parse().unwrap_or(u64::MAX);
132 id_a.cmp(&id_b)
133 });
134
135 Ok(tasks)
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn test_task_deserialization() {
144 let json = r#"{
145 "id": "1",
146 "subject": "Fix authentication bug",
147 "description": "The login flow has a bug",
148 "activeForm": "Fixing authentication bug",
149 "status": "in_progress",
150 "owner": "dev",
151 "blocks": ["3"],
152 "blockedBy": []
153 }"#;
154
155 let task: TeamTask = serde_json::from_str(json).unwrap();
156 assert_eq!(task.id, "1");
157 assert_eq!(task.subject, "Fix authentication bug");
158 assert_eq!(task.status, TaskStatus::InProgress);
159 assert_eq!(task.owner.as_deref(), Some("dev"));
160 assert_eq!(task.blocks, vec!["3"]);
161 assert!(task.blocked_by.is_empty());
162 assert_eq!(
163 task.active_form.as_deref(),
164 Some("Fixing authentication bug")
165 );
166 }
167
168 #[test]
169 fn test_task_default_status() {
170 let json = r#"{
171 "id": "2",
172 "subject": "Write tests"
173 }"#;
174
175 let task: TeamTask = serde_json::from_str(json).unwrap();
176 assert_eq!(task.status, TaskStatus::Pending);
177 assert!(task.owner.is_none());
178 assert!(task.blocks.is_empty());
179 assert!(task.blocked_by.is_empty());
180 }
181
182 #[test]
183 fn test_task_forward_compat() {
184 let json = r#"{
185 "id": "1",
186 "subject": "Test",
187 "status": "completed",
188 "unknown_field": true
189 }"#;
190
191 let task: TeamTask = serde_json::from_str(json).unwrap();
192 assert_eq!(task.status, TaskStatus::Completed);
193 }
194
195 #[test]
196 fn test_task_status_display() {
197 assert_eq!(TaskStatus::Pending.to_string(), "pending");
198 assert_eq!(TaskStatus::InProgress.to_string(), "in_progress");
199 assert_eq!(TaskStatus::Completed.to_string(), "completed");
200 }
201}