1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4#[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#[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#[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#[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#[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#[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#[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#[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#[derive(Debug, Serialize, Deserialize, Clone)]
105pub struct Deadline {
106 pub date: String,
107}
108
109#[derive(Debug, Serialize, Deserialize, Clone)]
111pub struct Duration {
112 pub amount: i32,
113 pub unit: String, }
115
116#[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#[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 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#[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#[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 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#[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#[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 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#[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#[derive(Debug, Serialize, Default)]
281pub struct UpdateSectionArgs {
282 pub name: String,
283}
284
285#[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#[derive(Debug, Serialize, Default)]
299pub struct UpdateCommentArgs {
300 pub content: String,
301}
302
303impl UpdateCommentArgs {
304 pub fn has_updates(&self) -> bool {
307 !self.content.is_empty()
308 }
309}
310
311#[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#[derive(Debug, Serialize)]
322pub struct ProjectFilterArgs {
323 pub limit: Option<i32>,
324 pub cursor: Option<String>,
325}
326
327#[derive(Debug, Serialize)]
329pub struct LabelFilterArgs {
330 pub limit: Option<i32>,
331 pub cursor: Option<String>,
332}
333
334#[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#[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#[derive(Debug, Clone)]
353pub enum TodoistError {
354 RateLimited { retry_after: Option<u64>, message: String },
356 AuthenticationError { message: String },
358 AuthorizationError { message: String },
360 NotFound {
362 resource_type: String,
363 resource_id: Option<String>,
364 message: String,
365 },
366 ValidationError { field: Option<String>, message: String },
368 ServerError { status_code: u16, message: String },
370 NetworkError { message: String },
372 ParseError { message: String },
374 EmptyResponse { endpoint: String, message: String },
376 Generic { status_code: Option<u16>, message: String },
378}
379
380impl TodoistError {
381 pub fn is_rate_limited(&self) -> bool {
383 matches!(self, TodoistError::RateLimited { .. })
384 }
385
386 pub fn is_authentication_error(&self) -> bool {
388 matches!(self, TodoistError::AuthenticationError { .. })
389 }
390
391 pub fn is_authorization_error(&self) -> bool {
393 matches!(self, TodoistError::AuthorizationError { .. })
394 }
395
396 pub fn is_not_found(&self) -> bool {
398 matches!(self, TodoistError::NotFound { .. })
399 }
400
401 pub fn is_validation_error(&self) -> bool {
403 matches!(self, TodoistError::ValidationError { .. })
404 }
405
406 pub fn is_server_error(&self) -> bool {
408 matches!(self, TodoistError::ServerError { .. })
409 }
410
411 pub fn is_network_error(&self) -> bool {
413 matches!(self, TodoistError::NetworkError { .. })
414 }
415
416 pub fn is_empty_response(&self) -> bool {
418 matches!(self, TodoistError::EmptyResponse { .. })
419 }
420
421 pub fn retry_after(&self) -> Option<u64> {
423 match self {
424 TodoistError::RateLimited { retry_after, .. } => *retry_after,
425 _ => None,
426 }
427 }
428
429 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
514pub type TodoistResult<T> = Result<T, TodoistError>;
516
517pub 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
525pub 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
533pub 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}