use chrono::{DateTime, Utc};
use serde_json::{Value, json};
use uuid::Uuid;
pub fn create_minimal_task(
actions: Option<Vec<Value>>,
messages: Option<Vec<Value>>,
start_date: Option<DateTime<Utc>>,
complete_date: Option<DateTime<Utc>>,
) -> Result<Value, String> {
let mut task = json!({
"$schema": "https://hai.ai/schemas/task/v1/task.schema.json",
"jacsTaskState": "creating",
});
if let Some(actions) = actions {
task["jacsTaskActionsDesired"] = json!(actions);
}
if let Some(messages) = messages {
task["jacsTaskMessages"] = json!(messages);
}
if let Some(start_date) = start_date {
task["jacsTaskStartDate"] = json!(start_date.to_rfc3339());
}
if let Some(complete_date) = complete_date {
task["jacsTaskCompleteDate"] = json!(complete_date.to_rfc3339());
}
task["id"] = json!(Uuid::new_v4().to_string());
task["jacsType"] = json!("task");
task["jacsLevel"] = json!("config");
Ok(task)
}
fn get_array_mut<'a>(task: &'a mut Value, field: &str) -> Result<&'a mut Vec<Value>, String> {
task[field]
.as_array_mut()
.ok_or_else(|| "Invalid task format".to_string())
}
fn get_or_init_array_mut<'a>(
task: &'a mut Value,
field: &str,
) -> Result<&'a mut Vec<Value>, String> {
if task.get(field).is_none() {
task[field] = json!([]);
}
get_array_mut(task, field)
}
fn update_value_in_array(
values: &mut [Value],
old_value: &Value,
new_value: Value,
not_found_message: &str,
) -> Result<(), String> {
let index = values
.iter()
.position(|v| v == old_value)
.ok_or_else(|| not_found_message.to_string())?;
values[index] = new_value;
Ok(())
}
fn remove_value_from_array(
values: &mut Vec<Value>,
target: &Value,
not_found_message: &str,
) -> Result<(), String> {
let index = values
.iter()
.position(|v| v == target)
.ok_or_else(|| not_found_message.to_string())?;
values.remove(index);
Ok(())
}
pub fn add_action_to_task(task: &mut Value, action: Value) -> Result<(), String> {
get_or_init_array_mut(task, "jacsTaskActionsDesired")?.push(action);
Ok(())
}
pub fn update_action_in_task(
task: &mut Value,
old_action: Value,
new_action: Value,
) -> Result<(), String> {
update_value_in_array(
get_array_mut(task, "jacsTaskActionsDesired")?,
&old_action,
new_action,
"Action not found",
)
}
pub fn remove_action_from_task(task: &mut Value, action: Value) -> Result<(), String> {
remove_value_from_array(
get_array_mut(task, "jacsTaskActionsDesired")?,
&action,
"Action not found",
)
}
pub fn update_task_state(task: &mut Value, new_state: &str) -> Result<(), String> {
let allowed_states = ["open", "editlock", "closed"];
if !allowed_states.contains(&new_state) {
return Err(format!("Invalid task state: {}", new_state));
}
task["jacsTaskState"] = json!(new_state);
Ok(())
}
pub fn update_task_start_date(
task: &mut Value,
new_start_date: DateTime<Utc>,
) -> Result<(), String> {
task["jacsTaskStartDate"] = json!(new_start_date.to_rfc3339());
Ok(())
}
pub fn remove_task_start_date(task: &mut Value) -> Result<(), String> {
task.as_object_mut()
.ok_or_else(|| "Invalid task format".to_string())?
.remove("jacsTaskStartDate");
Ok(())
}
pub fn update_task_complete_date(
task: &mut Value,
new_complete_date: DateTime<Utc>,
) -> Result<(), String> {
task["jacsTaskCompleteDate"] = json!(new_complete_date.to_rfc3339());
Ok(())
}
pub fn remove_task_complete_date(task: &mut Value) -> Result<(), String> {
task.as_object_mut()
.ok_or_else(|| "Invalid task format".to_string())?
.remove("jacsTaskCompleteDate");
Ok(())
}
pub fn add_subtask_to_task(task: &mut Value, subtask_id: &str) -> Result<(), String> {
get_or_init_array_mut(task, "jacsTaskSubTaskOf")?.push(json!(subtask_id));
Ok(())
}
pub fn remove_subtask_from_task(task: &mut Value, subtask_id: &str) -> Result<(), String> {
remove_value_from_array(
get_array_mut(task, "jacsTaskSubTaskOf")?,
&json!(subtask_id),
"Subtask not found",
)
}
pub fn add_copy_task_to_task(task: &mut Value, copy_task_id: &str) -> Result<(), String> {
get_or_init_array_mut(task, "jacsTaskCopyOf")?.push(json!(copy_task_id));
Ok(())
}
pub fn remove_copy_task_from_task(task: &mut Value, copy_task_id: &str) -> Result<(), String> {
remove_value_from_array(
get_array_mut(task, "jacsTaskCopyOf")?,
&json!(copy_task_id),
"Copy task not found",
)
}
pub fn add_merged_task_to_task(task: &mut Value, merged_task_id: &str) -> Result<(), String> {
get_or_init_array_mut(task, "jacsTaskMergedTasks")?.push(json!(merged_task_id));
Ok(())
}
pub fn remove_merged_task_from_task(task: &mut Value, merged_task_id: &str) -> Result<(), String> {
remove_value_from_array(
get_array_mut(task, "jacsTaskMergedTasks")?,
&json!(merged_task_id),
"Merged task not found",
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn list_field_crud_helpers_work_for_task_refs() {
let mut task = create_minimal_task(None, None, None, None).expect("create task");
add_subtask_to_task(&mut task, "sub-1").expect("add subtask");
add_copy_task_to_task(&mut task, "copy-1").expect("add copy");
add_merged_task_to_task(&mut task, "merge-1").expect("add merged");
assert_eq!(
task["jacsTaskSubTaskOf"].as_array().map(|a| a.len()),
Some(1)
);
assert_eq!(task["jacsTaskCopyOf"].as_array().map(|a| a.len()), Some(1));
assert_eq!(
task["jacsTaskMergedTasks"].as_array().map(|a| a.len()),
Some(1)
);
remove_subtask_from_task(&mut task, "sub-1").expect("remove subtask");
remove_copy_task_from_task(&mut task, "copy-1").expect("remove copy");
remove_merged_task_from_task(&mut task, "merge-1").expect("remove merged");
assert_eq!(
task["jacsTaskSubTaskOf"].as_array().map(|a| a.len()),
Some(0)
);
assert_eq!(task["jacsTaskCopyOf"].as_array().map(|a| a.len()), Some(0));
assert_eq!(
task["jacsTaskMergedTasks"].as_array().map(|a| a.len()),
Some(0)
);
}
#[test]
fn update_task_state_rejects_invalid_value() {
let mut task = create_minimal_task(None, None, None, None).expect("create task");
let result = update_task_state(&mut task, "invalid-state");
assert!(result.is_err());
assert!(
result
.expect_err("invalid state should fail")
.contains("Invalid task state")
);
}
}