1use crate::ids::ThingsId;
2use crate::wire::deserialize_optional_field;
3use crate::wire::notes::TaskNotes;
4use crate::wire::recurrence::RecurrenceRule;
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(rename = "ti", default)]
76 pub today_sort_index: i32,
77
78 #[serde(rename = "do", default)]
80 pub due_date_offset: i32,
81
82 #[serde(rename = "rr", default)]
84 pub recurrence_rule: Option<RecurrenceRule>,
85
86 #[serde(rename = "rt", default)]
88 pub recurrence_template_ids: Vec<ThingsId>,
89
90 #[serde(rename = "icsd", default)]
92 pub instance_creation_suppressed_date: Option<i64>,
93
94 #[serde(rename = "acrd", default)]
96 pub after_completion_reference_date: Option<i64>,
97
98 #[serde(rename = "icc", default)]
100 pub checklist_item_count: i32,
101
102 #[serde(rename = "icp", default)]
104 pub instance_creation_paused: bool,
105
106 #[serde(rename = "ato", default)]
108 pub alarm_time_offset: Option<i64>,
109
110 #[serde(rename = "lai", default)]
112 pub last_alarm_interaction: Option<f64>,
113
114 #[serde(rename = "sb", default)]
116 pub evening_bit: i32,
117
118 #[serde(rename = "lt", default)]
120 pub leaves_tombstone: bool,
121
122 #[serde(rename = "tr", default)]
124 pub trashed: bool,
125
126 #[serde(rename = "dl", default)]
128 pub deadline_list: Vec<Value>,
129
130 #[serde(rename = "xx", default)]
132 pub conflict_overrides: Option<Value>,
133
134 #[serde(rename = "cd", default)]
136 pub creation_date: Option<f64>,
137
138 #[serde(rename = "md", default)]
140 pub modification_date: Option<f64>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
145pub struct TaskPatch {
146 #[serde(rename = "tt", skip_serializing_if = "Option::is_none")]
148 pub title: Option<String>,
149
150 #[serde(rename = "nt", skip_serializing_if = "Option::is_none")]
152 pub notes: Option<TaskNotes>,
153
154 #[serde(rename = "st", skip_serializing_if = "Option::is_none")]
156 pub start_location: Option<TaskStart>,
157
158 #[serde(
160 rename = "sr",
161 default,
162 deserialize_with = "deserialize_optional_field",
163 skip_serializing_if = "Option::is_none"
164 )]
165 pub scheduled_date: Option<Option<i64>>,
166
167 #[serde(
169 rename = "tir",
170 default,
171 deserialize_with = "deserialize_optional_field",
172 skip_serializing_if = "Option::is_none"
173 )]
174 pub today_index_reference: Option<Option<i64>>,
175
176 #[serde(rename = "pr", skip_serializing_if = "Option::is_none")]
178 pub parent_project_ids: Option<Vec<ThingsId>>,
179
180 #[serde(rename = "ar", skip_serializing_if = "Option::is_none")]
182 pub area_ids: Option<Vec<ThingsId>>,
183
184 #[serde(rename = "agr", skip_serializing_if = "Option::is_none")]
186 pub action_group_ids: Option<Vec<ThingsId>>,
187
188 #[serde(rename = "tg", skip_serializing_if = "Option::is_none")]
190 pub tag_ids: Option<Vec<ThingsId>>,
191
192 #[serde(rename = "sb", skip_serializing_if = "Option::is_none")]
194 pub evening_bit: Option<i32>,
195
196 #[serde(rename = "tp", skip_serializing_if = "Option::is_none")]
198 pub item_type: Option<TaskType>,
199
200 #[serde(rename = "ss", skip_serializing_if = "Option::is_none")]
202 pub status: Option<TaskStatus>,
203
204 #[serde(
206 rename = "sp",
207 default,
208 deserialize_with = "deserialize_optional_field",
209 skip_serializing_if = "Option::is_none"
210 )]
211 pub stop_date: Option<Option<f64>>,
212
213 #[serde(
215 rename = "dd",
216 default,
217 deserialize_with = "deserialize_optional_field",
218 skip_serializing_if = "Option::is_none"
219 )]
220 pub deadline: Option<Option<f64>>,
221
222 #[serde(rename = "ix", skip_serializing_if = "Option::is_none")]
224 pub sort_index: Option<i32>,
225
226 #[serde(rename = "ti", skip_serializing_if = "Option::is_none")]
228 pub today_sort_index: Option<i32>,
229
230 #[serde(
232 rename = "rr",
233 default,
234 deserialize_with = "deserialize_optional_field",
235 skip_serializing_if = "Option::is_none"
236 )]
237 pub recurrence_rule: Option<Option<RecurrenceRule>>,
238
239 #[serde(rename = "rt", skip_serializing_if = "Option::is_none")]
241 pub recurrence_template_ids: Option<Vec<ThingsId>>,
242
243 #[serde(rename = "icp", skip_serializing_if = "Option::is_none")]
245 pub instance_creation_paused: Option<bool>,
246
247 #[serde(rename = "lt", skip_serializing_if = "Option::is_none")]
249 pub leaves_tombstone: Option<bool>,
250
251 #[serde(rename = "tr", skip_serializing_if = "Option::is_none")]
253 pub trashed: Option<bool>,
254
255 #[serde(
257 rename = "cd",
258 default,
259 deserialize_with = "deserialize_optional_field",
260 skip_serializing_if = "Option::is_none"
261 )]
262 pub creation_date: Option<Option<f64>>,
263
264 #[serde(rename = "md", skip_serializing_if = "Option::is_none")]
266 pub modification_date: Option<f64>,
267}
268
269impl TaskPatch {
270 pub fn is_empty(&self) -> bool {
271 self.title.is_none()
272 && self.notes.is_none()
273 && self.start_location.is_none()
274 && self.scheduled_date.is_none()
275 && self.today_index_reference.is_none()
276 && self.parent_project_ids.is_none()
277 && self.area_ids.is_none()
278 && self.action_group_ids.is_none()
279 && self.tag_ids.is_none()
280 && self.evening_bit.is_none()
281 && self.item_type.is_none()
282 && self.status.is_none()
283 && self.stop_date.is_none()
284 && self.deadline.is_none()
285 && self.sort_index.is_none()
286 && self.today_sort_index.is_none()
287 && self.recurrence_rule.is_none()
288 && self.recurrence_template_ids.is_none()
289 && self.instance_creation_paused.is_none()
290 && self.leaves_tombstone.is_none()
291 && self.trashed.is_none()
292 && self.creation_date.is_none()
293 && self.modification_date.is_none()
294 }
295
296 pub fn into_properties(self) -> BTreeMap<String, Value> {
297 match serde_json::to_value(self) {
298 Ok(Value::Object(map)) => map.into_iter().collect(),
299 _ => BTreeMap::new(),
300 }
301 }
302}
303
304#[derive(
306 Debug,
307 Clone,
308 Copy,
309 Serialize,
310 Deserialize,
311 PartialEq,
312 Eq,
313 Display,
314 EnumString,
315 FromPrimitive,
316 IntoPrimitive,
317)]
318#[repr(i32)]
319#[serde(from = "i32", into = "i32")]
320pub enum TaskType {
321 Todo = 0,
323 Project = 1,
325 Heading = 2,
327
328 #[num_enum(catch_all)]
330 #[strum(disabled, to_string = "{0}")]
331 Unknown(i32),
332}
333
334#[expect(
335 clippy::derivable_impls,
336 reason = "num_enum(catch_all) conflicts with #[default]"
337)]
338impl Default for TaskType {
339 fn default() -> Self {
340 Self::Todo
341 }
342}
343
344#[derive(
346 Debug,
347 Clone,
348 Copy,
349 Serialize,
350 Deserialize,
351 PartialEq,
352 Eq,
353 Display,
354 EnumString,
355 FromPrimitive,
356 IntoPrimitive,
357)]
358#[repr(i32)]
359#[serde(from = "i32", into = "i32")]
360pub enum TaskStatus {
361 Incomplete = 0,
363 Canceled = 2,
365 Completed = 3,
367
368 #[num_enum(catch_all)]
370 #[strum(disabled, to_string = "{0}")]
371 Unknown(i32),
372}
373
374#[expect(
375 clippy::derivable_impls,
376 reason = "num_enum(catch_all) conflicts with #[default]"
377)]
378impl Default for TaskStatus {
379 fn default() -> Self {
380 Self::Incomplete
381 }
382}
383
384#[derive(
386 Debug,
387 Clone,
388 Copy,
389 Serialize,
390 Deserialize,
391 PartialEq,
392 Eq,
393 Display,
394 EnumString,
395 FromPrimitive,
396 IntoPrimitive,
397)]
398#[repr(i32)]
399#[serde(from = "i32", into = "i32")]
400pub enum TaskStart {
401 Inbox = 0,
403 Anytime = 1,
405 Someday = 2,
407
408 #[num_enum(catch_all)]
410 #[strum(disabled, to_string = "{0}")]
411 Unknown(i32),
412}
413
414#[expect(
415 clippy::derivable_impls,
416 reason = "num_enum(catch_all) conflicts with #[default]"
417)]
418impl Default for TaskStart {
419 fn default() -> Self {
420 Self::Inbox
421 }
422}