use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TaskStatus {
Pending,
InProgress,
Completed,
Deleted,
}
impl TaskStatus {
pub fn can_transition_to(self, target: TaskStatus) -> bool {
if target == TaskStatus::Deleted {
return true;
}
matches!(
(self, target),
(TaskStatus::Pending, TaskStatus::InProgress)
| (TaskStatus::InProgress, TaskStatus::Completed)
)
}
}
impl std::fmt::Display for TaskStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TaskStatus::Pending => write!(f, "pending"),
TaskStatus::InProgress => write!(f, "in_progress"),
TaskStatus::Completed => write!(f, "completed"),
TaskStatus::Deleted => write!(f, "deleted"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskFile {
pub id: String,
pub subject: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_form: Option<String>,
pub status: TaskStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub blocks: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub blocked_by: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateTaskRequest {
pub subject: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_form: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskUpdate {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_form: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<TaskStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub add_blocks: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub add_blocked_by: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Default)]
pub struct TaskFilter {
pub status: Option<TaskStatus>,
pub owner: Option<String>,
pub unblocked_only: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_transitions() {
assert!(TaskStatus::Pending.can_transition_to(TaskStatus::InProgress));
assert!(TaskStatus::InProgress.can_transition_to(TaskStatus::Completed));
assert!(!TaskStatus::Pending.can_transition_to(TaskStatus::Completed));
assert!(!TaskStatus::Completed.can_transition_to(TaskStatus::InProgress));
assert!(TaskStatus::Pending.can_transition_to(TaskStatus::Deleted));
assert!(TaskStatus::InProgress.can_transition_to(TaskStatus::Deleted));
assert!(TaskStatus::Completed.can_transition_to(TaskStatus::Deleted));
}
#[test]
fn serde_round_trip_task() {
let task = TaskFile {
id: "1".into(),
subject: "Fix auth bug".into(),
description: Some("The login endpoint returns 500".into()),
active_form: Some("Fixing auth bug".into()),
status: TaskStatus::Pending,
owner: None,
blocks: vec!["2".into()],
blocked_by: vec![],
metadata: None,
};
let json = serde_json::to_string_pretty(&task).unwrap();
let parsed: TaskFile = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.id, "1");
assert_eq!(parsed.blocks, vec!["2"]);
}
#[test]
fn deserialize_claude_code_format() {
let json = r#"{
"id": "42",
"subject": "Implement feature",
"description": "Add caching",
"activeForm": "Implementing feature",
"status": "in_progress",
"owner": "coder",
"blocks": [],
"blockedBy": ["41"]
}"#;
let task: TaskFile = serde_json::from_str(json).unwrap();
assert_eq!(task.id, "42");
assert_eq!(task.status, TaskStatus::InProgress);
assert_eq!(task.owner.as_deref(), Some("coder"));
assert_eq!(task.blocked_by, vec!["41"]);
}
}