git_internal/internal/object/
task.rs1use std::{fmt, str::FromStr};
36
37use serde::{Deserialize, Serialize};
38use uuid::Uuid;
39
40use crate::{
41 errors::GitError,
42 hash::ObjectHash,
43 internal::object::{
44 ObjectTrait,
45 types::{ActorRef, Header, ObjectType},
46 },
47};
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
51#[serde(rename_all = "snake_case")]
52pub enum TaskStatus {
53 Draft,
55 Running,
57 Done,
59 Failed,
61 Cancelled,
63}
64
65impl TaskStatus {
66 pub fn as_str(&self) -> &'static str {
67 match self {
68 TaskStatus::Draft => "draft",
69 TaskStatus::Running => "running",
70 TaskStatus::Done => "done",
71 TaskStatus::Failed => "failed",
72 TaskStatus::Cancelled => "cancelled",
73 }
74 }
75}
76
77impl fmt::Display for TaskStatus {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 write!(f, "{}", self.as_str())
80 }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
87#[serde(rename_all = "snake_case")]
88pub enum GoalType {
89 Feature,
90 Bugfix,
91 Refactor,
92 Docs,
93 Perf,
94 Test,
95 Chore,
96 Build,
97 Ci,
98 Style,
99}
100
101impl GoalType {
102 pub fn as_str(&self) -> &'static str {
103 match self {
104 GoalType::Feature => "feature",
105 GoalType::Bugfix => "bugfix",
106 GoalType::Refactor => "refactor",
107 GoalType::Docs => "docs",
108 GoalType::Perf => "perf",
109 GoalType::Test => "test",
110 GoalType::Chore => "chore",
111 GoalType::Build => "build",
112 GoalType::Ci => "ci",
113 GoalType::Style => "style",
114 }
115 }
116}
117
118impl fmt::Display for GoalType {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 write!(f, "{}", self.as_str())
121 }
122}
123
124impl FromStr for GoalType {
125 type Err = String;
126
127 fn from_str(value: &str) -> Result<Self, Self::Err> {
128 match value {
129 "feature" => Ok(GoalType::Feature),
130 "bugfix" => Ok(GoalType::Bugfix),
131 "refactor" => Ok(GoalType::Refactor),
132 "docs" => Ok(GoalType::Docs),
133 "perf" => Ok(GoalType::Perf),
134 "test" => Ok(GoalType::Test),
135 "chore" => Ok(GoalType::Chore),
136 "build" => Ok(GoalType::Build),
137 "ci" => Ok(GoalType::Ci),
138 "style" => Ok(GoalType::Style),
139 _ => Err(format!("Invalid goal_type: {}", value)),
140 }
141 }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct Task {
150 #[serde(flatten)]
151 header: Header,
152 title: String,
153 description: Option<String>,
154 goal_type: Option<GoalType>,
155 #[serde(default)]
156 constraints: Vec<String>,
157 #[serde(default)]
158 acceptance_criteria: Vec<String>,
159 requested_by: Option<ActorRef>,
160 #[serde(default)]
161 dependencies: Vec<Uuid>,
162 status: TaskStatus,
163}
164
165impl Task {
166 pub fn new(
174 repo_id: Uuid,
175 created_by: ActorRef,
176 title: impl Into<String>,
177 goal_type: Option<GoalType>,
178 ) -> Result<Self, String> {
179 Ok(Self {
180 header: Header::new(ObjectType::Task, repo_id, created_by)?,
181 title: title.into(),
182 description: None,
183 goal_type,
184 constraints: Vec::new(),
185 acceptance_criteria: Vec::new(),
186 requested_by: None,
187 dependencies: Vec::new(),
188 status: TaskStatus::Draft,
189 })
190 }
191
192 pub fn header(&self) -> &Header {
193 &self.header
194 }
195
196 pub fn title(&self) -> &str {
197 &self.title
198 }
199
200 pub fn description(&self) -> Option<&str> {
201 self.description.as_deref()
202 }
203
204 pub fn goal_type(&self) -> Option<&GoalType> {
205 self.goal_type.as_ref()
206 }
207
208 pub fn constraints(&self) -> &[String] {
209 &self.constraints
210 }
211
212 pub fn acceptance_criteria(&self) -> &[String] {
213 &self.acceptance_criteria
214 }
215
216 pub fn requested_by(&self) -> Option<&ActorRef> {
217 self.requested_by.as_ref()
218 }
219
220 pub fn dependencies(&self) -> &[Uuid] {
221 &self.dependencies
222 }
223
224 pub fn status(&self) -> &TaskStatus {
225 &self.status
226 }
227
228 pub fn set_description(&mut self, description: Option<String>) {
229 self.description = description;
230 }
231
232 pub fn add_constraint(&mut self, constraint: impl Into<String>) {
233 self.constraints.push(constraint.into());
234 }
235
236 pub fn add_acceptance_criterion(&mut self, criterion: impl Into<String>) {
237 self.acceptance_criteria.push(criterion.into());
238 }
239
240 pub fn set_requested_by(&mut self, requested_by: Option<ActorRef>) {
241 self.requested_by = requested_by;
242 }
243
244 pub fn add_dependency(&mut self, task_id: Uuid) {
245 self.dependencies.push(task_id);
246 }
247
248 pub fn set_status(&mut self, status: TaskStatus) {
249 self.status = status;
250 }
251}
252
253impl fmt::Display for Task {
254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255 write!(f, "Task: {}", self.header.object_id())
256 }
257}
258
259impl ObjectTrait for Task {
260 fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
261 where
262 Self: Sized,
263 {
264 serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
265 }
266
267 fn get_type(&self) -> ObjectType {
268 ObjectType::Task
269 }
270
271 fn get_size(&self) -> usize {
272 serde_json::to_vec(self).map(|v| v.len()).unwrap_or(0)
273 }
274
275 fn to_data(&self) -> Result<Vec<u8>, GitError> {
276 serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use crate::internal::object::types::ActorKind;
284
285 #[test]
286 fn test_task_creation() {
287 let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
288 let actor = ActorRef::human("jackie").expect("actor");
289 let mut task = Task::new(repo_id, actor, "Fix bug", Some(GoalType::Bugfix)).expect("task");
290
291 let dep_id = Uuid::from_u128(0x00000000000000000000000000000001);
293 task.add_dependency(dep_id);
294
295 assert_eq!(task.header().object_type(), &ObjectType::Task);
296 assert_eq!(task.status(), &TaskStatus::Draft);
297 assert_eq!(task.goal_type(), Some(&GoalType::Bugfix));
298 assert_eq!(task.dependencies().len(), 1);
299 assert_eq!(task.dependencies()[0], dep_id);
300 }
301
302 #[test]
303 fn test_task_goal_type_optional() {
304 let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
305 let actor = ActorRef::human("jackie").expect("actor");
306 let task = Task::new(repo_id, actor, "Write docs", None).expect("task");
307
308 assert!(task.goal_type().is_none());
309 }
310
311 #[test]
312 fn test_task_requested_by() {
313 let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
314 let actor = ActorRef::human("jackie").expect("actor");
315 let mut task =
316 Task::new(repo_id, actor.clone(), "Fix bug", Some(GoalType::Bugfix)).expect("task");
317
318 task.set_requested_by(Some(ActorRef::mcp_client("vscode-client").expect("actor")));
319
320 assert!(task.requested_by().is_some());
321 assert_eq!(task.requested_by().unwrap().kind(), &ActorKind::McpClient);
322 }
323}