Functional Weave
Code in Python

todo.item@1.0.0

impl/rust/validate_todo.rs

4,799 bytes · the Rust implementation · view raw

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])))
}