1use crate::ids::ThingsId;
2use crate::wire::notes::TaskNotes;
3use crate::wire::recurrence::RecurrenceRule;
4use crate::wire::{deserialize_default_on_null, deserialize_optional_field};
5use num_enum::{FromPrimitive, IntoPrimitive};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::collections::BTreeMap;
9use strum::{Display, EnumString};
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
13pub struct TaskProps {
14 #[serde(rename = "tt", default)]
16 pub title: String,
17
18 #[serde(rename = "nt", default)]
20 pub notes: Option<TaskNotes>,
21
22 #[serde(rename = "tp", default)]
24 pub item_type: TaskType,
25
26 #[serde(rename = "ss", default)]
28 pub status: TaskStatus,
29
30 #[serde(rename = "sp", default)]
32 pub stop_date: Option<f64>,
33
34 #[serde(rename = "st", default)]
36 pub start_location: TaskStart,
37
38 #[serde(rename = "sr", default)]
40 pub scheduled_date: Option<i64>,
41
42 #[serde(rename = "tir", default)]
44 pub today_index_reference: Option<i64>,
45
46 #[serde(rename = "dd", default)]
48 pub deadline: Option<i64>,
49
50 #[serde(rename = "dds", default)]
52 pub deadline_suppressed_date: Option<Value>,
53
54 #[serde(rename = "pr", default)]
56 pub parent_project_ids: Vec<ThingsId>,
57
58 #[serde(rename = "ar", default)]
60 pub area_ids: Vec<ThingsId>,
61
62 #[serde(rename = "agr", default)]
64 pub action_group_ids: Vec<ThingsId>,
65
66 #[serde(rename = "tg", default)]
68 pub tag_ids: Vec<ThingsId>,
69
70 #[serde(rename = "ix", default)]
72 pub sort_index: i32,
73
74 #[serde(
76 rename = "ti",
77 default,
78 deserialize_with = "deserialize_default_on_null"
79 )]
80 pub today_sort_index: i32,
81
82 #[serde(
84 rename = "do",
85 default,
86 deserialize_with = "deserialize_default_on_null"
87 )]
88 pub due_date_offset: i32,
89
90 #[serde(rename = "rr", default)]
92 pub recurrence_rule: Option<RecurrenceRule>,
93
94 #[serde(rename = "rt", default)]
96 pub recurrence_template_ids: Vec<ThingsId>,
97
98 #[serde(rename = "icsd", default)]
100 pub instance_creation_suppressed_date: Option<i64>,
101
102 #[serde(rename = "acrd", default)]
104 pub after_completion_reference_date: Option<i64>,
105
106 #[serde(
108 rename = "icc",
109 default,
110 deserialize_with = "deserialize_default_on_null"
111 )]
112 pub checklist_item_count: i32,
113
114 #[serde(rename = "icp", default)]
116 pub instance_creation_paused: bool,
117
118 #[serde(rename = "ato", default)]
120 pub alarm_time_offset: Option<i64>,
121
122 #[serde(rename = "lai", default)]
124 pub last_alarm_interaction: Option<f64>,
125
126 #[serde(
128 rename = "sb",
129 default,
130 deserialize_with = "deserialize_default_on_null"
131 )]
132 pub evening_bit: i32,
133
134 #[serde(
136 rename = "lt",
137 default,
138 deserialize_with = "deserialize_default_on_null"
139 )]
140 pub leaves_tombstone: bool,
141
142 #[serde(rename = "tr", default)]
144 pub trashed: bool,
145
146 #[serde(rename = "dl", default)]
148 pub deadline_list: Vec<Value>,
149
150 #[serde(rename = "xx", default)]
152 pub conflict_overrides: Option<Value>,
153
154 #[serde(rename = "cd", default)]
156 pub creation_date: Option<f64>,
157
158 #[serde(rename = "md", default)]
160 pub modification_date: Option<f64>,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
165pub struct TaskPatch {
166 #[serde(rename = "tt", skip_serializing_if = "Option::is_none")]
168 pub title: Option<String>,
169
170 #[serde(rename = "nt", skip_serializing_if = "Option::is_none")]
172 pub notes: Option<TaskNotes>,
173
174 #[serde(rename = "st", skip_serializing_if = "Option::is_none")]
176 pub start_location: Option<TaskStart>,
177
178 #[serde(
180 rename = "sr",
181 default,
182 deserialize_with = "deserialize_optional_field",
183 skip_serializing_if = "Option::is_none"
184 )]
185 pub scheduled_date: Option<Option<i64>>,
186
187 #[serde(
189 rename = "tir",
190 default,
191 deserialize_with = "deserialize_optional_field",
192 skip_serializing_if = "Option::is_none"
193 )]
194 pub today_index_reference: Option<Option<i64>>,
195
196 #[serde(rename = "pr", skip_serializing_if = "Option::is_none")]
198 pub parent_project_ids: Option<Vec<ThingsId>>,
199
200 #[serde(rename = "ar", skip_serializing_if = "Option::is_none")]
202 pub area_ids: Option<Vec<ThingsId>>,
203
204 #[serde(rename = "agr", skip_serializing_if = "Option::is_none")]
206 pub action_group_ids: Option<Vec<ThingsId>>,
207
208 #[serde(rename = "tg", skip_serializing_if = "Option::is_none")]
210 pub tag_ids: Option<Vec<ThingsId>>,
211
212 #[serde(rename = "sb", skip_serializing_if = "Option::is_none")]
214 pub evening_bit: Option<i32>,
215
216 #[serde(rename = "tp", skip_serializing_if = "Option::is_none")]
218 pub item_type: Option<TaskType>,
219
220 #[serde(rename = "ss", skip_serializing_if = "Option::is_none")]
222 pub status: Option<TaskStatus>,
223
224 #[serde(
226 rename = "sp",
227 default,
228 deserialize_with = "deserialize_optional_field",
229 skip_serializing_if = "Option::is_none"
230 )]
231 pub stop_date: Option<Option<f64>>,
232
233 #[serde(
235 rename = "dd",
236 default,
237 deserialize_with = "deserialize_optional_field",
238 skip_serializing_if = "Option::is_none"
239 )]
240 pub deadline: Option<Option<f64>>,
241
242 #[serde(rename = "ix", skip_serializing_if = "Option::is_none")]
244 pub sort_index: Option<i32>,
245
246 #[serde(rename = "ti", skip_serializing_if = "Option::is_none")]
248 pub today_sort_index: Option<i32>,
249
250 #[serde(
252 rename = "rr",
253 default,
254 deserialize_with = "deserialize_optional_field",
255 skip_serializing_if = "Option::is_none"
256 )]
257 pub recurrence_rule: Option<Option<RecurrenceRule>>,
258
259 #[serde(rename = "rt", skip_serializing_if = "Option::is_none")]
261 pub recurrence_template_ids: Option<Vec<ThingsId>>,
262
263 #[serde(rename = "icp", skip_serializing_if = "Option::is_none")]
265 pub instance_creation_paused: Option<bool>,
266
267 #[serde(rename = "lt", skip_serializing_if = "Option::is_none")]
269 pub leaves_tombstone: Option<bool>,
270
271 #[serde(rename = "tr", skip_serializing_if = "Option::is_none")]
273 pub trashed: Option<bool>,
274
275 #[serde(
277 rename = "cd",
278 default,
279 deserialize_with = "deserialize_optional_field",
280 skip_serializing_if = "Option::is_none"
281 )]
282 pub creation_date: Option<Option<f64>>,
283
284 #[serde(rename = "md", skip_serializing_if = "Option::is_none")]
286 pub modification_date: Option<f64>,
287}
288
289impl TaskPatch {
290 pub fn is_empty(&self) -> bool {
291 self.title.is_none()
292 && self.notes.is_none()
293 && self.start_location.is_none()
294 && self.scheduled_date.is_none()
295 && self.today_index_reference.is_none()
296 && self.parent_project_ids.is_none()
297 && self.area_ids.is_none()
298 && self.action_group_ids.is_none()
299 && self.tag_ids.is_none()
300 && self.evening_bit.is_none()
301 && self.item_type.is_none()
302 && self.status.is_none()
303 && self.stop_date.is_none()
304 && self.deadline.is_none()
305 && self.sort_index.is_none()
306 && self.today_sort_index.is_none()
307 && self.recurrence_rule.is_none()
308 && self.recurrence_template_ids.is_none()
309 && self.instance_creation_paused.is_none()
310 && self.leaves_tombstone.is_none()
311 && self.trashed.is_none()
312 && self.creation_date.is_none()
313 && self.modification_date.is_none()
314 }
315
316 pub fn into_properties(self) -> BTreeMap<String, Value> {
317 match serde_json::to_value(self) {
318 Ok(Value::Object(map)) => map.into_iter().collect(),
319 _ => BTreeMap::new(),
320 }
321 }
322}
323
324#[derive(
326 Debug,
327 Clone,
328 Copy,
329 Serialize,
330 Deserialize,
331 PartialEq,
332 Eq,
333 Display,
334 EnumString,
335 FromPrimitive,
336 IntoPrimitive,
337)]
338#[repr(i32)]
339#[serde(from = "i32", into = "i32")]
340pub enum TaskType {
341 Todo = 0,
343 Project = 1,
345 Heading = 2,
347
348 #[num_enum(catch_all)]
350 #[strum(disabled, to_string = "{0}")]
351 Unknown(i32),
352}
353
354#[expect(
355 clippy::derivable_impls,
356 reason = "num_enum(catch_all) conflicts with #[default]"
357)]
358impl Default for TaskType {
359 fn default() -> Self {
360 Self::Todo
361 }
362}
363
364#[derive(
366 Debug,
367 Clone,
368 Copy,
369 Serialize,
370 Deserialize,
371 PartialEq,
372 Eq,
373 Display,
374 EnumString,
375 FromPrimitive,
376 IntoPrimitive,
377)]
378#[repr(i32)]
379#[serde(from = "i32", into = "i32")]
380pub enum TaskStatus {
381 Incomplete = 0,
383 Canceled = 2,
385 Completed = 3,
387
388 #[num_enum(catch_all)]
390 #[strum(disabled, to_string = "{0}")]
391 Unknown(i32),
392}
393
394#[expect(
395 clippy::derivable_impls,
396 reason = "num_enum(catch_all) conflicts with #[default]"
397)]
398impl Default for TaskStatus {
399 fn default() -> Self {
400 Self::Incomplete
401 }
402}
403
404#[derive(
406 Debug,
407 Clone,
408 Copy,
409 Serialize,
410 Deserialize,
411 PartialEq,
412 Eq,
413 Display,
414 EnumString,
415 FromPrimitive,
416 IntoPrimitive,
417)]
418#[repr(i32)]
419#[serde(from = "i32", into = "i32")]
420pub enum TaskStart {
421 Inbox = 0,
423 Anytime = 1,
425 Someday = 2,
427
428 #[num_enum(catch_all)]
430 #[strum(disabled, to_string = "{0}")]
431 Unknown(i32),
432}
433
434#[expect(
435 clippy::derivable_impls,
436 reason = "num_enum(catch_all) conflicts with #[default]"
437)]
438impl Default for TaskStart {
439 fn default() -> Self {
440 Self::Inbox
441 }
442}