use super::funejson::Value; use super::todo_item::{validate_draft, Todo, TodoDraft}; use super::todo_item_validate_draft::{todo_draft_from_value, trim_space}; use super::todo_item_validate_todo::{is_utc_timestamp, todos_from_value, todos_to_value}; use super::todo_normalise_tags::normalise_tags; /// Refuse a clock reading that would not compare as a string with the stored ones. pub fn require_utc_now(now: &str) { if !is_utc_timestamp(now) { panic!("now must be a UTC timestamp like 2026-09-28T09:30:00Z"); } } /// The draft as it will be stored: title and notes trimmed (ASCII only), /// blank notes as None, tags in normal form. Then checked with validate_draft; /// the first field in field order that fails is the error. pub fn checked_draft(draft: &TodoDraft) -> TodoDraft { let notes = draft.notes.as_deref().map(trim_space).unwrap_or(""); let clean = TodoDraft { title: trim_space(&draft.title).to_string(), notes: if notes.is_empty() { None } else { Some(notes.to_string()) }, priority: draft.priority.clone(), due: draft.due.clone(), tags: normalise_tags(&draft.tags), recurrence: draft.recurrence.clone(), }; let verdict = validate_draft(&clean); if let Some((field, message)) = verdict.errors.first() { panic!("invalid todo: {}: {}", field, message); } clean } /// Add a todo from the add form: open, created now, last in manual order, /// appended. Caller mistakes (id, clock) are reported before form mistakes. /// /// # Panics /// On a blank or duplicate id, a `now` that is not a UTC timestamp, or an /// invalid draft. pub fn add_todo(todos: &[Todo], draft: &TodoDraft, id: &str, now: &str) -> Vec { if trim_space(id).is_empty() { panic!("a todo needs an id"); } if todos.iter().any(|t| t.id == id) { panic!("a todo with id \"{}\" already exists", id); } require_utc_now(now); let clean = checked_draft(draft); let order = todos.iter().map(|t| t.order + 1).fold(0, i64::max); let mut out = todos.to_vec(); out.push(Todo { id: id.to_string(), title: clean.title, notes: clean.notes, done: false, priority: clean.priority, due: clean.due, tags: clean.tags, recurrence: clean.recurrence, created_at: now.to_string(), completed_at: None, order, }); out } pub fn fune_vector(args: &[Value]) -> Value { let todos = todos_from_value(&args[0]); todos_to_value(&add_todo(&todos, &todo_draft_from_value(&args[1]), args[2].as_str(), args[3].as_str())) }