1use chrono::{DateTime, Utc};
2use serde::Serialize;
3
4use crate::{
5 ids::ThingsId,
6 store::{Area, Tag, Task, ThingsStore},
7 wire::task::{TaskStart, TaskStatus, TaskType},
8};
9
10#[derive(Debug, Serialize)]
11pub struct ResolvedTaskJson {
12 #[serde(flatten)]
13 core: TaskCoreJson,
14 #[serde(flatten)]
15 links: TaskLinksJson,
16 dates: TaskDatesJson,
17 notes: Option<String>,
18 checklist: Vec<ResolvedChecklistItemJson>,
19 recurrence: TaskRecurrenceJson,
20 flags: TaskFlagsJson,
21 indexes: TaskIndexesJson,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 progress: Option<TaskProgressJson>,
24}
25
26#[derive(Debug, Serialize)]
27pub struct ResolvedAreaJson {
28 pub id: String,
29 pub short_id: String,
30 pub title: String,
31 pub index: i32,
32 pub tags: Vec<TagRefJson>,
33}
34
35#[derive(Debug, Serialize)]
36pub struct ResolvedTagJson {
37 pub id: String,
38 pub short_id: String,
39 pub title: String,
40 pub shortcut: Option<String>,
41 pub parent: Option<LinkRefJson>,
42 pub index: i32,
43}
44
45#[derive(Debug, Serialize)]
46struct TaskProgressJson {
47 done: i32,
48 total: i32,
49}
50
51#[derive(Debug, Serialize)]
52pub struct TaskCoreJson {
53 pub id: String,
54 pub short_id: String,
55 pub title: String,
56 pub status: JsonTaskStatus,
57 pub r#type: JsonItemType,
58 pub start: TaskStartJson,
59}
60
61#[derive(Debug, Serialize)]
62pub struct TaskLinksJson {
63 pub project: Option<LinkRefJson>,
64 pub area: Option<LinkRefJson>,
65 pub heading: Option<LinkRefJson>,
66 pub tags: Vec<TagRefJson>,
67}
68
69#[derive(Debug, Serialize)]
70pub struct TaskStartJson {
71 pub bucket: JsonStartBucket,
72 pub scheduled_at: Option<String>,
73 pub today_index_reference: Option<i64>,
74 pub evening: bool,
75}
76
77#[derive(Debug, Serialize)]
78pub struct TaskDatesJson {
79 pub deadline_at: Option<String>,
80 pub created_at: Option<String>,
81 pub modified_at: Option<String>,
82 pub completed_at: Option<String>,
83}
84
85#[derive(Debug, Serialize)]
86pub struct TaskRecurrenceJson {
87 pub is_template: bool,
88 pub is_instance: bool,
89 pub rule: Option<serde_json::Value>,
90 pub template_ids: Vec<String>,
91}
92
93#[derive(Debug, Serialize)]
94pub struct TaskFlagsJson {
95 pub trashed: bool,
96 pub is_new: bool,
97 pub instance_creation_paused: bool,
98 pub leaves_tombstone: bool,
99}
100
101#[derive(Debug, Serialize)]
102pub struct TaskIndexesJson {
103 pub sort_index: i32,
104 pub today_sort_index: i32,
105}
106
107#[derive(Debug, Serialize)]
108pub struct LinkRefJson {
109 pub id: String,
110 pub short_id: String,
111 pub title: String,
112}
113
114#[derive(Debug, Serialize)]
115pub struct TagRefJson {
116 pub id: String,
117 pub short_id: String,
118 pub title: String,
119 pub shortcut: Option<String>,
120}
121
122#[derive(Debug, Serialize)]
123pub struct ResolvedChecklistItemJson {
124 pub id: String,
125 pub short_id: String,
126 pub title: String,
127 pub status: JsonTaskStatus,
128 pub index: i32,
129}
130
131#[derive(Debug, Clone, Copy, Serialize)]
132#[serde(rename_all = "snake_case")]
133pub enum JsonTaskStatus {
134 Incomplete,
135 Completed,
136 Canceled,
137}
138
139#[derive(Debug, Clone, Copy, Serialize)]
140#[serde(rename_all = "snake_case")]
141pub enum JsonStartBucket {
142 Inbox,
143 Anytime,
144 Someday,
145}
146
147#[derive(Debug, Clone, Copy, Serialize)]
148#[serde(rename_all = "snake_case")]
149pub enum JsonItemType {
150 Todo,
151 Project,
152}
153
154pub fn link_ref(id: &str, title: String, store: &ThingsStore) -> LinkRefJson {
155 LinkRefJson {
156 id: id.to_string(),
157 short_id: store.short_id(id),
158 title,
159 }
160}
161
162pub fn resolve_heading_title(id: &ThingsId, store: &ThingsStore) -> String {
163 store
164 .tasks_by_uuid
165 .get(id)
166 .map(|task| task.title.clone())
167 .unwrap_or_else(|| id.to_string())
168}
169
170pub fn task_status_json(status: TaskStatus) -> JsonTaskStatus {
171 match status {
172 TaskStatus::Incomplete => JsonTaskStatus::Incomplete,
173 TaskStatus::Completed => JsonTaskStatus::Completed,
174 TaskStatus::Canceled => JsonTaskStatus::Canceled,
175 TaskStatus::Unknown(_) => JsonTaskStatus::Incomplete,
176 }
177}
178
179pub fn task_start_json(start: TaskStart) -> JsonStartBucket {
180 match start {
181 TaskStart::Inbox => JsonStartBucket::Inbox,
182 TaskStart::Anytime => JsonStartBucket::Anytime,
183 TaskStart::Someday => JsonStartBucket::Someday,
184 TaskStart::Unknown(_) => JsonStartBucket::Inbox,
185 }
186}
187
188pub fn task_type_json(item_type: TaskType) -> JsonItemType {
189 match item_type {
190 TaskType::Project => JsonItemType::Project,
191 TaskType::Todo | TaskType::Heading | TaskType::Unknown(_) => JsonItemType::Todo,
192 }
193}
194
195pub fn build_tasks_json(
196 tasks: &[Task],
197 store: &ThingsStore,
198 today: &DateTime<Utc>,
199) -> Vec<ResolvedTaskJson> {
200 tasks
201 .iter()
202 .map(|task| task_to_json(task, store, today))
203 .collect()
204}
205
206pub fn build_area_json(area: &Area, store: &ThingsStore) -> ResolvedAreaJson {
207 area_to_json(area, store)
208}
209
210pub fn build_tags_json(tags: &[Tag], store: &ThingsStore) -> Vec<ResolvedTagJson> {
211 tags.iter().map(|tag| tag_to_json(tag, store)).collect()
212}
213
214fn task_to_json(task: &Task, store: &ThingsStore, today: &DateTime<Utc>) -> ResolvedTaskJson {
215 ResolvedTaskJson {
216 core: TaskCoreJson {
217 id: task.uuid.to_string(),
218 short_id: store.short_id(&task.uuid),
219 title: task.title.clone(),
220 status: task_status_json(task.status),
221 r#type: task_type_json(task.item_type),
222 start: TaskStartJson {
223 bucket: task_start_json(task.start),
224 scheduled_at: task.start_date.map(|d| d.to_rfc3339()),
225 today_index_reference: task.today_index_reference,
226 evening: task.evening,
227 },
228 },
229 links: TaskLinksJson {
230 project: store
231 .effective_project_uuid(task)
232 .map(|id| link_ref(&id.to_string(), store.resolve_project_title(&id), store)),
233 area: store
234 .effective_area_uuid(task)
235 .map(|id| link_ref(&id.to_string(), store.resolve_area_title(&id), store)),
236 heading: task
237 .action_group
238 .as_ref()
239 .map(|id| link_ref(&id.to_string(), resolve_heading_title(id, store), store)),
240 tags: task
241 .tags
242 .iter()
243 .map(|tag_id| {
244 let tag = store.tags_by_uuid.get(tag_id);
245 TagRefJson {
246 id: tag_id.to_string(),
247 short_id: store.short_id(tag_id),
248 title: tag.map(|t| t.title.clone()).unwrap_or_default(),
249 shortcut: tag.and_then(|t| t.shortcut.clone()),
250 }
251 })
252 .collect(),
253 },
254 dates: TaskDatesJson {
255 deadline_at: task.deadline.map(|d| d.to_rfc3339()),
256 created_at: task.creation_date.map(|d| d.to_rfc3339()),
257 modified_at: task.modification_date.map(|d| d.to_rfc3339()),
258 completed_at: task.stop_date.map(|d| d.to_rfc3339()),
259 },
260 notes: task.notes.clone(),
261 checklist: task
262 .checklist_items
263 .iter()
264 .map(|item| ResolvedChecklistItemJson {
265 id: item.uuid.to_string(),
266 short_id: store.short_id(&item.uuid),
267 title: item.title.clone(),
268 status: task_status_json(item.status),
269 index: item.index,
270 })
271 .collect(),
272 recurrence: TaskRecurrenceJson {
273 is_template: task.is_recurrence_template(),
274 is_instance: task.is_recurrence_instance(),
275 rule: task
276 .recurrence_rule
277 .as_ref()
278 .map(|rule| serde_json::to_value(rule).unwrap_or(serde_json::Value::Null)),
279 template_ids: task
280 .recurrence_templates
281 .iter()
282 .map(ToString::to_string)
283 .collect(),
284 },
285 flags: TaskFlagsJson {
286 trashed: task.trashed,
287 is_new: task.is_staged_for_today(today),
288 instance_creation_paused: task.instance_creation_paused,
289 leaves_tombstone: task.leaves_tombstone,
290 },
291 indexes: TaskIndexesJson {
292 sort_index: task.index,
293 today_sort_index: task.today_index,
294 },
295 progress: if task.is_project() {
296 let progress = store.project_progress(&task.uuid);
297 Some(TaskProgressJson {
298 done: progress.done,
299 total: progress.total,
300 })
301 } else {
302 None
303 },
304 }
305}
306
307fn area_to_json(area: &Area, store: &ThingsStore) -> ResolvedAreaJson {
308 ResolvedAreaJson {
309 id: area.uuid.to_string(),
310 short_id: store.short_id(&area.uuid),
311 title: area.title.clone(),
312 index: area.index,
313 tags: area
314 .tags
315 .iter()
316 .map(|tag_id| {
317 let tag = store.tags_by_uuid.get(tag_id);
318 TagRefJson {
319 id: tag_id.to_string(),
320 short_id: store.short_id(tag_id),
321 title: tag.map(|t| t.title.clone()).unwrap_or_default(),
322 shortcut: tag.and_then(|t| t.shortcut.clone()),
323 }
324 })
325 .collect(),
326 }
327}
328
329fn tag_to_json(tag: &Tag, store: &ThingsStore) -> ResolvedTagJson {
330 ResolvedTagJson {
331 id: tag.uuid.to_string(),
332 short_id: store.short_id(&tag.uuid),
333 title: tag.title.clone(),
334 shortcut: tag.shortcut.clone(),
335 parent: tag.parent_uuid.as_ref().map(|id| {
336 link_ref(
337 &id.to_string(),
338 store.resolve_tag_title(id.to_string()),
339 store,
340 )
341 }),
342 index: tag.index,
343 }
344}