git_internal/internal/object/
task.rs1use std::{fmt, str::FromStr};
30
31use serde::{Deserialize, Serialize};
32use uuid::Uuid;
33
34use crate::{
35 errors::GitError,
36 hash::ObjectHash,
37 internal::object::{
38 ObjectTrait,
39 types::{ActorRef, Header, ObjectType},
40 },
41};
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
44#[serde(rename_all = "snake_case")]
45pub enum GoalType {
46 Feature,
47 Bugfix,
48 Refactor,
49 Docs,
50 Perf,
51 Test,
52 Chore,
53 Build,
54 Ci,
55 Style,
56 Other(String),
57}
58
59impl GoalType {
60 pub fn as_str(&self) -> &str {
63 match self {
64 GoalType::Feature => "feature",
65 GoalType::Bugfix => "bugfix",
66 GoalType::Refactor => "refactor",
67 GoalType::Docs => "docs",
68 GoalType::Perf => "perf",
69 GoalType::Test => "test",
70 GoalType::Chore => "chore",
71 GoalType::Build => "build",
72 GoalType::Ci => "ci",
73 GoalType::Style => "style",
74 GoalType::Other(value) => value.as_str(),
75 }
76 }
77}
78
79impl fmt::Display for GoalType {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 write!(f, "{}", self.as_str())
82 }
83}
84
85impl FromStr for GoalType {
86 type Err = String;
87
88 fn from_str(value: &str) -> Result<Self, Self::Err> {
89 match value {
90 "feature" => Ok(GoalType::Feature),
91 "bugfix" => Ok(GoalType::Bugfix),
92 "refactor" => Ok(GoalType::Refactor),
93 "docs" => Ok(GoalType::Docs),
94 "perf" => Ok(GoalType::Perf),
95 "test" => Ok(GoalType::Test),
96 "chore" => Ok(GoalType::Chore),
97 "build" => Ok(GoalType::Build),
98 "ci" => Ok(GoalType::Ci),
99 "style" => Ok(GoalType::Style),
100 _ => Ok(GoalType::Other(value.to_string())),
101 }
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(deny_unknown_fields)]
112pub struct Task {
113 #[serde(flatten)]
116 header: Header,
117 title: String,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
121 description: Option<String>,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
124 goal: Option<GoalType>,
125 #[serde(default, skip_serializing_if = "Vec::is_empty")]
127 constraints: Vec<String>,
128 #[serde(default, skip_serializing_if = "Vec::is_empty")]
130 acceptance_criteria: Vec<String>,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
133 requester: Option<ActorRef>,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
137 parent: Option<Uuid>,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
140 intent: Option<Uuid>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
143 origin_step_id: Option<Uuid>,
144 #[serde(default, skip_serializing_if = "Vec::is_empty")]
146 dependencies: Vec<Uuid>,
147}
148
149impl Task {
150 pub fn new(
153 created_by: ActorRef,
154 title: impl Into<String>,
155 goal: Option<GoalType>,
156 ) -> Result<Self, String> {
157 Ok(Self {
158 header: Header::new(ObjectType::Task, created_by)?,
159 title: title.into(),
160 description: None,
161 goal,
162 constraints: Vec::new(),
163 acceptance_criteria: Vec::new(),
164 requester: None,
165 parent: None,
166 intent: None,
167 origin_step_id: None,
168 dependencies: Vec::new(),
169 })
170 }
171
172 pub fn header(&self) -> &Header {
174 &self.header
175 }
176
177 pub fn title(&self) -> &str {
179 &self.title
180 }
181
182 pub fn description(&self) -> Option<&str> {
184 self.description.as_deref()
185 }
186
187 pub fn goal(&self) -> Option<&GoalType> {
189 self.goal.as_ref()
190 }
191
192 pub fn constraints(&self) -> &[String] {
194 &self.constraints
195 }
196
197 pub fn acceptance_criteria(&self) -> &[String] {
199 &self.acceptance_criteria
200 }
201
202 pub fn requester(&self) -> Option<&ActorRef> {
204 self.requester.as_ref()
205 }
206
207 pub fn parent(&self) -> Option<Uuid> {
210 self.parent
211 }
212
213 pub fn intent(&self) -> Option<Uuid> {
215 self.intent
216 }
217
218 pub fn origin_step_id(&self) -> Option<Uuid> {
221 self.origin_step_id
222 }
223
224 pub fn dependencies(&self) -> &[Uuid] {
226 &self.dependencies
227 }
228
229 pub fn set_description(&mut self, description: Option<String>) {
231 self.description = description;
232 }
233
234 pub fn add_constraint(&mut self, constraint: impl Into<String>) {
236 self.constraints.push(constraint.into());
237 }
238
239 pub fn add_acceptance_criterion(&mut self, criterion: impl Into<String>) {
241 self.acceptance_criteria.push(criterion.into());
242 }
243
244 pub fn set_requester(&mut self, requester: Option<ActorRef>) {
246 self.requester = requester;
247 }
248
249 pub fn set_parent(&mut self, parent: Option<Uuid>) {
251 self.parent = parent;
252 }
253
254 pub fn set_intent(&mut self, intent: Option<Uuid>) {
256 self.intent = intent;
257 }
258
259 pub fn set_origin_step_id(&mut self, origin_step_id: Option<Uuid>) {
261 self.origin_step_id = origin_step_id;
262 }
263
264 pub fn add_dependency(&mut self, task_id: Uuid) {
266 self.dependencies.push(task_id);
267 }
268}
269
270impl fmt::Display for Task {
271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272 write!(f, "Task: {}", self.header.object_id())
273 }
274}
275
276impl ObjectTrait for Task {
277 fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
278 where
279 Self: Sized,
280 {
281 serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
282 }
283
284 fn get_type(&self) -> ObjectType {
285 ObjectType::Task
286 }
287
288 fn get_size(&self) -> usize {
289 match serde_json::to_vec(self) {
290 Ok(v) => v.len(),
291 Err(e) => {
292 tracing::warn!("failed to compute Task size: {}", e);
293 0
294 }
295 }
296 }
297
298 fn to_data(&self) -> Result<Vec<u8>, GitError> {
299 serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn test_task_creation() {
309 let actor = ActorRef::agent("worker").expect("actor");
310 let task = Task::new(actor, "Implement pagination", Some(GoalType::Feature)).expect("task");
311
312 assert_eq!(task.title(), "Implement pagination");
313 assert_eq!(task.goal(), Some(&GoalType::Feature));
314 assert!(task.origin_step_id().is_none());
315 }
316
317 #[test]
318 fn test_task_requester() {
319 let actor = ActorRef::agent("worker").expect("actor");
320 let requester = ActorRef::human("alice").expect("requester");
321 let mut task = Task::new(actor, "Implement pagination", None).expect("task");
322 task.set_requester(Some(requester.clone()));
323 assert_eq!(task.requester(), Some(&requester));
324 }
325
326 #[test]
327 fn test_task_goal_optional() {
328 let actor = ActorRef::agent("worker").expect("actor");
329 let task = Task::new(actor, "Investigate", None).expect("task");
330 assert!(task.goal().is_none());
331 }
332
333 #[test]
334 fn test_task_origin_step_id() {
335 let actor = ActorRef::agent("worker").expect("actor");
336 let mut task = Task::new(actor, "Implement pagination", None).expect("task");
337 let step_id = Uuid::from_u128(0x1234);
338 task.set_origin_step_id(Some(step_id));
339 assert_eq!(task.origin_step_id(), Some(step_id));
340 }
341
342 #[test]
343 fn test_task_dependencies() {
344 let actor = ActorRef::agent("worker").expect("actor");
345 let mut task = Task::new(actor, "Implement pagination", None).expect("task");
346 let dep = Uuid::from_u128(0xAA);
347 task.add_dependency(dep);
348 assert_eq!(task.dependencies(), &[dep]);
349 }
350}