todoist_api/
models.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4/// Todoist Task model
5#[derive(Debug, Serialize, Deserialize, Clone)]
6pub struct Task {
7    pub id: String,
8    pub content: String,
9    pub description: String,
10    pub project_id: String,
11    pub section_id: Option<String>,
12    pub parent_id: Option<String>,
13    pub order: i32,
14    pub priority: i32,
15    pub is_completed: bool,
16    pub labels: Vec<String>,
17    pub created_at: String,
18    pub due: Option<Due>,
19    pub deadline: Option<Deadline>,
20    pub duration: Option<Duration>,
21    pub assignee_id: Option<String>,
22    pub url: String,
23    pub comment_count: i32,
24}
25
26/// Todoist Project model
27#[derive(Debug, Serialize, Deserialize, Clone)]
28pub struct Project {
29    pub id: String,
30    pub name: String,
31    pub comment_count: i32,
32    pub order: i32,
33    pub color: String,
34    pub is_shared: bool,
35    pub is_favorite: bool,
36    pub is_inbox_project: bool,
37    pub is_team_inbox: bool,
38    pub view_style: String,
39    pub url: String,
40    pub parent_id: Option<String>,
41}
42
43/// Todoist Label model
44#[derive(Debug, Serialize, Deserialize, Clone)]
45pub struct Label {
46    pub id: String,
47    pub name: String,
48    pub color: String,
49    pub order: i32,
50    pub is_favorite: bool,
51}
52
53/// Todoist Section model
54#[derive(Debug, Serialize, Deserialize, Clone)]
55pub struct Section {
56    pub id: String,
57    pub name: String,
58    pub project_id: String,
59    pub order: i32,
60}
61
62/// Todoist Comment model
63#[derive(Debug, Serialize, Deserialize, Clone)]
64pub struct Comment {
65    pub id: String,
66    pub content: String,
67    pub posted_at: String,
68    pub attachment: Option<Attachment>,
69    pub project_id: Option<String>,
70    pub task_id: Option<String>,
71}
72
73/// Todoist Attachment model
74#[derive(Debug, Serialize, Deserialize, Clone)]
75pub struct Attachment {
76    pub file_name: String,
77    pub file_type: String,
78    pub file_url: String,
79    pub resource_type: String,
80}
81
82/// Todoist User model
83#[derive(Debug, Serialize, Deserialize, Clone)]
84pub struct User {
85    pub id: String,
86    pub name: String,
87    pub email: String,
88    pub avatar_url: Option<String>,
89    pub is_premium: bool,
90    pub is_business_account: bool,
91}
92
93/// Todoist Due date model
94#[derive(Debug, Serialize, Deserialize, Clone)]
95pub struct Due {
96    pub string: String,
97    pub date: String,
98    pub is_recurring: bool,
99    pub datetime: Option<String>,
100    pub timezone: Option<String>,
101}
102
103/// Todoist Deadline model
104#[derive(Debug, Serialize, Deserialize, Clone)]
105pub struct Deadline {
106    pub date: String,
107}
108
109/// Todoist Duration model
110#[derive(Debug, Serialize, Deserialize, Clone)]
111pub struct Duration {
112    pub amount: i32,
113    pub unit: String, // "minute", "hour", "day"
114}
115
116/// Task creation arguments
117#[derive(Debug, Serialize, Default)]
118pub struct CreateTaskArgs {
119    pub content: String,
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub description: Option<String>,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub project_id: Option<String>,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub section_id: Option<String>,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub parent_id: Option<String>,
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub order: Option<i32>,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub priority: Option<i32>,
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub labels: Option<Vec<String>>,
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub due_string: Option<String>,
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub due_date: Option<String>,
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub due_datetime: Option<String>,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub due_lang: Option<String>,
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub deadline_date: Option<String>,
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub deadline_lang: Option<String>,
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub assignee_id: Option<String>,
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub duration: Option<i32>,
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub duration_unit: Option<String>,
152}
153
154/// Task update arguments
155#[derive(Debug, Serialize, Default)]
156pub struct UpdateTaskArgs {
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub content: Option<String>,
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub description: Option<String>,
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub priority: Option<i32>,
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub labels: Option<Vec<String>>,
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub due_string: Option<String>,
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub due_date: Option<String>,
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub due_datetime: Option<String>,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub due_lang: Option<String>,
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub deadline_date: Option<String>,
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub deadline_lang: Option<String>,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub assignee_id: Option<String>,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub duration: Option<i32>,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub duration_unit: Option<String>,
183}
184
185impl UpdateTaskArgs {
186    /// Check if any fields are set for updating
187    pub fn has_updates(&self) -> bool {
188        self.content.is_some()
189            || self.description.is_some()
190            || self.priority.is_some()
191            || self.labels.is_some()
192            || self.due_string.is_some()
193            || self.due_date.is_some()
194            || self.due_datetime.is_some()
195            || self.due_lang.is_some()
196            || self.deadline_date.is_some()
197            || self.deadline_lang.is_some()
198            || self.assignee_id.is_some()
199            || self.duration.is_some()
200            || self.duration_unit.is_some()
201    }
202}
203
204/// Project creation arguments
205#[derive(Debug, Serialize, Default)]
206pub struct CreateProjectArgs {
207    pub name: String,
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub color: Option<String>,
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub parent_id: Option<String>,
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub is_favorite: Option<bool>,
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub view_style: Option<String>,
216}
217
218/// Project update arguments
219#[derive(Debug, Serialize, Default)]
220pub struct UpdateProjectArgs {
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub name: Option<String>,
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub color: Option<String>,
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub is_favorite: Option<bool>,
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub view_style: Option<String>,
229}
230
231impl UpdateProjectArgs {
232    /// Check if any fields are set for updating
233    pub fn has_updates(&self) -> bool {
234        self.name.is_some() || self.color.is_some() || self.is_favorite.is_some() || self.view_style.is_some()
235    }
236}
237
238/// Label creation arguments
239#[derive(Debug, Serialize, Default)]
240pub struct CreateLabelArgs {
241    pub name: String,
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub color: Option<String>,
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub order: Option<i32>,
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub is_favorite: Option<bool>,
248}
249
250/// Label update arguments
251#[derive(Debug, Serialize, Default)]
252pub struct UpdateLabelArgs {
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub name: Option<String>,
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub color: Option<String>,
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub order: Option<i32>,
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub is_favorite: Option<bool>,
261}
262
263impl UpdateLabelArgs {
264    /// Check if any fields are set for updating
265    pub fn has_updates(&self) -> bool {
266        self.name.is_some() || self.color.is_some() || self.order.is_some() || self.is_favorite.is_some()
267    }
268}
269
270/// Section creation arguments
271#[derive(Debug, Serialize, Default)]
272pub struct CreateSectionArgs {
273    pub name: String,
274    pub project_id: String,
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub order: Option<i32>,
277}
278
279/// Section update arguments
280#[derive(Debug, Serialize, Default)]
281pub struct UpdateSectionArgs {
282    pub name: String,
283}
284
285/// Comment creation arguments
286#[derive(Debug, Serialize, Default)]
287pub struct CreateCommentArgs {
288    pub content: String,
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub task_id: Option<String>,
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub project_id: Option<String>,
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub attachment: Option<Attachment>,
295}
296
297/// Comment update arguments
298#[derive(Debug, Serialize, Default)]
299pub struct UpdateCommentArgs {
300    pub content: String,
301}
302
303impl UpdateCommentArgs {
304    /// Check if any fields are set for updating
305    /// Note: UpdateCommentArgs only has required fields, so this always returns true when instantiated
306    pub fn has_updates(&self) -> bool {
307        !self.content.is_empty()
308    }
309}
310
311/// Task filter arguments
312#[derive(Debug, Serialize)]
313pub struct TaskFilterArgs {
314    pub query: String,
315    pub lang: Option<String>,
316    pub limit: Option<i32>,
317    pub cursor: Option<String>,
318}
319
320/// Project filter arguments
321#[derive(Debug, Serialize)]
322pub struct ProjectFilterArgs {
323    pub limit: Option<i32>,
324    pub cursor: Option<String>,
325}
326
327/// Label filter arguments
328#[derive(Debug, Serialize)]
329pub struct LabelFilterArgs {
330    pub limit: Option<i32>,
331    pub cursor: Option<String>,
332}
333
334/// Section filter arguments
335#[derive(Debug, Serialize)]
336pub struct SectionFilterArgs {
337    pub project_id: Option<String>,
338    pub limit: Option<i32>,
339    pub cursor: Option<String>,
340}
341
342/// Comment filter arguments
343#[derive(Debug, Serialize)]
344pub struct CommentFilterArgs {
345    pub task_id: Option<String>,
346    pub project_id: Option<String>,
347    pub limit: Option<i32>,
348    pub cursor: Option<String>,
349}
350
351/// Represents different types of errors that can occur when interacting with the Todoist API
352#[derive(Debug, Clone)]
353pub enum TodoistError {
354    /// Rate limiting error (HTTP 429)
355    RateLimited { retry_after: Option<u64>, message: String },
356    /// Authentication error (HTTP 401)
357    AuthenticationError { message: String },
358    /// Authorization error (HTTP 403)
359    AuthorizationError { message: String },
360    /// Resource not found (HTTP 404)
361    NotFound {
362        resource_type: String,
363        resource_id: Option<String>,
364        message: String,
365    },
366    /// Validation error (HTTP 400)
367    ValidationError { field: Option<String>, message: String },
368    /// Server error (HTTP 5xx)
369    ServerError { status_code: u16, message: String },
370    /// Network/connection error
371    NetworkError { message: String },
372    /// JSON parsing error
373    ParseError { message: String },
374    /// Unexpected empty response (when API returns nothing)
375    EmptyResponse { endpoint: String, message: String },
376    /// Generic error for other cases
377    Generic { status_code: Option<u16>, message: String },
378}
379
380impl TodoistError {
381    /// Check if this is a rate limiting error
382    pub fn is_rate_limited(&self) -> bool {
383        matches!(self, TodoistError::RateLimited { .. })
384    }
385
386    /// Check if this is an authentication error
387    pub fn is_authentication_error(&self) -> bool {
388        matches!(self, TodoistError::AuthenticationError { .. })
389    }
390
391    /// Check if this is an authorization error
392    pub fn is_authorization_error(&self) -> bool {
393        matches!(self, TodoistError::AuthorizationError { .. })
394    }
395
396    /// Check if this is a not found error
397    pub fn is_not_found(&self) -> bool {
398        matches!(self, TodoistError::NotFound { .. })
399    }
400
401    /// Check if this is a validation error
402    pub fn is_validation_error(&self) -> bool {
403        matches!(self, TodoistError::ValidationError { .. })
404    }
405
406    /// Check if this is a server error
407    pub fn is_server_error(&self) -> bool {
408        matches!(self, TodoistError::ServerError { .. })
409    }
410
411    /// Check if this is a network error
412    pub fn is_network_error(&self) -> bool {
413        matches!(self, TodoistError::NetworkError { .. })
414    }
415
416    /// Check if this is an empty response error
417    pub fn is_empty_response(&self) -> bool {
418        matches!(self, TodoistError::EmptyResponse { .. })
419    }
420
421    /// Get the retry after value for rate limiting errors
422    pub fn retry_after(&self) -> Option<u64> {
423        match self {
424            TodoistError::RateLimited { retry_after, .. } => *retry_after,
425            _ => None,
426        }
427    }
428
429    /// Get the HTTP status code if available
430    pub fn status_code(&self) -> Option<u16> {
431        match self {
432            TodoistError::ServerError { status_code, .. } => Some(*status_code),
433            TodoistError::Generic { status_code, .. } => *status_code,
434            _ => None,
435        }
436    }
437}
438
439impl fmt::Display for TodoistError {
440    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
441        match self {
442            TodoistError::RateLimited { retry_after, message } => {
443                if let Some(seconds) = retry_after {
444                    write!(f, "Rate limited: {} (retry after {} seconds)", message, seconds)
445                } else {
446                    write!(f, "Rate limited: {}", message)
447                }
448            }
449            TodoistError::AuthenticationError { message } => {
450                write!(f, "Authentication error: {}", message)
451            }
452            TodoistError::AuthorizationError { message } => {
453                write!(f, "Authorization error: {}", message)
454            }
455            TodoistError::NotFound {
456                resource_type,
457                resource_id,
458                message,
459            } => {
460                if let Some(id) = resource_id {
461                    write!(f, "{} not found (ID: {}): {}", resource_type, id, message)
462                } else {
463                    write!(f, "{} not found: {}", resource_type, message)
464                }
465            }
466            TodoistError::ValidationError { field, message } => {
467                if let Some(field_name) = field {
468                    write!(f, "Validation error for field '{}': {}", field_name, message)
469                } else {
470                    write!(f, "Validation error: {}", message)
471                }
472            }
473            TodoistError::ServerError { status_code, message } => {
474                write!(f, "Server error ({}): {}", status_code, message)
475            }
476            TodoistError::NetworkError { message } => {
477                write!(f, "Network error: {}", message)
478            }
479            TodoistError::ParseError { message } => {
480                write!(f, "Parse error: {}", message)
481            }
482            TodoistError::EmptyResponse { endpoint, message } => {
483                write!(f, "Empty response from {}: {}", endpoint, message)
484            }
485            TodoistError::Generic { status_code, message } => {
486                if let Some(code) = status_code {
487                    write!(f, "Error ({}): {}", code, message)
488                } else {
489                    write!(f, "Error: {}", message)
490                }
491            }
492        }
493    }
494}
495
496impl std::error::Error for TodoistError {}
497
498impl From<reqwest::Error> for TodoistError {
499    fn from(err: reqwest::Error) -> Self {
500        TodoistError::NetworkError {
501            message: format!("Request failed: {}", err),
502        }
503    }
504}
505
506impl From<serde_json::Error> for TodoistError {
507    fn from(err: serde_json::Error) -> Self {
508        TodoistError::ParseError {
509            message: format!("JSON error: {}", err),
510        }
511    }
512}
513
514/// Result type for Todoist API operations
515pub type TodoistResult<T> = Result<T, TodoistError>;
516
517/// Helper function to create a rate limiting error
518pub fn rate_limited_error(message: impl Into<String>, retry_after: Option<u64>) -> TodoistError {
519    TodoistError::RateLimited {
520        retry_after,
521        message: message.into(),
522    }
523}
524
525/// Helper function to create an empty response error
526pub fn empty_response_error(endpoint: impl Into<String>, message: impl Into<String>) -> TodoistError {
527    TodoistError::EmptyResponse {
528        endpoint: endpoint.into(),
529        message: message.into(),
530    }
531}
532
533/// Helper function to create a not found error
534pub fn not_found_error(
535    resource_type: impl Into<String>,
536    resource_id: Option<impl Into<String>>,
537    message: impl Into<String>,
538) -> TodoistError {
539    TodoistError::NotFound {
540        resource_type: resource_type.into(),
541        resource_id: resource_id.map(|id| id.into()),
542        message: message.into(),
543    }
544}