use std::collections::BTreeMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use starweaver_core::Metadata;
pub const TASK_SNAPSHOT_EVENT_KIND: &str = "task_snapshot";
#[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TaskStatus {
#[default]
Pending,
InProgress,
Completed,
}
impl TaskStatus {
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
match value.trim() {
"pending" => Some(Self::Pending),
"in_progress" => Some(Self::InProgress),
"completed" => Some(Self::Completed),
_ => None,
}
}
#[must_use]
pub const fn as_str(&self) -> &str {
match self {
Self::Pending => "pending",
Self::InProgress => "in_progress",
Self::Completed => "completed",
}
}
#[must_use]
pub const fn is_completed(&self) -> bool {
matches!(self, Self::Completed)
}
}
impl std::fmt::Display for TaskStatus {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Task {
pub id: String,
pub subject: String,
pub description: String,
#[serde(default)]
pub status: TaskStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_form: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub blocked_by: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub blocks: Vec<String>,
#[serde(default, skip_serializing_if = "Metadata::is_empty")]
pub metadata: Metadata,
#[serde(default = "Utc::now")]
pub created_at: DateTime<Utc>,
#[serde(default = "Utc::now")]
pub updated_at: DateTime<Utc>,
}
impl Task {
#[must_use]
pub fn new(
id: impl Into<String>,
subject: impl Into<String>,
description: impl Into<String>,
) -> Self {
let now = Utc::now();
Self {
id: id.into(),
subject: subject.into(),
description: description.into(),
status: TaskStatus::Pending,
active_form: None,
owner: None,
blocked_by: Vec::new(),
blocks: Vec::new(),
metadata: Metadata::default(),
created_at: now,
updated_at: now,
}
}
#[must_use]
pub const fn is_blocked(&self) -> bool {
!self.blocked_by.is_empty()
}
#[must_use]
pub const fn status_str(&self) -> &str {
self.status.as_str()
}
pub fn set_status(&mut self, status: TaskStatus) {
self.status = status;
self.updated_at = Utc::now();
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct TaskSnapshot {
pub tasks: Vec<Task>,
}
impl TaskSnapshot {
#[must_use]
pub fn into_payload(self) -> Value {
serde_json::to_value(self).unwrap_or_else(|_| serde_json::json!({"tasks": []}))
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct TaskManager {
#[serde(default)]
pub tasks: BTreeMap<String, Task>,
}
impl TaskManager {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn create(
&mut self,
subject: impl Into<String>,
description: impl Into<String>,
active_form: Option<String>,
metadata: Metadata,
) -> Task {
let id = self.next_id();
let mut task = Task::new(id.clone(), subject, description);
task.active_form = active_form;
task.metadata = metadata;
self.tasks.insert(id, task.clone());
task
}
#[must_use]
pub fn get(&self, task_id: &str) -> Option<&Task> {
self.tasks.get(task_id)
}
#[must_use]
pub fn list_all(&self) -> Vec<Task> {
let mut tasks = self.tasks.values().cloned().collect::<Vec<_>>();
tasks.sort_by_key(|task| task_sort_key(&task.id));
tasks
}
pub fn replace_all(&mut self, tasks: Vec<Task>) {
self.tasks = tasks
.into_iter()
.map(|task| (normalize_task_id(&task.id), task))
.collect();
}
#[allow(clippy::too_many_arguments)]
pub fn update(
&mut self,
task_id: &str,
status: Option<TaskStatus>,
subject: Option<String>,
description: Option<String>,
active_form: Option<Option<String>>,
owner: Option<Option<String>>,
add_blocks: Option<&[String]>,
add_blocked_by: Option<&[String]>,
metadata: Option<&Metadata>,
) -> Option<Task> {
let task_id = normalize_task_id(task_id);
let was_completed = status.as_ref().is_some_and(TaskStatus::is_completed)
&& self
.tasks
.get(&task_id)
.is_some_and(|task| !task.status.is_completed());
{
let task = self.tasks.get_mut(&task_id)?;
if let Some(status) = status {
task.status = status;
}
if let Some(subject) = subject.filter(|value| !value.trim().is_empty()) {
task.subject = subject;
}
if let Some(description) = description.filter(|value| !value.trim().is_empty()) {
task.description = description;
}
if let Some(active_form) = active_form {
task.active_form = active_form;
}
if let Some(owner) = owner {
task.owner = owner;
}
extend_unique(&mut task.blocks, add_blocks);
extend_unique(&mut task.blocked_by, add_blocked_by);
if let Some(metadata) = metadata {
for (key, value) in metadata {
task.metadata.insert(key.clone(), value.clone());
}
}
task.updated_at = Utc::now();
}
for blocked_id in add_blocks.unwrap_or_default() {
self.add_blocking_relationship(&task_id, blocked_id);
}
for blocker_id in add_blocked_by.unwrap_or_default() {
self.add_blocked_by_relationship(&task_id, blocker_id);
}
if was_completed {
self.resolve_completion(&task_id);
}
self.tasks.get(&task_id).cloned()
}
#[must_use]
pub fn export_tasks(&self) -> BTreeMap<String, Value> {
self.tasks
.iter()
.map(|(id, task)| {
(
id.clone(),
serde_json::to_value(task).unwrap_or_else(|_| serde_json::json!({})),
)
})
.collect()
}
#[must_use]
pub fn from_exported(data: BTreeMap<String, Value>) -> Self {
let tasks = data
.into_iter()
.filter_map(|(id, value)| {
serde_json::from_value::<Task>(value)
.ok()
.map(|task| (id, task))
})
.collect();
Self { tasks }
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.tasks.is_empty()
}
fn next_id(&self) -> String {
self.tasks
.keys()
.filter_map(|id| normalize_task_id(id).parse::<u64>().ok())
.max()
.unwrap_or(0)
.saturating_add(1)
.to_string()
}
fn add_blocking_relationship(&mut self, task_id: &str, blocked_id: &str) {
let blocked_id = normalize_task_id(blocked_id);
if let Some(task) = self.tasks.get_mut(task_id) {
extend_unique(&mut task.blocks, Some(std::slice::from_ref(&blocked_id)));
}
if let Some(blocked_task) = self.tasks.get_mut(&blocked_id) {
extend_unique(
&mut blocked_task.blocked_by,
Some(std::slice::from_ref(&task_id.to_string())),
);
blocked_task.updated_at = Utc::now();
}
}
fn add_blocked_by_relationship(&mut self, task_id: &str, blocker_id: &str) {
let blocker_id = normalize_task_id(blocker_id);
if let Some(task) = self.tasks.get_mut(task_id) {
extend_unique(
&mut task.blocked_by,
Some(std::slice::from_ref(&blocker_id)),
);
}
if let Some(blocker) = self.tasks.get_mut(&blocker_id) {
extend_unique(
&mut blocker.blocks,
Some(std::slice::from_ref(&task_id.to_string())),
);
blocker.updated_at = Utc::now();
}
}
fn resolve_completion(&mut self, task_id: &str) {
let Some(blocked_ids) = self.tasks.get(task_id).map(|task| task.blocks.clone()) else {
return;
};
for blocked_id in blocked_ids {
if let Some(blocked_task) = self.tasks.get_mut(&blocked_id) {
blocked_task.blocked_by.retain(|blocker| blocker != task_id);
blocked_task.updated_at = Utc::now();
}
}
}
}
fn normalize_task_id(id: &str) -> String {
id.trim().trim_start_matches('#').to_string()
}
fn task_sort_key(id: &str) -> (u8, u64, String) {
normalize_task_id(id).parse::<u64>().map_or_else(
|_| (1, 0, id.to_string()),
|value| (0, value, String::new()),
)
}
fn extend_unique(target: &mut Vec<String>, values: Option<&[String]>) {
let Some(values) = values else {
return;
};
let mut seen = target
.iter()
.cloned()
.collect::<std::collections::BTreeSet<_>>();
for value in values {
let normalized = normalize_task_id(value);
if !normalized.is_empty() && seen.insert(normalized.clone()) {
target.push(normalized);
}
}
}