use chrono::{DateTime, Utc}; use serde::{Serialize, Serializer}; use crate::{ ids::ThingsId, store::{Area, Tag, Task, ThingsStore}, wire::task::{TaskStart, TaskStatus, TaskType}, }; #[derive(Serialize)] pub struct ResolvedTaskJson { #[serde(flatten)] core: TaskCoreJson, #[serde(flatten)] links: TaskLinksJson, dates: TaskDatesJson, notes: Option, checklist: Vec, recurrence: TaskRecurrenceJson, flags: TaskFlagsJson, indexes: TaskIndexesJson, #[serde(skip_serializing_if = "Option::is_none")] progress: Option, } #[derive(Serialize)] pub struct ResolvedAreaJson { pub id: String, pub short_id: String, pub title: String, pub index: i32, pub tags: Vec, /// the area's history did not replay completely /// /// the CLI shows what it has and puts nothing into it pub degraded: bool, } #[derive(Serialize)] pub struct ResolvedTagJson { pub id: String, pub short_id: String, pub title: String, pub shortcut: Option, pub parent: Option, pub index: i32, /// the tag's history did not replay completely /// /// the CLI shows what it has and puts nothing under it pub degraded: bool, } #[derive(Serialize)] struct TaskProgressJson { done: i32, total: i32, } #[derive(Serialize)] pub struct TaskCoreJson { pub id: String, pub short_id: String, pub title: String, pub status: JsonTaskStatus, pub r#type: JsonItemType, pub start: TaskStartJson, } #[derive(Serialize)] pub struct TaskLinksJson { pub project: Option, pub area: Option, pub heading: Option, pub tags: Vec, } #[derive(Serialize)] pub struct TaskStartJson { pub bucket: JsonStartBucket, pub scheduled_at: Option, pub reminder: Option, pub today_index_reference: Option, pub evening: bool, } #[derive(Serialize)] pub struct TaskDatesJson { pub deadline_at: Option, pub created_at: Option, pub modified_at: Option, pub completed_at: Option, } #[derive(Serialize)] pub struct TaskRecurrenceJson { pub is_template: bool, pub is_instance: bool, pub rule: Option, pub template_ids: Vec, } #[derive(Serialize)] pub struct TaskFlagsJson { pub trashed: bool, pub is_new: bool, pub instance_creation_paused: bool, pub leaves_tombstone: bool, /// the object's history did not replay completely /// /// the CLI shows what it has and refuses to write it pub degraded: bool, } #[derive(Serialize)] pub struct TaskIndexesJson { pub sort_index: i32, pub today_sort_index: i32, } #[derive(Serialize)] pub struct LinkRefJson { pub id: String, pub short_id: String, pub title: String, } #[derive(Serialize)] pub struct TagRefJson { pub id: String, pub short_id: String, pub title: String, pub shortcut: Option, } #[derive(Serialize)] pub struct ResolvedChecklistItemJson { pub id: String, pub short_id: String, pub title: String, pub status: JsonTaskStatus, pub index: i32, } /// a wire value the CLI does not know /// /// kept as `unknown:` rather than passed off as a known one fn serialize_unknown(raw: i32, serializer: S) -> Result { serializer.serialize_str(&format!("unknown:{raw}")) } pub enum JsonTaskStatus { Incomplete, Completed, Canceled, Unknown(i32), } impl Serialize for JsonTaskStatus { fn serialize(&self, serializer: S) -> Result { match self { Self::Incomplete => serializer.serialize_str("incomplete"), Self::Completed => serializer.serialize_str("completed"), Self::Canceled => serializer.serialize_str("canceled"), Self::Unknown(raw) => serialize_unknown(*raw, serializer), } } } pub enum JsonStartBucket { Inbox, Anytime, Someday, Unknown(i32), } impl Serialize for JsonStartBucket { fn serialize(&self, serializer: S) -> Result { match self { Self::Inbox => serializer.serialize_str("inbox"), Self::Anytime => serializer.serialize_str("anytime"), Self::Someday => serializer.serialize_str("someday"), Self::Unknown(raw) => serialize_unknown(*raw, serializer), } } } pub enum JsonItemType { Todo, Project, Heading, Unknown(i32), } impl Serialize for JsonItemType { fn serialize(&self, serializer: S) -> Result { match self { Self::Todo => serializer.serialize_str("todo"), Self::Project => serializer.serialize_str("project"), Self::Heading => serializer.serialize_str("heading"), Self::Unknown(raw) => serialize_unknown(*raw, serializer), } } } pub fn link_ref(id: &str, title: String, store: &ThingsStore) -> LinkRefJson { LinkRefJson { id: id.to_string(), short_id: store.short_id(id), title, } } pub fn resolve_heading_title(id: &ThingsId, store: &ThingsStore) -> String { store .tasks_by_uuid .get(id) .map(|task| task.title.clone()) .unwrap_or_else(|| id.to_string()) } pub fn task_status_json(status: TaskStatus) -> JsonTaskStatus { match status { TaskStatus::Incomplete => JsonTaskStatus::Incomplete, TaskStatus::Completed => JsonTaskStatus::Completed, TaskStatus::Canceled => JsonTaskStatus::Canceled, TaskStatus::Unknown(raw) => JsonTaskStatus::Unknown(raw), } } pub fn task_start_json(start: TaskStart) -> JsonStartBucket { match start { TaskStart::Inbox => JsonStartBucket::Inbox, TaskStart::Anytime => JsonStartBucket::Anytime, TaskStart::Someday => JsonStartBucket::Someday, TaskStart::Unknown(raw) => JsonStartBucket::Unknown(raw), } } pub fn task_type_json(item_type: TaskType) -> JsonItemType { match item_type { TaskType::Todo => JsonItemType::Todo, TaskType::Project => JsonItemType::Project, TaskType::Heading => JsonItemType::Heading, TaskType::Unknown(raw) => JsonItemType::Unknown(raw), } } pub fn build_tasks_json( tasks: &[Task], store: &ThingsStore, today: &DateTime, ) -> Vec { tasks .iter() .map(|task| task_to_json(task, store, today)) .collect() } pub fn build_area_json(area: &Area, store: &ThingsStore) -> ResolvedAreaJson { area_to_json(area, store) } pub fn build_tags_json(tags: &[Tag], store: &ThingsStore) -> Vec { tags.iter().map(|tag| tag_to_json(tag, store)).collect() } fn task_to_json(task: &Task, store: &ThingsStore, today: &DateTime) -> ResolvedTaskJson { ResolvedTaskJson { core: TaskCoreJson { id: task.uuid.to_string(), short_id: store.short_id(&task.uuid), title: task.title.clone(), status: task_status_json(task.status), r#type: task_type_json(task.item_type), start: TaskStartJson { bucket: task_start_json(task.start), scheduled_at: task.start_date.map(|d| d.to_rfc3339()), reminder: task.reminder(), today_index_reference: task.today_index_reference, evening: task.evening, }, }, links: TaskLinksJson { project: store .effective_project_uuid(task) .map(|id| link_ref(&id.to_string(), store.resolve_project_title(&id), store)), area: store .effective_area_uuid(task) .map(|id| link_ref(&id.to_string(), store.resolve_area_title(&id), store)), heading: task .action_group .as_ref() .map(|id| link_ref(&id.to_string(), resolve_heading_title(id, store), store)), tags: task .tags .iter() .map(|tag_id| { let tag = store.tags_by_uuid.get(tag_id); TagRefJson { id: tag_id.to_string(), short_id: store.short_id(tag_id), title: tag.map(|t| t.title.clone()).unwrap_or_default(), shortcut: tag.and_then(|t| t.shortcut.clone()), } }) .collect(), }, dates: TaskDatesJson { deadline_at: task.deadline.map(|d| d.to_rfc3339()), created_at: task.creation_date.map(|d| d.to_rfc3339()), modified_at: task.modification_date.map(|d| d.to_rfc3339()), completed_at: task.stop_date.map(|d| d.to_rfc3339()), }, notes: task.notes.clone(), checklist: task .checklist_items .iter() .map(|item| ResolvedChecklistItemJson { id: item.uuid.to_string(), short_id: store.short_id(&item.uuid), title: item.title.clone(), status: task_status_json(item.status), index: item.index, }) .collect(), recurrence: TaskRecurrenceJson { is_template: task.is_recurrence_template(), is_instance: task.is_recurrence_instance(), rule: task .recurrence_rule .as_ref() .map(|rule| serde_json::to_value(rule).unwrap_or(serde_json::Value::Null)), template_ids: task .recurrence_templates .iter() .map(ToString::to_string) .collect(), }, flags: TaskFlagsJson { trashed: store.in_trash(task), is_new: store.in_today(task, today) && task.is_staged_for_today(today), instance_creation_paused: task.instance_creation_paused, leaves_tombstone: task.leaves_tombstone, degraded: task.degraded, }, indexes: TaskIndexesJson { sort_index: task.index, today_sort_index: task.today_index, }, progress: if task.is_project() { let progress = store.project_progress(&task.uuid); Some(TaskProgressJson { done: progress.done, total: progress.total, }) } else { None }, } } fn area_to_json(area: &Area, store: &ThingsStore) -> ResolvedAreaJson { ResolvedAreaJson { id: area.uuid.to_string(), short_id: store.short_id(&area.uuid), title: area.title.clone(), index: area.index, tags: area .tags .iter() .map(|tag_id| { let tag = store.tags_by_uuid.get(tag_id); TagRefJson { id: tag_id.to_string(), short_id: store.short_id(tag_id), title: tag.map(|t| t.title.clone()).unwrap_or_default(), shortcut: tag.and_then(|t| t.shortcut.clone()), } }) .collect(), degraded: area.degraded, } } fn tag_to_json(tag: &Tag, store: &ThingsStore) -> ResolvedTagJson { ResolvedTagJson { id: tag.uuid.to_string(), short_id: store.short_id(&tag.uuid), title: tag.title.clone(), shortcut: tag.shortcut.clone(), parent: tag.parent_uuid.as_ref().map(|id| { link_ref( &id.to_string(), store.resolve_tag_title(id.to_string()), store, ) }), index: tag.index, degraded: tag.degraded, } }