ccboard_core/parsers/
task.rs1use anyhow::{Context, Result};
6use serde::Deserialize;
7use std::fs;
8use std::path::Path;
9
10#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum TaskStatus {
14 Pending,
15 InProgress,
16 Completed,
17}
18
19#[derive(Debug, Clone, PartialEq, Deserialize)]
21pub struct Task {
22 pub id: String,
23 pub status: TaskStatus,
24 pub subject: String,
25 pub description: Option<String>,
26 pub blocked_by: Vec<String>,
27}
28
29pub struct TaskParser;
31
32impl TaskParser {
33 pub fn parse(json: &str) -> Result<Task> {
37 let task: Task = serde_json::from_str(json).context("Failed to parse task JSON")?;
39 Ok(task)
40 }
41
42 pub fn load(path: &Path) -> Result<Task> {
44 let content = fs::read_to_string(path)
45 .with_context(|| format!("Failed to read task file: {}", path.display()))?;
46
47 Self::parse(&content)
48 .with_context(|| format!("Failed to parse task from: {}", path.display()))
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 #[test]
59 fn test_parses_minimal_pending_task() {
60 let json = r#"{
61 "id": "task-1",
62 "status": "pending",
63 "subject": "Write tests first",
64 "blocked_by": []
65 }"#;
66
67 let task = TaskParser::parse(json).unwrap();
68
69 assert_eq!(task.id, "task-1");
70 assert_eq!(task.status, TaskStatus::Pending);
71 assert_eq!(task.subject, "Write tests first");
72 assert!(task.blocked_by.is_empty());
73 assert!(task.description.is_none());
74 }
75
76 #[test]
78 fn test_parses_task_with_description_and_dependencies() {
79 let json = r#"{
80 "id": "task-2",
81 "status": "inprogress",
82 "subject": "Implement feature",
83 "description": "Detailed implementation steps",
84 "blocked_by": ["task-1", "task-3"]
85 }"#;
86
87 let task = TaskParser::parse(json).unwrap();
88
89 assert_eq!(task.id, "task-2");
90 assert_eq!(task.status, TaskStatus::InProgress);
91 assert_eq!(task.subject, "Implement feature");
92 assert_eq!(
93 task.description,
94 Some("Detailed implementation steps".to_string())
95 );
96 assert_eq!(task.blocked_by, vec!["task-1", "task-3"]);
97 }
98
99 #[test]
101 fn test_parses_completed_task() {
102 let json = r#"{
103 "id": "task-3",
104 "status": "completed",
105 "subject": "Done task",
106 "blocked_by": []
107 }"#;
108
109 let task = TaskParser::parse(json).unwrap();
110 assert_eq!(task.status, TaskStatus::Completed);
111 }
112
113 #[test]
115 fn test_invalid_json_returns_error_with_context() {
116 let invalid_json = "{ invalid json }";
117
118 let result = TaskParser::parse(invalid_json);
119
120 assert!(result.is_err());
121 let err_msg = format!("{:?}", result.unwrap_err());
122 assert!(err_msg.contains("Failed to parse task JSON"));
123 }
124
125 #[test]
127 fn test_missing_required_field_returns_error() {
128 let json = r#"{
129 "id": "task-4",
130 "status": "pending"
131 }"#;
132
133 let result = TaskParser::parse(json);
134
135 assert!(result.is_err());
137 }
138
139 #[test]
141 fn test_unknown_status_returns_error() {
142 let json = r#"{
143 "id": "task-5",
144 "status": "invalid_status",
145 "subject": "Test",
146 "blocked_by": []
147 }"#;
148
149 let result = TaskParser::parse(json);
150
151 assert!(result.is_err());
153 }
154
155 #[test]
157 fn test_load_from_file() {
158 use std::io::Write;
159 use tempfile::NamedTempFile;
160
161 let json = r#"{
162 "id": "task-file",
163 "status": "pending",
164 "subject": "Test from file",
165 "blocked_by": []
166 }"#;
167
168 let mut temp_file = NamedTempFile::new().unwrap();
169 temp_file.write_all(json.as_bytes()).unwrap();
170
171 let task = TaskParser::load(temp_file.path()).unwrap();
172
173 assert_eq!(task.id, "task-file");
174 assert_eq!(task.subject, "Test from file");
175 }
176
177 #[test]
179 fn test_load_from_missing_file_returns_error() {
180 use std::path::PathBuf;
181
182 let path = PathBuf::from("/nonexistent/path/task.json");
183 let result = TaskParser::load(&path);
184
185 assert!(result.is_err());
186 let err_msg = format!("{:?}", result.unwrap_err());
187 assert!(err_msg.contains("Failed to read task file"));
188 }
189
190 #[test]
192 fn test_parse_real_fixture_pending() {
193 let fixture = include_str!("../../tests/fixtures/tasks/task-pending.json");
194 let task = TaskParser::parse(fixture).unwrap();
195
196 assert_eq!(task.id, "task-123");
197 assert_eq!(task.status, TaskStatus::Pending);
198 assert!(task.description.is_some());
199 assert!(task.blocked_by.is_empty());
200 }
201
202 #[test]
203 fn test_parse_real_fixture_with_dependencies() {
204 let fixture = include_str!("../../tests/fixtures/tasks/task-inprogress.json");
205 let task = TaskParser::parse(fixture).unwrap();
206
207 assert_eq!(task.id, "task-456");
208 assert_eq!(task.status, TaskStatus::InProgress);
209 assert_eq!(task.blocked_by, vec!["task-123"]);
210 }
211}