1use std::collections::BTreeMap;
2
3use num_enum::{FromPrimitive, IntoPrimitive};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use strum::{Display, EnumString};
7
8use crate::{
9 ids::ThingsId,
10 wire::{
11 deserialize_default_on_null,
12 deserialize_optional_field,
13 notes::TaskNotes,
14 recurrence::RecurrenceRule,
15 },
16};
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
20pub struct TaskProps {
21 #[serde(rename = "tt", default)]
23 pub title: String,
24
25 #[serde(rename = "nt", default)]
27 pub notes: Option<TaskNotes>,
28
29 #[serde(rename = "tp", default)]
31 pub item_type: TaskType,
32
33 #[serde(rename = "ss", default)]
35 pub status: TaskStatus,
36
37 #[serde(rename = "sp", default)]
39 pub stop_date: Option<f64>,
40
41 #[serde(rename = "st", default)]
43 pub start_location: TaskStart,
44
45 #[serde(rename = "sr", default)]
47 pub scheduled_date: Option<i64>,
48
49 #[serde(rename = "tir", default)]
51 pub today_index_reference: Option<i64>,
52
53 #[serde(rename = "dd", default)]
55 pub deadline: Option<i64>,
56
57 #[serde(rename = "dds", default)]
59 pub deadline_suppressed_date: Option<Value>,
60
61 #[serde(rename = "pr", default)]
63 pub parent_project_ids: Vec<ThingsId>,
64
65 #[serde(rename = "ar", default)]
67 pub area_ids: Vec<ThingsId>,
68
69 #[serde(rename = "agr", default)]
71 pub action_group_ids: Vec<ThingsId>,
72
73 #[serde(rename = "tg", default)]
75 pub tag_ids: Vec<ThingsId>,
76
77 #[serde(rename = "ix", default)]
79 pub sort_index: i32,
80
81 #[serde(
83 rename = "ti",
84 default,
85 deserialize_with = "deserialize_default_on_null"
86 )]
87 pub today_sort_index: i32,
88
89 #[serde(
91 rename = "do",
92 default,
93 deserialize_with = "deserialize_default_on_null"
94 )]
95 pub due_date_offset: i32,
96
97 #[serde(rename = "rr", default)]
99 pub recurrence_rule: Option<RecurrenceRule>,
100
101 #[serde(rename = "rt", default)]
103 pub recurrence_template_ids: Vec<ThingsId>,
104
105 #[serde(rename = "icsd", default)]
107 pub instance_creation_suppressed_date: Option<i64>,
108
109 #[serde(rename = "acrd", default)]
111 pub after_completion_reference_date: Option<i64>,
112
113 #[serde(
115 rename = "icc",
116 default,
117 deserialize_with = "deserialize_default_on_null"
118 )]
119 pub checklist_item_count: i32,
120
121 #[serde(rename = "icp", default)]
123 pub instance_creation_paused: bool,
124
125 #[serde(rename = "ato", default)]
127 pub alarm_time_offset: Option<i64>,
128
129 #[serde(rename = "lai", default)]
131 pub last_alarm_interaction: Option<f64>,
132
133 #[serde(
135 rename = "sb",
136 default,
137 deserialize_with = "deserialize_default_on_null"
138 )]
139 pub evening_bit: i32,
140
141 #[serde(
143 rename = "lt",
144 default,
145 deserialize_with = "deserialize_default_on_null"
146 )]
147 pub leaves_tombstone: bool,
148
149 #[serde(rename = "tr", default)]
151 pub trashed: bool,
152
153 #[serde(rename = "dl", default)]
155 pub deadline_list: Vec<Value>,
156
157 #[serde(rename = "xx", default)]
159 pub conflict_overrides: Option<Value>,
160
161 #[serde(rename = "cd", default)]
163 pub creation_date: Option<f64>,
164
165 #[serde(rename = "md", default)]
167 pub modification_date: Option<f64>,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
172pub struct TaskPatch {
173 #[serde(rename = "tt", skip_serializing_if = "Option::is_none")]
175 pub title: Option<String>,
176
177 #[serde(rename = "nt", skip_serializing_if = "Option::is_none")]
179 pub notes: Option<TaskNotes>,
180
181 #[serde(rename = "st", skip_serializing_if = "Option::is_none")]
183 pub start_location: Option<TaskStart>,
184
185 #[serde(
187 rename = "sr",
188 default,
189 deserialize_with = "deserialize_optional_field",
190 skip_serializing_if = "Option::is_none"
191 )]
192 pub scheduled_date: Option<Option<i64>>,
193
194 #[serde(
196 rename = "tir",
197 default,
198 deserialize_with = "deserialize_optional_field",
199 skip_serializing_if = "Option::is_none"
200 )]
201 pub today_index_reference: Option<Option<i64>>,
202
203 #[serde(rename = "pr", skip_serializing_if = "Option::is_none")]
205 pub parent_project_ids: Option<Vec<ThingsId>>,
206
207 #[serde(rename = "ar", skip_serializing_if = "Option::is_none")]
209 pub area_ids: Option<Vec<ThingsId>>,
210
211 #[serde(rename = "agr", skip_serializing_if = "Option::is_none")]
213 pub action_group_ids: Option<Vec<ThingsId>>,
214
215 #[serde(rename = "tg", skip_serializing_if = "Option::is_none")]
217 pub tag_ids: Option<Vec<ThingsId>>,
218
219 #[serde(rename = "sb", skip_serializing_if = "Option::is_none")]
221 pub evening_bit: Option<i32>,
222
223 #[serde(rename = "tp", skip_serializing_if = "Option::is_none")]
225 pub item_type: Option<TaskType>,
226
227 #[serde(rename = "ss", skip_serializing_if = "Option::is_none")]
229 pub status: Option<TaskStatus>,
230
231 #[serde(
233 rename = "sp",
234 default,
235 deserialize_with = "deserialize_optional_field",
236 skip_serializing_if = "Option::is_none"
237 )]
238 pub stop_date: Option<Option<f64>>,
239
240 #[serde(
242 rename = "dd",
243 default,
244 deserialize_with = "deserialize_optional_field",
245 skip_serializing_if = "Option::is_none"
246 )]
247 pub deadline: Option<Option<f64>>,
248
249 #[serde(rename = "ix", skip_serializing_if = "Option::is_none")]
251 pub sort_index: Option<i32>,
252
253 #[serde(rename = "ti", skip_serializing_if = "Option::is_none")]
255 pub today_sort_index: Option<i32>,
256
257 #[serde(
259 rename = "rr",
260 default,
261 deserialize_with = "deserialize_optional_field",
262 skip_serializing_if = "Option::is_none"
263 )]
264 pub recurrence_rule: Option<Option<RecurrenceRule>>,
265
266 #[serde(rename = "rt", skip_serializing_if = "Option::is_none")]
268 pub recurrence_template_ids: Option<Vec<ThingsId>>,
269
270 #[serde(rename = "icp", skip_serializing_if = "Option::is_none")]
272 pub instance_creation_paused: Option<bool>,
273
274 #[serde(rename = "lt", skip_serializing_if = "Option::is_none")]
276 pub leaves_tombstone: Option<bool>,
277
278 #[serde(rename = "tr", skip_serializing_if = "Option::is_none")]
280 pub trashed: Option<bool>,
281
282 #[serde(
284 rename = "cd",
285 default,
286 deserialize_with = "deserialize_optional_field",
287 skip_serializing_if = "Option::is_none"
288 )]
289 pub creation_date: Option<Option<f64>>,
290
291 #[serde(rename = "md", skip_serializing_if = "Option::is_none")]
293 pub modification_date: Option<f64>,
294}
295
296impl TaskPatch {
297 pub fn is_empty(&self) -> bool {
298 self.title.is_none()
299 && self.notes.is_none()
300 && self.start_location.is_none()
301 && self.scheduled_date.is_none()
302 && self.today_index_reference.is_none()
303 && self.parent_project_ids.is_none()
304 && self.area_ids.is_none()
305 && self.action_group_ids.is_none()
306 && self.tag_ids.is_none()
307 && self.evening_bit.is_none()
308 && self.item_type.is_none()
309 && self.status.is_none()
310 && self.stop_date.is_none()
311 && self.deadline.is_none()
312 && self.sort_index.is_none()
313 && self.today_sort_index.is_none()
314 && self.recurrence_rule.is_none()
315 && self.recurrence_template_ids.is_none()
316 && self.instance_creation_paused.is_none()
317 && self.leaves_tombstone.is_none()
318 && self.trashed.is_none()
319 && self.creation_date.is_none()
320 && self.modification_date.is_none()
321 }
322
323 pub fn into_properties(self) -> BTreeMap<String, Value> {
324 match serde_json::to_value(self) {
325 Ok(Value::Object(map)) => map.into_iter().collect(),
326 _ => BTreeMap::new(),
327 }
328 }
329}
330
331#[derive(
333 Debug,
334 Clone,
335 Copy,
336 Serialize,
337 Deserialize,
338 PartialEq,
339 Eq,
340 Display,
341 EnumString,
342 FromPrimitive,
343 IntoPrimitive,
344)]
345#[repr(i32)]
346#[serde(from = "i32", into = "i32")]
347pub enum TaskType {
348 Todo = 0,
350 Project = 1,
352 Heading = 2,
354
355 #[num_enum(catch_all)]
357 #[strum(disabled, to_string = "{0}")]
358 Unknown(i32),
359}
360
361#[expect(
362 clippy::derivable_impls,
363 reason = "num_enum(catch_all) conflicts with #[default]"
364)]
365impl Default for TaskType {
366 fn default() -> Self {
367 Self::Todo
368 }
369}
370
371#[derive(
373 Debug,
374 Clone,
375 Copy,
376 Serialize,
377 Deserialize,
378 PartialEq,
379 Eq,
380 Display,
381 EnumString,
382 FromPrimitive,
383 IntoPrimitive,
384)]
385#[repr(i32)]
386#[serde(from = "i32", into = "i32")]
387pub enum TaskStatus {
388 Incomplete = 0,
390 Canceled = 2,
392 Completed = 3,
394
395 #[num_enum(catch_all)]
397 #[strum(disabled, to_string = "{0}")]
398 Unknown(i32),
399}
400
401#[expect(
402 clippy::derivable_impls,
403 reason = "num_enum(catch_all) conflicts with #[default]"
404)]
405impl Default for TaskStatus {
406 fn default() -> Self {
407 Self::Incomplete
408 }
409}
410
411#[derive(
413 Debug,
414 Clone,
415 Copy,
416 Serialize,
417 Deserialize,
418 PartialEq,
419 Eq,
420 Display,
421 EnumString,
422 FromPrimitive,
423 IntoPrimitive,
424)]
425#[repr(i32)]
426#[serde(from = "i32", into = "i32")]
427pub enum TaskStart {
428 Inbox = 0,
430 Anytime = 1,
432 Someday = 2,
434
435 #[num_enum(catch_all)]
437 #[strum(disabled, to_string = "{0}")]
438 Unknown(i32),
439}
440
441#[expect(
442 clippy::derivable_impls,
443 reason = "num_enum(catch_all) conflicts with #[default]"
444)]
445impl Default for TaskStart {
446 fn default() -> Self {
447 Self::Inbox
448 }
449}