use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::core::reader::queries::{get_tags_for_task, get_todo};
use crate::core::types::MaybeTodo;
use crate::state::AppState;
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct GetTodoArgs {
pub id: String,
}
pub async fn things_get_todo(
state: AppState,
args: GetTodoArgs,
) -> anyhow::Result<MaybeTodo> {
let todo = get_todo(&state.pool, args.id).await?;
Ok(MaybeTodo { todo })
}
use std::time::{SystemTime, UNIX_EPOCH};
use crate::core::writer::operation::{AddTodoSpec, Operation};
use crate::core::writer::outcome::WriteOutcome;
use crate::core::writer::verify::VerifyPredicate;
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
pub struct AddTodoArgs {
pub title: String,
#[serde(default)]
pub notes: Option<String>,
#[serde(default)]
pub when: Option<String>,
#[serde(default)]
pub deadline: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub checklist_items: Vec<String>,
#[serde(default)]
pub list_id: Option<String>,
#[serde(default)]
pub heading_id: Option<String>,
}
pub async fn things_add_todo(
state: AppState,
args: AddTodoArgs,
) -> anyhow::Result<WriteOutcome> {
if args.title.trim().is_empty() {
return Err(crate::core::error::ThingsError::InvalidInput {
field: "title".into(),
reason: "title must be non-empty".into(),
}
.into());
}
let since_unix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs_f64())
.unwrap_or(0.0);
let op = Operation::AddTodo(AddTodoSpec {
title: args.title.clone(),
notes: args.notes,
when: args.when,
deadline: args.deadline,
tags: args.tags,
checklist_items: args.checklist_items,
list_id: args.list_id,
heading_id: args.heading_id,
});
let predicate = VerifyPredicate::CreateByTitle {
title: args.title,
since_unix,
kind: crate::core::types::TaskKind::Todo,
};
let outcome = state.writer.fire(op, Some(predicate)).await?;
Ok(outcome)
}
use crate::core::writer::operation::{MoveTodoSpec, UpdateTodoSpec};
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
pub struct UpdateTodoArgs {
pub id: String,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub notes: Option<String>,
#[serde(default)]
pub when: Option<String>,
#[serde(default)]
pub deadline: Option<String>,
#[serde(default)]
pub tags: Option<Vec<String>>,
#[serde(default)]
pub list_id: Option<String>,
#[serde(default)]
pub completed: Option<bool>,
#[serde(default)]
pub canceled: Option<bool>,
}
pub async fn things_update_todo(
state: AppState,
args: UpdateTodoArgs,
) -> anyhow::Result<WriteOutcome> {
if args.id.trim().is_empty() {
return Err(crate::core::error::ThingsError::InvalidInput {
field: "id".into(),
reason: "id must be non-empty".into(),
}
.into());
}
let op = Operation::UpdateTodo(UpdateTodoSpec {
id: args.id.clone(),
title: args.title.clone(),
notes: args.notes.clone(),
when: args.when,
deadline: args.deadline,
tags: args.tags,
list_id: args.list_id,
completed: args.completed,
canceled: args.canceled,
});
let predicate = VerifyPredicate::UpdateById {
id: args.id,
expected_title: args.title,
expected_notes: args.notes,
};
let outcome = state.writer.fire(op, Some(predicate)).await?;
Ok(outcome)
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
pub struct StatusChangeArgs {
pub id: String,
}
pub async fn things_complete_todo(
state: AppState,
args: StatusChangeArgs,
) -> anyhow::Result<WriteOutcome> {
if args.id.trim().is_empty() {
return Err(crate::core::error::ThingsError::InvalidInput {
field: "id".into(),
reason: "id must be non-empty".into(),
}
.into());
}
let op = Operation::CompleteTodo { id: args.id.clone() };
let predicate = VerifyPredicate::StatusChange {
id: args.id,
want: crate::core::types::TaskStatus::Completed,
};
let outcome = state.writer.fire(op, Some(predicate)).await?;
Ok(outcome)
}
pub async fn things_cancel_todo(
state: AppState,
args: StatusChangeArgs,
) -> anyhow::Result<WriteOutcome> {
if args.id.trim().is_empty() {
return Err(crate::core::error::ThingsError::InvalidInput {
field: "id".into(),
reason: "id must be non-empty".into(),
}
.into());
}
let op = Operation::CancelTodo { id: args.id.clone() };
let predicate = VerifyPredicate::StatusChange {
id: args.id,
want: crate::core::types::TaskStatus::Canceled,
};
let outcome = state.writer.fire(op, Some(predicate)).await?;
Ok(outcome)
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
pub struct MoveTodoArgs {
pub id: String,
#[serde(default)]
pub list_id: Option<String>,
}
pub async fn things_move_todo(
state: AppState,
args: MoveTodoArgs,
) -> anyhow::Result<WriteOutcome> {
if args.id.trim().is_empty() {
return Err(crate::core::error::ThingsError::InvalidInput {
field: "id".into(),
reason: "id must be non-empty".into(),
}
.into());
}
let op = Operation::MoveTodo(MoveTodoSpec {
id: args.id.clone(),
list_id: args.list_id.clone(),
});
let predicate = VerifyPredicate::MoveById {
id: args.id,
expected_list_id: args.list_id,
};
let outcome = state.writer.fire(op, Some(predicate)).await?;
Ok(outcome)
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
pub struct TagAssignmentArgs {
pub id: String,
pub tags: Vec<String>,
}
pub async fn things_assign_tag(
state: AppState,
args: TagAssignmentArgs,
) -> anyhow::Result<WriteOutcome> {
if args.id.trim().is_empty() {
return Err(crate::core::error::ThingsError::InvalidInput {
field: "id".into(),
reason: "id must be non-empty".into(),
}
.into());
}
if args.tags.is_empty() {
return Err(crate::core::error::ThingsError::InvalidInput {
field: "tags".into(),
reason: "tags must be non-empty".into(),
}
.into());
}
if args.tags.iter().any(|t| t.trim().is_empty()) {
return Err(crate::core::error::ThingsError::InvalidInput {
field: "tags".into(),
reason: "tags must not contain empty or whitespace-only entries".into(),
}
.into());
}
let current = get_tags_for_task(&state.pool, args.id.clone()).await?;
let mut merged: Vec<String> = current.clone();
for t in &args.tags {
if !merged.iter().any(|x| x == t) {
merged.push(t.clone());
}
}
let op = Operation::UpdateTodo(UpdateTodoSpec {
id: args.id.clone(),
tags: Some(merged),
..Default::default()
});
let predicate = VerifyPredicate::TagOnTodoById {
id: args.id,
tag: args.tags[0].clone(),
present: true,
};
let outcome = state.writer.fire(op, Some(predicate)).await?;
Ok(outcome)
}
pub async fn things_unassign_tag(
state: AppState,
args: TagAssignmentArgs,
) -> anyhow::Result<WriteOutcome> {
if args.id.trim().is_empty() {
return Err(crate::core::error::ThingsError::InvalidInput {
field: "id".into(),
reason: "id must be non-empty".into(),
}
.into());
}
if args.tags.is_empty() {
return Err(crate::core::error::ThingsError::InvalidInput {
field: "tags".into(),
reason: "tags must be non-empty".into(),
}
.into());
}
if args.tags.iter().any(|t| t.trim().is_empty()) {
return Err(crate::core::error::ThingsError::InvalidInput {
field: "tags".into(),
reason: "tags must not contain empty or whitespace-only entries".into(),
}
.into());
}
let current = get_tags_for_task(&state.pool, args.id.clone()).await?;
let to_remove: std::collections::HashSet<&str> =
args.tags.iter().map(|s| s.as_str()).collect();
let new_set: Vec<String> = current
.into_iter()
.filter(|t| !to_remove.contains(t.as_str()))
.collect();
let op = Operation::UpdateTodo(UpdateTodoSpec {
id: args.id.clone(),
tags: Some(new_set),
..Default::default()
});
let predicate = VerifyPredicate::TagOnTodoById {
id: args.id,
tag: args.tags[0].clone(),
present: false,
};
let outcome = state.writer.fire(op, Some(predicate)).await?;
Ok(outcome)
}