Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::todo_item_validate_draft::{
draft_errors, is_iso_date, recurrence_from_value, recurrence_to_value, texts_from_value, todo_validation_to_value,
trim_space,
};
const FIELD_ORDER: [&str; 10] = [
"id", "title", "notes", "priority", "due", "tags", "recurrence", "createdAt", "completedAt", "order",
];
/// YYYY-MM-DDTHH:MM:SS, an optional 1-9 digit fraction, then Z.
pub fn is_utc_timestamp(text: &str) -> bool {
let b = text.as_bytes();
if b.len() < 20 || !text.is_char_boundary(10) || !is_iso_date(&text[0..10]) {
return false;
}
if b[10] != b'T' || b[13] != b':' || b[16] != b':' {
return false;
}
let two = |at: usize, most: u32| {
b[at].is_ascii_digit() && b[at + 1].is_ascii_digit() && ((b[at] - b'0') as u32 * 10 + (b[at + 1] - b'0') as u32) <= most
};
if !two(11, 23) || !two(14, 59) || !two(17, 59) {
return false;
}
let mut rest = &b[19..];
if rest.first() == Some(&b'.') {
let mut n = 1;
while n < rest.len() && rest[n].is_ascii_digit() {
n += 1;
}
if n == 1 || n > 10 {
return false;
}
rest = &rest[n..];
}
rest == b"Z"
}
/// Check a stored todo: the draft rules plus id, trimmed title, timestamps,
/// completion and order.
pub fn validate_todo(todo: &Todo) -> TodoValidation {
let mut found = draft_errors(
&todo.title,
todo.notes.as_deref(),
&todo.priority,
todo.due.as_deref(),
&todo.tags,
todo.recurrence.as_ref(),
);
let has_title_error = found.iter().any(|(f, _)| f == "title");
let mut set = |field: &str, message: &str| {
found.retain(|(f, _)| f != field);
found.push((field.to_string(), message.to_string()));
};
if trim_space(&todo.id).is_empty() {
set("id", "Every todo needs an id.");
}
if !has_title_error && trim_space(&todo.title) != todo.title {
set("title", "Remove the spaces around the title.");
}
if !is_utc_timestamp(&todo.created_at) {
set("createdAt", "Record when it was created as a UTC timestamp, e.g. 2026-09-28T09:30:00Z.");
}
match (todo.done, todo.completed_at.as_deref()) {
(true, None) => set("completedAt", "A done todo needs the time it was completed."),
(false, Some(_)) => set("completedAt", "An open todo cannot have a completion time."),
(_, Some(t)) if !is_utc_timestamp(t) => {
set("completedAt", "Record when it was completed as a UTC timestamp, e.g. 2026-09-28T09:30:00Z.")
}
_ => {}
}
if todo.order < 0 {
set("order", "Order must be a whole number, 0 or more.");
}
let mut errors: Vec<(String, String)> = Vec::new();
for field in FIELD_ORDER {
if let Some((f, m)) = found.iter().find(|(f, _)| f == field) {
errors.push((f.clone(), m.clone()));
}
}
TodoValidation { valid: errors.is_empty(), errors }
}
fn opt_text(v: &Value) -> Option<String> {
if v.is_null() { None } else { Some(v.as_str().to_string()) }
}
fn opt_value(v: &Option<String>) -> Value {
v.as_deref().map(Value::str).unwrap_or(Value::Null)
}
pub fn todo_from_value(v: &Value) -> Todo {
Todo {
id: v.get("id").as_str().to_string(),
title: v.get("title").as_str().to_string(),
notes: opt_text(v.get("notes")),
done: v.get("done").as_bool(),
priority: v.get("priority").as_str().to_string(),
due: opt_text(v.get("due")),
tags: texts_from_value(v.get("tags")),
recurrence: recurrence_from_value(v.get("recurrence")),
created_at: v.get("createdAt").as_str().to_string(),
completed_at: opt_text(v.get("completedAt")),
order: v.get("order").as_i64(),
}
}
pub fn todos_from_value(v: &Value) -> Vec<Todo> {
v.as_arr().iter().map(todo_from_value).collect()
}
pub fn todo_to_value(todo: &Todo) -> Value {
Value::obj(vec![
("id", Value::str(&todo.id)),
("title", Value::str(&todo.title)),
("notes", opt_value(&todo.notes)),
("done", Value::Bool(todo.done)),
("priority", Value::str(&todo.priority)),
("due", opt_value(&todo.due)),
("tags", Value::Arr(todo.tags.iter().map(|t| Value::str(t)).collect())),
("recurrence", recurrence_to_value(&todo.recurrence)),
("createdAt", Value::str(&todo.created_at)),
("completedAt", opt_value(&todo.completed_at)),
("order", Value::Int(todo.order)),
])
}
pub fn todos_to_value(todos: &[Todo]) -> Value {
Value::Arr(todos.iter().map(todo_to_value).collect())
}
pub fn fune_vector(args: &[Value]) -> Value {
todo_validation_to_value(&validate_todo(&todo_from_value(&args[0])))
}