use std::path::PathBuf;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use thiserror::Error;
use super::{RuntimeHandle, TaskItem, TaskStatus, task::TaskAccess};
use crate::runtime::task::TaskIntrinsicTool;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NewTask {
pub subject: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub owner: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub working_directory: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub blocked_by: Vec<u64>,
}
impl NewTask {
pub fn new(subject: impl Into<String>) -> Self {
Self {
subject: subject.into(),
description: String::new(),
owner: String::new(),
working_directory: None,
blocked_by: Vec::new(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskPatch {
#[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 owner: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "super::task::deserialize_present_nullable_string"
)]
pub working_directory: Option<Option<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<TaskStatus>,
}
#[derive(Debug, Error)]
pub enum TaskBoardError {
#[error("{0}")]
Operation(String),
#[error("task operation returned an incompatible result: {0}")]
InvalidResult(#[source] serde_json::Error),
}
#[derive(Debug, Clone)]
enum BoardAccess {
Lead,
Teammate(String),
}
#[derive(Clone)]
pub struct TaskBoard {
runtime: RuntimeHandle,
namespace: PathBuf,
access: BoardAccess,
}
impl TaskBoard {
pub(crate) fn lead(runtime: RuntimeHandle, namespace: PathBuf) -> Self {
Self {
runtime,
namespace,
access: BoardAccess::Lead,
}
}
pub(crate) fn agent(
runtime: RuntimeHandle,
namespace: PathBuf,
name: String,
is_teammate: bool,
) -> Self {
Self {
runtime,
namespace,
access: if is_teammate {
BoardAccess::Teammate(name)
} else {
BoardAccess::Lead
},
}
}
pub fn create(&self, spec: NewTask) -> Result<TaskItem, TaskBoardError> {
self.execute(TaskIntrinsicTool::Create, spec, self.access())
}
pub fn get(&self, id: u64) -> Result<TaskItem, TaskBoardError> {
self.execute(
TaskIntrinsicTool::Get,
serde_json::json!({ "taskId": id }),
self.access(),
)
}
pub fn list(&self) -> Result<Vec<TaskItem>, TaskBoardError> {
let result: TaskListResult = self.execute(
TaskIntrinsicTool::List,
serde_json::json!({}),
self.access(),
)?;
Ok(result.tasks)
}
pub fn update(&self, id: u64, patch: TaskPatch) -> Result<TaskItem, TaskBoardError> {
let mut input = serde_json::to_value(patch).map_err(TaskBoardError::InvalidResult)?;
input
.as_object_mut()
.expect("TaskPatch serializes as an object")
.insert("taskId".to_string(), serde_json::json!(id));
let result: TaskUpdateResult =
self.execute(TaskIntrinsicTool::Update, input, self.access())?;
Ok(result.task)
}
pub fn claim(&self, id: Option<u64>, owner: &str) -> Result<TaskItem, TaskBoardError> {
let owner = owner.trim();
if owner.is_empty() {
return Err(TaskBoardError::Operation(
"Task claimant must not be empty".to_string(),
));
}
let access = match &self.access {
BoardAccess::Lead => TaskAccess::LeadClaimant(owner),
BoardAccess::Teammate(name) if name == owner => TaskAccess::Teammate(name),
BoardAccess::Teammate(name) => {
return Err(TaskBoardError::Operation(format!(
"Teammate '{name}' cannot claim a task for '{owner}'"
)));
}
};
self.execute(
TaskIntrinsicTool::Claim,
serde_json::json!({ "taskId": id }),
access,
)
}
pub fn add_dependency(&self, blocker: u64, dependent: u64) -> Result<TaskItem, TaskBoardError> {
self.update_dependency(dependent, "addBlockedBy", blocker)
}
pub fn remove_dependency(
&self,
blocker: u64,
dependent: u64,
) -> Result<TaskItem, TaskBoardError> {
self.update_dependency(dependent, "removeBlockedBy", blocker)
}
fn update_dependency(
&self,
task_id: u64,
field: &str,
related_id: u64,
) -> Result<TaskItem, TaskBoardError> {
let result: TaskUpdateResult = self.execute(
TaskIntrinsicTool::Update,
serde_json::json!({ "taskId": task_id, (field): [related_id] }),
self.access(),
)?;
Ok(result.task)
}
fn access(&self) -> TaskAccess<'_> {
match &self.access {
BoardAccess::Lead => TaskAccess::Lead,
BoardAccess::Teammate(name) => TaskAccess::Teammate(name),
}
}
fn execute<T: DeserializeOwned>(
&self,
tool: TaskIntrinsicTool,
input: impl Serialize,
access: TaskAccess<'_>,
) -> Result<T, TaskBoardError> {
let input = serde_json::to_value(input).map_err(TaskBoardError::InvalidResult)?;
let output = self
.runtime
.execute_task_mutation(&tool, input, &self.namespace, access)
.map_err(TaskBoardError::Operation)?;
serde_json::from_str(&output).map_err(TaskBoardError::InvalidResult)
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct TaskListResult {
tasks: Vec<TaskItem>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct TaskUpdateResult {
task: TaskItem,
}