1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4#[derive(Debug, Serialize, Deserialize, Clone)]
7pub struct Task {
8 pub id: String,
9 #[serde(alias = "creator_id")]
11 pub user_id: String,
12 pub content: String,
13 pub description: String,
14 pub project_id: String,
15 pub section_id: Option<String>,
16 pub parent_id: Option<String>,
17 pub added_by_uid: Option<String>,
18 pub assigned_by_uid: Option<String>,
19 pub responsible_uid: Option<String>,
20 pub labels: Vec<String>,
21 pub deadline: Option<Deadline>,
22 pub duration: Option<Duration>,
23 #[serde(default)]
25 pub checked: bool,
26 #[serde(default)]
28 pub is_deleted: bool,
29 pub added_at: String,
30 pub completed_at: Option<String>,
32 pub completed_by_uid: Option<String>,
34 pub updated_at: Option<String>,
35 pub due: Option<Due>,
36 pub priority: i32,
37 pub child_order: i32,
38 #[serde(default)]
40 pub note_count: i32,
41 pub day_order: i32,
42 pub is_collapsed: bool,
43}
44
45#[derive(Debug, Serialize, Deserialize, Clone)]
48pub struct Project {
49 pub id: String,
50 pub name: String,
51 pub color: String,
52 #[serde(alias = "shared")]
54 pub is_shared: bool,
55 pub is_favorite: bool,
56 #[serde(alias = "is_inbox_project")]
58 pub inbox_project: bool,
59 pub view_style: String,
60 pub parent_id: Option<String>,
61 #[serde(default)]
63 pub child_order: i32,
64 pub creator_uid: Option<String>,
66 pub created_at: Option<String>,
68 pub updated_at: Option<String>,
70 #[serde(default)]
72 pub is_archived: bool,
73 #[serde(default)]
75 pub is_deleted: bool,
76 #[serde(default)]
78 pub is_frozen: bool,
79 #[serde(default)]
81 pub is_collapsed: bool,
82 #[serde(default)]
84 pub can_assign_tasks: bool,
85 #[serde(default)]
87 pub default_order: i32,
88 #[serde(default)]
90 pub description: String,
91 #[serde(default)]
93 pub public_key: String,
94 pub role: Option<String>,
96}
97
98#[derive(Debug, Serialize, Deserialize, Clone)]
101pub struct Label {
102 pub id: String,
103 pub name: String,
104 pub color: String,
105 pub order: Option<i32>,
107 pub is_favorite: bool,
108}
109
110#[derive(Debug, Serialize, Deserialize, Clone)]
113pub struct Section {
114 pub id: String,
115 #[serde(alias = "creator_id")]
117 pub user_id: String,
118 pub project_id: String,
119 pub added_at: String,
120 pub updated_at: Option<String>,
121 pub archived_at: Option<String>,
122 pub name: String,
123 pub section_order: i32,
124 pub is_archived: bool,
125 #[serde(default)]
127 pub is_deleted: bool,
128 pub is_collapsed: bool,
129}
130
131#[derive(Debug, Serialize, Deserialize, Clone)]
134pub struct Comment {
135 pub id: String,
136 #[serde(default)]
137 pub content: String,
138 pub posted_at: Option<String>,
139 pub posted_uid: Option<String>,
140 #[serde(alias = "attachment")]
142 pub file_attachment: Option<Attachment>,
143 pub uids_to_notify: Option<Vec<String>>,
145 #[serde(default)]
147 pub is_deleted: bool,
148 pub reactions: Option<serde_json::Value>,
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub project_id: Option<String>,
153 #[serde(skip_serializing_if = "Option::is_none")]
155 pub task_id: Option<String>,
156}
157
158#[derive(Debug, Serialize, Deserialize, Clone)]
160pub struct Attachment {
161 pub file_name: String,
162 pub file_type: String,
163 pub file_url: String,
164 pub resource_type: String,
165}
166
167#[derive(Debug, Serialize, Deserialize, Clone)]
169pub struct User {
170 pub id: String,
171 pub name: String,
172 pub email: String,
173 pub avatar_url: Option<String>,
174 pub is_premium: bool,
175 pub is_business_account: bool,
176}
177
178#[derive(Debug, Serialize, Deserialize, Clone)]
181pub struct Due {
182 pub string: String,
183 pub date: String,
184 pub is_recurring: bool,
185 pub datetime: Option<String>,
186 pub timezone: Option<String>,
187 pub lang: Option<String>,
189}
190
191#[derive(Debug, Serialize, Deserialize, Clone)]
194pub struct Deadline {
195 pub date: String,
196 pub lang: Option<String>,
198}
199
200#[derive(Debug, Serialize, Deserialize, Clone)]
202pub struct Duration {
203 pub amount: i32,
204 pub unit: String, }
206
207#[derive(Debug, Serialize, Deserialize, Clone)]
210pub struct PaginatedResponse<T> {
211 pub results: Vec<T>,
212 #[serde(skip_serializing_if = "Option::is_none")]
213 pub next_cursor: Option<String>,
214}
215
216#[derive(Debug, Serialize, Default)]
218pub struct CreateTaskArgs {
219 pub content: String,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub description: Option<String>,
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub project_id: Option<String>,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 pub section_id: Option<String>,
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub parent_id: Option<String>,
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub order: Option<i32>,
230 #[serde(skip_serializing_if = "Option::is_none")]
231 pub priority: Option<i32>,
232 #[serde(skip_serializing_if = "Option::is_none")]
233 pub labels: Option<Vec<String>>,
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub due_string: Option<String>,
236 #[serde(skip_serializing_if = "Option::is_none")]
237 pub due_date: Option<String>,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub due_datetime: Option<String>,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub due_lang: Option<String>,
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub deadline_date: Option<String>,
244 #[serde(skip_serializing_if = "Option::is_none")]
245 pub deadline_lang: Option<String>,
246 #[serde(skip_serializing_if = "Option::is_none")]
247 pub duration: Option<i32>,
248 #[serde(skip_serializing_if = "Option::is_none")]
249 pub duration_unit: Option<String>,
250}
251
252#[derive(Debug, Serialize, Default)]
254pub struct UpdateTaskArgs {
255 #[serde(skip_serializing_if = "Option::is_none")]
256 pub content: Option<String>,
257 #[serde(skip_serializing_if = "Option::is_none")]
258 pub description: Option<String>,
259 #[serde(skip_serializing_if = "Option::is_none")]
260 pub priority: Option<i32>,
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub labels: Option<Vec<String>>,
263 #[serde(skip_serializing_if = "Option::is_none")]
264 pub due_string: Option<String>,
265 #[serde(skip_serializing_if = "Option::is_none")]
266 pub due_date: Option<String>,
267 #[serde(skip_serializing_if = "Option::is_none")]
268 pub due_datetime: Option<String>,
269 #[serde(skip_serializing_if = "Option::is_none")]
270 pub due_lang: Option<String>,
271 #[serde(skip_serializing_if = "Option::is_none")]
272 pub deadline_date: Option<String>,
273 #[serde(skip_serializing_if = "Option::is_none")]
274 pub deadline_lang: Option<String>,
275 #[serde(skip_serializing_if = "Option::is_none")]
276 pub duration: Option<i32>,
277 #[serde(skip_serializing_if = "Option::is_none")]
278 pub duration_unit: Option<String>,
279}
280
281impl UpdateTaskArgs {
282 pub fn has_updates(&self) -> bool {
284 self.content.is_some()
285 || self.description.is_some()
286 || self.priority.is_some()
287 || self.labels.is_some()
288 || self.due_string.is_some()
289 || self.due_date.is_some()
290 || self.due_datetime.is_some()
291 || self.due_lang.is_some()
292 || self.deadline_date.is_some()
293 || self.deadline_lang.is_some()
294 || self.duration.is_some()
295 || self.duration_unit.is_some()
296 }
297}
298
299#[derive(Debug, Serialize, Default)]
301pub struct CreateProjectArgs {
302 pub name: String,
303 #[serde(skip_serializing_if = "Option::is_none")]
304 pub color: Option<String>,
305 #[serde(skip_serializing_if = "Option::is_none")]
306 pub parent_id: Option<String>,
307 #[serde(skip_serializing_if = "Option::is_none")]
308 pub is_favorite: Option<bool>,
309 #[serde(skip_serializing_if = "Option::is_none")]
310 pub view_style: Option<String>,
311}
312
313#[derive(Debug, Serialize, Default)]
315pub struct UpdateProjectArgs {
316 #[serde(skip_serializing_if = "Option::is_none")]
317 pub name: Option<String>,
318 #[serde(skip_serializing_if = "Option::is_none")]
319 pub color: Option<String>,
320 #[serde(skip_serializing_if = "Option::is_none")]
321 pub is_favorite: Option<bool>,
322 #[serde(skip_serializing_if = "Option::is_none")]
323 pub view_style: Option<String>,
324}
325
326impl UpdateProjectArgs {
327 pub fn has_updates(&self) -> bool {
329 self.name.is_some() || self.color.is_some() || self.is_favorite.is_some() || self.view_style.is_some()
330 }
331}
332
333#[derive(Debug, Serialize, Default)]
335pub struct CreateLabelArgs {
336 pub name: String,
337 #[serde(skip_serializing_if = "Option::is_none")]
338 pub color: Option<String>,
339 #[serde(skip_serializing_if = "Option::is_none")]
340 pub order: Option<i32>,
341 #[serde(skip_serializing_if = "Option::is_none")]
342 pub is_favorite: Option<bool>,
343}
344
345#[derive(Debug, Serialize, Default)]
347pub struct UpdateLabelArgs {
348 #[serde(skip_serializing_if = "Option::is_none")]
349 pub name: Option<String>,
350 #[serde(skip_serializing_if = "Option::is_none")]
351 pub color: Option<String>,
352 #[serde(skip_serializing_if = "Option::is_none")]
353 pub order: Option<i32>,
354 #[serde(skip_serializing_if = "Option::is_none")]
355 pub is_favorite: Option<bool>,
356}
357
358impl UpdateLabelArgs {
359 pub fn has_updates(&self) -> bool {
361 self.name.is_some() || self.color.is_some() || self.order.is_some() || self.is_favorite.is_some()
362 }
363}
364
365#[derive(Debug, Serialize, Default)]
367pub struct CreateSectionArgs {
368 pub name: String,
369 pub project_id: String,
370 #[serde(skip_serializing_if = "Option::is_none")]
371 pub order: Option<i32>,
372}
373
374#[derive(Debug, Serialize, Default)]
376pub struct UpdateSectionArgs {
377 pub name: String,
378}
379
380#[derive(Debug, Serialize, Default)]
382pub struct CreateCommentArgs {
383 pub content: String,
384 #[serde(skip_serializing_if = "Option::is_none")]
385 pub task_id: Option<String>,
386 #[serde(skip_serializing_if = "Option::is_none")]
387 pub project_id: Option<String>,
388 #[serde(skip_serializing_if = "Option::is_none")]
389 pub attachment: Option<Attachment>,
390}
391
392#[derive(Debug, Serialize, Default)]
394pub struct UpdateCommentArgs {
395 pub content: String,
396}
397
398impl UpdateCommentArgs {
399 pub fn has_updates(&self) -> bool {
402 !self.content.is_empty()
403 }
404}
405
406#[derive(Debug, Serialize)]
408pub struct TaskFilterArgs {
409 pub query: String,
410 pub lang: Option<String>,
411 pub limit: Option<i32>,
412 pub cursor: Option<String>,
413}
414
415#[derive(Debug, Serialize)]
417pub struct ProjectFilterArgs {
418 pub limit: Option<i32>,
419 pub cursor: Option<String>,
420}
421
422#[derive(Debug, Serialize)]
424pub struct LabelFilterArgs {
425 pub limit: Option<i32>,
426 pub cursor: Option<String>,
427}
428
429#[derive(Debug, Serialize)]
431pub struct SectionFilterArgs {
432 pub project_id: Option<String>,
433 pub limit: Option<i32>,
434 pub cursor: Option<String>,
435}
436
437#[derive(Debug, Serialize)]
439pub struct CommentFilterArgs {
440 pub task_id: Option<String>,
441 pub project_id: Option<String>,
442 pub limit: Option<i32>,
443 pub cursor: Option<String>,
444}
445
446#[derive(Debug, Serialize, Default)]
449pub struct CompletedTasksFilterArgs {
450 #[serde(skip_serializing_if = "Option::is_none")]
451 pub project_id: Option<String>,
452 #[serde(skip_serializing_if = "Option::is_none")]
453 pub section_id: Option<String>,
454 #[serde(skip_serializing_if = "Option::is_none")]
455 pub since: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
457 pub until: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
459 pub limit: Option<i32>,
460 #[serde(skip_serializing_if = "Option::is_none")]
461 pub cursor: Option<String>,
462}
463
464#[derive(Debug, Clone)]
466pub enum TodoistError {
467 RateLimited { retry_after: Option<u64>, message: String },
469 AuthenticationError { message: String },
471 AuthorizationError { message: String },
473 NotFound {
475 resource_type: String,
476 resource_id: Option<String>,
477 message: String,
478 },
479 ValidationError { field: Option<String>, message: String },
481 ServerError { status_code: u16, message: String },
483 NetworkError { message: String },
485 ParseError { message: String },
487 EmptyResponse { endpoint: String, message: String },
489 Generic { status_code: Option<u16>, message: String },
491}
492
493impl TodoistError {
494 pub fn is_rate_limited(&self) -> bool {
496 matches!(self, TodoistError::RateLimited { .. })
497 }
498
499 pub fn is_authentication_error(&self) -> bool {
501 matches!(self, TodoistError::AuthenticationError { .. })
502 }
503
504 pub fn is_authorization_error(&self) -> bool {
506 matches!(self, TodoistError::AuthorizationError { .. })
507 }
508
509 pub fn is_not_found(&self) -> bool {
511 matches!(self, TodoistError::NotFound { .. })
512 }
513
514 pub fn is_validation_error(&self) -> bool {
516 matches!(self, TodoistError::ValidationError { .. })
517 }
518
519 pub fn is_server_error(&self) -> bool {
521 matches!(self, TodoistError::ServerError { .. })
522 }
523
524 pub fn is_network_error(&self) -> bool {
526 matches!(self, TodoistError::NetworkError { .. })
527 }
528
529 pub fn is_empty_response(&self) -> bool {
531 matches!(self, TodoistError::EmptyResponse { .. })
532 }
533
534 pub fn retry_after(&self) -> Option<u64> {
536 match self {
537 TodoistError::RateLimited { retry_after, .. } => *retry_after,
538 _ => None,
539 }
540 }
541
542 pub fn status_code(&self) -> Option<u16> {
544 match self {
545 TodoistError::ServerError { status_code, .. } => Some(*status_code),
546 TodoistError::Generic { status_code, .. } => *status_code,
547 _ => None,
548 }
549 }
550}
551
552impl fmt::Display for TodoistError {
553 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
554 match self {
555 TodoistError::RateLimited { retry_after, message } => {
556 if let Some(seconds) = retry_after {
557 write!(f, "Rate limited: {} (retry after {} seconds)", message, seconds)
558 } else {
559 write!(f, "Rate limited: {}", message)
560 }
561 }
562 TodoistError::AuthenticationError { message } => {
563 write!(f, "Authentication error: {}", message)
564 }
565 TodoistError::AuthorizationError { message } => {
566 write!(f, "Authorization error: {}", message)
567 }
568 TodoistError::NotFound {
569 resource_type,
570 resource_id,
571 message,
572 } => {
573 if let Some(id) = resource_id {
574 write!(f, "{} not found (ID: {}): {}", resource_type, id, message)
575 } else {
576 write!(f, "{} not found: {}", resource_type, message)
577 }
578 }
579 TodoistError::ValidationError { field, message } => {
580 if let Some(field_name) = field {
581 write!(f, "Validation error for field '{}': {}", field_name, message)
582 } else {
583 write!(f, "Validation error: {}", message)
584 }
585 }
586 TodoistError::ServerError { status_code, message } => {
587 write!(f, "Server error ({}): {}", status_code, message)
588 }
589 TodoistError::NetworkError { message } => {
590 write!(f, "Network error: {}", message)
591 }
592 TodoistError::ParseError { message } => {
593 write!(f, "Parse error: {}", message)
594 }
595 TodoistError::EmptyResponse { endpoint, message } => {
596 write!(f, "Empty response from {}: {}", endpoint, message)
597 }
598 TodoistError::Generic { status_code, message } => {
599 if let Some(code) = status_code {
600 write!(f, "Error ({}): {}", code, message)
601 } else {
602 write!(f, "Error: {}", message)
603 }
604 }
605 }
606 }
607}
608
609impl std::error::Error for TodoistError {}
610
611impl From<reqwest::Error> for TodoistError {
612 fn from(err: reqwest::Error) -> Self {
613 TodoistError::NetworkError {
614 message: format!("Request failed: {}", err),
615 }
616 }
617}
618
619impl From<serde_json::Error> for TodoistError {
620 fn from(err: serde_json::Error) -> Self {
621 TodoistError::ParseError {
622 message: format!("JSON error: {}", err),
623 }
624 }
625}
626
627pub type TodoistResult<T> = Result<T, TodoistError>;
629
630pub fn rate_limited_error(message: impl Into<String>, retry_after: Option<u64>) -> TodoistError {
632 TodoistError::RateLimited {
633 retry_after,
634 message: message.into(),
635 }
636}
637
638pub fn empty_response_error(endpoint: impl Into<String>, message: impl Into<String>) -> TodoistError {
640 TodoistError::EmptyResponse {
641 endpoint: endpoint.into(),
642 message: message.into(),
643 }
644}
645
646pub fn not_found_error(
648 resource_type: impl Into<String>,
649 resource_id: Option<impl Into<String>>,
650 message: impl Into<String>,
651) -> TodoistError {
652 TodoistError::NotFound {
653 resource_type: resource_type.into(),
654 resource_id: resource_id.map(|id| id.into()),
655 message: message.into(),
656 }
657}