use std::{fmt, str::FromStr};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
errors::GitError,
hash::ObjectHash,
internal::object::{
ObjectTrait,
types::{ActorRef, Header, ObjectType},
},
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum GoalType {
Feature,
Bugfix,
Refactor,
Docs,
Perf,
Test,
Chore,
Build,
Ci,
Style,
Other(String),
}
impl GoalType {
pub fn as_str(&self) -> &str {
match self {
GoalType::Feature => "feature",
GoalType::Bugfix => "bugfix",
GoalType::Refactor => "refactor",
GoalType::Docs => "docs",
GoalType::Perf => "perf",
GoalType::Test => "test",
GoalType::Chore => "chore",
GoalType::Build => "build",
GoalType::Ci => "ci",
GoalType::Style => "style",
GoalType::Other(value) => value.as_str(),
}
}
}
impl fmt::Display for GoalType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl FromStr for GoalType {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"feature" => Ok(GoalType::Feature),
"bugfix" => Ok(GoalType::Bugfix),
"refactor" => Ok(GoalType::Refactor),
"docs" => Ok(GoalType::Docs),
"perf" => Ok(GoalType::Perf),
"test" => Ok(GoalType::Test),
"chore" => Ok(GoalType::Chore),
"build" => Ok(GoalType::Build),
"ci" => Ok(GoalType::Ci),
"style" => Ok(GoalType::Style),
_ => Ok(GoalType::Other(value.to_string())),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Task {
#[serde(flatten)]
header: Header,
title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
goal: Option<GoalType>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
constraints: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
acceptance_criteria: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
requester: Option<ActorRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
parent: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
intent: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
origin_step_id: Option<Uuid>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
dependencies: Vec<Uuid>,
}
impl Task {
pub fn new(
created_by: ActorRef,
title: impl Into<String>,
goal: Option<GoalType>,
) -> Result<Self, String> {
Ok(Self {
header: Header::new(ObjectType::Task, created_by)?,
title: title.into(),
description: None,
goal,
constraints: Vec::new(),
acceptance_criteria: Vec::new(),
requester: None,
parent: None,
intent: None,
origin_step_id: None,
dependencies: Vec::new(),
})
}
pub fn header(&self) -> &Header {
&self.header
}
pub fn title(&self) -> &str {
&self.title
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn goal(&self) -> Option<&GoalType> {
self.goal.as_ref()
}
pub fn constraints(&self) -> &[String] {
&self.constraints
}
pub fn acceptance_criteria(&self) -> &[String] {
&self.acceptance_criteria
}
pub fn requester(&self) -> Option<&ActorRef> {
self.requester.as_ref()
}
pub fn parent(&self) -> Option<Uuid> {
self.parent
}
pub fn intent(&self) -> Option<Uuid> {
self.intent
}
pub fn origin_step_id(&self) -> Option<Uuid> {
self.origin_step_id
}
pub fn dependencies(&self) -> &[Uuid] {
&self.dependencies
}
pub fn set_description(&mut self, description: Option<String>) {
self.description = description;
}
pub fn add_constraint(&mut self, constraint: impl Into<String>) {
self.constraints.push(constraint.into());
}
pub fn add_acceptance_criterion(&mut self, criterion: impl Into<String>) {
self.acceptance_criteria.push(criterion.into());
}
pub fn set_requester(&mut self, requester: Option<ActorRef>) {
self.requester = requester;
}
pub fn set_parent(&mut self, parent: Option<Uuid>) {
self.parent = parent;
}
pub fn set_intent(&mut self, intent: Option<Uuid>) {
self.intent = intent;
}
pub fn set_origin_step_id(&mut self, origin_step_id: Option<Uuid>) {
self.origin_step_id = origin_step_id;
}
pub fn add_dependency(&mut self, task_id: Uuid) {
self.dependencies.push(task_id);
}
}
impl fmt::Display for Task {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Task: {}", self.header.object_id())
}
}
impl ObjectTrait for Task {
fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
where
Self: Sized,
{
serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
}
fn get_type(&self) -> ObjectType {
ObjectType::Task
}
fn get_size(&self) -> usize {
match serde_json::to_vec(self) {
Ok(v) => v.len(),
Err(e) => {
tracing::warn!("failed to compute Task size: {}", e);
0
}
}
}
fn to_data(&self) -> Result<Vec<u8>, GitError> {
serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_task_creation() {
let actor = ActorRef::agent("worker").expect("actor");
let task = Task::new(actor, "Implement pagination", Some(GoalType::Feature)).expect("task");
assert_eq!(task.title(), "Implement pagination");
assert_eq!(task.goal(), Some(&GoalType::Feature));
assert!(task.origin_step_id().is_none());
}
#[test]
fn test_task_requester() {
let actor = ActorRef::agent("worker").expect("actor");
let requester = ActorRef::human("alice").expect("requester");
let mut task = Task::new(actor, "Implement pagination", None).expect("task");
task.set_requester(Some(requester.clone()));
assert_eq!(task.requester(), Some(&requester));
}
#[test]
fn test_task_goal_optional() {
let actor = ActorRef::agent("worker").expect("actor");
let task = Task::new(actor, "Investigate", None).expect("task");
assert!(task.goal().is_none());
}
#[test]
fn test_task_origin_step_id() {
let actor = ActorRef::agent("worker").expect("actor");
let mut task = Task::new(actor, "Implement pagination", None).expect("task");
let step_id = Uuid::from_u128(0x1234);
task.set_origin_step_id(Some(step_id));
assert_eq!(task.origin_step_id(), Some(step_id));
}
#[test]
fn test_task_dependencies() {
let actor = ActorRef::agent("worker").expect("actor");
let mut task = Task::new(actor, "Implement pagination", None).expect("task");
let dep = Uuid::from_u128(0xAA);
task.add_dependency(dep);
assert_eq!(task.dependencies(), &[dep]);
}
}