1use reqwest::Client;
2use serde_json::Value;
3
4use crate::models::*;
5
6const TODOIST_API_BASE: &str = "https://api.todoist.com/api/v1";
7
8#[derive(Clone)]
10pub struct TodoistWrapper {
11 client: Client,
12 api_token: String,
13 base_url: String,
14}
15
16impl TodoistWrapper {
17 #[must_use]
19 pub fn new(api_token: String) -> Self {
20 let client = Client::builder()
21 .timeout(std::time::Duration::from_secs(10))
22 .build()
23 .unwrap_or_else(|_| Client::new());
24 Self {
25 client,
26 api_token,
27 base_url: TODOIST_API_BASE.to_string(),
28 }
29 }
30
31 #[doc(hidden)]
33 #[must_use]
34 pub fn with_base_url(api_token: String, base_url: String) -> Self {
35 let client = Client::builder()
36 .timeout(std::time::Duration::from_secs(10))
37 .build()
38 .unwrap_or_else(|_| Client::new());
39 Self {
40 client,
41 api_token,
42 base_url,
43 }
44 }
45
46 async fn make_get_request<T>(&self, endpoint: &str) -> TodoistResult<T>
48 where
49 T: serde::de::DeserializeOwned,
50 {
51 self.make_get_request_with_params(endpoint, &[] as &[(&str, String)])
52 .await
53 }
54
55 async fn make_get_request_with_params<T>(&self, endpoint: &str, query_params: &[(&str, String)]) -> TodoistResult<T>
57 where
58 T: serde::de::DeserializeOwned,
59 {
60 let url = format!(
61 "{}/{}",
62 self.base_url.trim_end_matches('/'),
63 endpoint.trim_start_matches('/')
64 );
65
66 let response = self
67 .client
68 .get(&url)
69 .query(query_params)
70 .bearer_auth(&self.api_token)
71 .send()
72 .await
73 .map_err(|e| TodoistError::NetworkError {
74 message: format!("Failed to send request: {}", e),
75 })?;
76
77 self.handle_response("GET", endpoint, response).await
78 }
79
80 async fn make_post_request<T>(&self, endpoint: &str, body: Option<&Value>) -> TodoistResult<T>
82 where
83 T: serde::de::DeserializeOwned,
84 {
85 let url = format!(
86 "{}/{}",
87 self.base_url.trim_end_matches('/'),
88 endpoint.trim_start_matches('/')
89 );
90 let mut request = self
91 .client
92 .post(&url)
93 .bearer_auth(&self.api_token)
94 .header("Content-Type", "application/json");
95
96 if let Some(body_value) = body {
97 request = request.json(body_value);
98 }
99
100 let response = request.send().await.map_err(|e| TodoistError::NetworkError {
101 message: format!("Failed to send request: {}", e),
102 })?;
103
104 self.handle_response("POST", endpoint, response).await
105 }
106
107 async fn make_delete_request<T>(&self, endpoint: &str) -> TodoistResult<T>
109 where
110 T: serde::de::DeserializeOwned,
111 {
112 let url = format!(
113 "{}/{}",
114 self.base_url.trim_end_matches('/'),
115 endpoint.trim_start_matches('/')
116 );
117 let response = self
118 .client
119 .delete(&url)
120 .bearer_auth(&self.api_token)
121 .send()
122 .await
123 .map_err(|e| TodoistError::NetworkError {
124 message: format!("Failed to send request: {}", e),
125 })?;
126
127 self.handle_response("DELETE", endpoint, response).await
128 }
129
130 async fn make_get_request_paginated<T>(
133 &self,
134 endpoint: &str,
135 query_params: &[(&str, String)],
136 ) -> TodoistResult<PaginatedResponse<T>>
137 where
138 T: serde::de::DeserializeOwned,
139 {
140 let url = format!(
141 "{}/{}",
142 self.base_url.trim_end_matches('/'),
143 endpoint.trim_start_matches('/')
144 );
145
146 let response = self
147 .client
148 .get(&url)
149 .query(query_params)
150 .bearer_auth(&self.api_token)
151 .send()
152 .await
153 .map_err(|e| TodoistError::NetworkError {
154 message: format!("Failed to send request: {}", e),
155 })?;
156
157 self.handle_response("GET", endpoint, response).await
158 }
159
160 async fn handle_response<T>(
162 &self,
163 http_method: &str,
164 endpoint: &str,
165 response: reqwest::Response,
166 ) -> TodoistResult<T>
167 where
168 T: serde::de::DeserializeOwned,
169 {
170 let status = response.status();
171 let headers = response.headers().clone();
172
173 if status.is_success() {
174 let text = response.text().await.map_err(|e| TodoistError::NetworkError {
176 message: format!("Failed to read response body: {}", e),
177 })?;
178
179 if http_method == "DELETE" && text.trim().is_empty() {
181 return serde_json::from_str::<T>("null").map_err(|e| TodoistError::ParseError {
183 message: format!("Failed to deserialize empty DELETE response: {}", e),
184 });
185 }
186
187 if http_method == "POST" && (status.as_u16() == 204 || text.trim().is_empty()) {
189 return serde_json::from_str::<T>("null").map_err(|e| TodoistError::ParseError {
191 message: format!("Failed to deserialize empty POST response: {}", e),
192 });
193 }
194
195 if text.trim().is_empty() {
197 return Err(empty_response_error(endpoint, "API returned empty response body"));
198 }
199
200 serde_json::from_str::<T>(&text).map_err(|e| TodoistError::ParseError {
202 message: format!("Failed to parse response: {}", e),
203 })
204 } else {
205 let error_text = response
207 .text()
208 .await
209 .unwrap_or_else(|_| format!("Unknown error occurred (HTTP {})", status));
210
211 let error = match status.as_u16() {
212 401 => TodoistError::AuthenticationError { message: error_text },
213 403 => TodoistError::AuthorizationError { message: error_text },
214 404 => TodoistError::NotFound {
215 resource_type: "Resource".to_string(),
216 resource_id: None,
217 message: error_text,
218 },
219 429 => {
220 let retry_after = headers
221 .get("Retry-After")
222 .and_then(|v| v.to_str().ok())
223 .and_then(|s| s.parse::<u64>().ok());
224 TodoistError::RateLimited {
225 retry_after,
226 message: error_text,
227 }
228 }
229 400 => TodoistError::ValidationError {
230 field: None,
231 message: error_text,
232 },
233 500..=599 => TodoistError::ServerError {
234 status_code: status.as_u16(),
235 message: error_text,
236 },
237 _ => TodoistError::Generic {
238 status_code: Some(status.as_u16()),
239 message: error_text,
240 },
241 };
242
243 Err(error)
244 }
245 }
246
247 pub async fn get_projects(
251 &self,
252 limit: Option<i32>,
253 cursor: Option<String>,
254 ) -> TodoistResult<PaginatedResponse<Project>> {
255 let mut query_params = Vec::new();
256 if let Some(l) = limit {
257 query_params.push(("limit", l.to_string()));
258 }
259 if let Some(c) = cursor {
260 query_params.push(("cursor", c));
261 }
262 self.make_get_request_paginated("/projects", &query_params).await
263 }
264
265 pub async fn get_projects_filtered(&self, args: &ProjectFilterArgs) -> TodoistResult<Vec<Project>> {
267 let mut query_params = Vec::new();
268
269 if let Some(limit) = args.limit {
270 query_params.push(("limit", limit.to_string()));
271 }
272 if let Some(cursor) = &args.cursor {
273 query_params.push(("cursor", cursor.clone()));
274 }
275
276 self.make_get_request_with_params("/projects", &query_params).await
277 }
278
279 pub async fn get_project(&self, project_id: &str) -> TodoistResult<Project> {
281 self.make_get_request(&format!("/projects/{project_id}")).await
282 }
283
284 pub async fn create_project(&self, args: &CreateProjectArgs) -> TodoistResult<Project> {
286 let body_value = serde_json::to_value(args)?;
287 self.make_post_request("/projects", Some(&body_value)).await
288 }
289
290 pub async fn update_project(&self, project_id: &str, args: &UpdateProjectArgs) -> TodoistResult<Project> {
292 if !args.has_updates() {
293 return Err(TodoistError::ValidationError {
294 field: None,
295 message: "No fields specified for update".to_string(),
296 });
297 }
298 let body_value = serde_json::to_value(args)?;
299 self.make_post_request(&format!("/projects/{project_id}"), Some(&body_value))
300 .await
301 }
302
303 pub async fn delete_project(&self, project_id: &str) -> TodoistResult<()> {
305 self.make_delete_request(&format!("/projects/{project_id}")).await
306 }
307
308 pub async fn get_tasks(
312 &self,
313 limit: Option<i32>,
314 cursor: Option<String>,
315 ) -> TodoistResult<PaginatedResponse<Task>> {
316 let mut query_params = Vec::new();
317 if let Some(l) = limit {
318 query_params.push(("limit", l.to_string()));
319 }
320 if let Some(c) = cursor {
321 query_params.push(("cursor", c));
322 }
323 self.make_get_request_paginated("/tasks", &query_params).await
324 }
325
326 pub async fn get_tasks_for_project(
328 &self,
329 project_id: &str,
330 limit: Option<i32>,
331 cursor: Option<String>,
332 ) -> TodoistResult<PaginatedResponse<Task>> {
333 let mut query_params = vec![("project_id", project_id.to_string())];
334 if let Some(l) = limit {
335 query_params.push(("limit", l.to_string()));
336 }
337 if let Some(c) = cursor {
338 query_params.push(("cursor", c));
339 }
340 self.make_get_request_paginated("/tasks", &query_params).await
341 }
342
343 pub async fn get_task(&self, task_id: &str) -> TodoistResult<Task> {
345 self.make_get_request(&format!("/tasks/{task_id}")).await
346 }
347
348 pub async fn get_tasks_by_filter(&self, args: &TaskFilterArgs) -> TodoistResult<PaginatedResponse<Task>> {
350 let mut query_params = vec![("query", args.query.clone())];
351
352 if let Some(lang) = &args.lang {
353 query_params.push(("lang", lang.clone()));
354 }
355 if let Some(limit) = args.limit {
356 query_params.push(("limit", limit.to_string()));
357 }
358 if let Some(cursor) = &args.cursor {
359 query_params.push(("cursor", cursor.clone()));
360 }
361
362 self.make_get_request_paginated("/tasks", &query_params).await
363 }
364
365 pub async fn create_task(&self, args: &CreateTaskArgs) -> TodoistResult<Task> {
367 let body_value = serde_json::to_value(args)?;
368 self.make_post_request("/tasks", Some(&body_value)).await
369 }
370
371 pub async fn update_task(&self, task_id: &str, args: &UpdateTaskArgs) -> TodoistResult<Task> {
373 if !args.has_updates() {
374 return Err(TodoistError::ValidationError {
375 field: None,
376 message: "No fields specified for update".to_string(),
377 });
378 }
379 let body_value = serde_json::to_value(args)?;
380 self.make_post_request(&format!("/tasks/{task_id}"), Some(&body_value))
381 .await
382 }
383
384 pub async fn complete_task(&self, task_id: &str) -> TodoistResult<()> {
386 self.make_post_request(&format!("/tasks/{task_id}/close"), None).await
387 }
388
389 pub async fn reopen_task(&self, task_id: &str) -> TodoistResult<()> {
391 self.make_post_request(&format!("/tasks/{task_id}/reopen"), None).await
392 }
393
394 pub async fn delete_task(&self, task_id: &str) -> TodoistResult<()> {
396 self.make_delete_request(&format!("/tasks/{task_id}")).await
397 }
398
399 pub async fn get_completed_tasks_by_completion_date(
402 &self,
403 args: &CompletedTasksFilterArgs,
404 ) -> TodoistResult<PaginatedResponse<Task>> {
405 let mut query_params = Vec::new();
406
407 if let Some(since) = &args.since {
408 query_params.push(("since", since.clone()));
409 }
410 if let Some(until) = &args.until {
411 query_params.push(("until", until.clone()));
412 }
413 if let Some(project_id) = &args.project_id {
414 query_params.push(("project_id", project_id.clone()));
415 }
416 if let Some(section_id) = &args.section_id {
417 query_params.push(("section_id", section_id.clone()));
418 }
419 if let Some(limit) = args.limit {
420 query_params.push(("limit", limit.to_string()));
421 }
422 if let Some(cursor) = &args.cursor {
423 query_params.push(("cursor", cursor.clone()));
424 }
425
426 self.make_get_request_paginated("/tasks/completed/by_completion_date", &query_params)
427 .await
428 }
429
430 pub async fn get_completed_tasks_by_due_date(
433 &self,
434 args: &CompletedTasksFilterArgs,
435 ) -> TodoistResult<PaginatedResponse<Task>> {
436 let mut query_params = Vec::new();
437
438 if let Some(since) = &args.since {
439 query_params.push(("since", since.clone()));
440 }
441 if let Some(until) = &args.until {
442 query_params.push(("until", until.clone()));
443 }
444 if let Some(project_id) = &args.project_id {
445 query_params.push(("project_id", project_id.clone()));
446 }
447 if let Some(section_id) = &args.section_id {
448 query_params.push(("section_id", section_id.clone()));
449 }
450 if let Some(limit) = args.limit {
451 query_params.push(("limit", limit.to_string()));
452 }
453 if let Some(cursor) = &args.cursor {
454 query_params.push(("cursor", cursor.clone()));
455 }
456
457 self.make_get_request_paginated("/tasks/completed/by_due_date", &query_params)
458 .await
459 }
460
461 pub async fn get_labels(
465 &self,
466 limit: Option<i32>,
467 cursor: Option<String>,
468 ) -> TodoistResult<PaginatedResponse<Label>> {
469 let mut query_params = Vec::new();
470 if let Some(l) = limit {
471 query_params.push(("limit", l.to_string()));
472 }
473 if let Some(c) = cursor {
474 query_params.push(("cursor", c));
475 }
476 self.make_get_request_paginated("/labels", &query_params).await
477 }
478
479 pub async fn get_labels_filtered(&self, args: &LabelFilterArgs) -> TodoistResult<Vec<Label>> {
481 let mut query_params = Vec::new();
482
483 if let Some(limit) = args.limit {
484 query_params.push(("limit", limit.to_string()));
485 }
486 if let Some(cursor) = &args.cursor {
487 query_params.push(("cursor", cursor.clone()));
488 }
489
490 self.make_get_request_with_params("/labels", &query_params).await
491 }
492
493 pub async fn get_label(&self, label_id: &str) -> TodoistResult<Label> {
495 self.make_get_request(&format!("/labels/{label_id}")).await
496 }
497
498 pub async fn create_label(&self, args: &CreateLabelArgs) -> TodoistResult<Label> {
500 let body_value = serde_json::to_value(args)?;
501 self.make_post_request("/labels", Some(&body_value)).await
502 }
503
504 pub async fn update_label(&self, label_id: &str, args: &UpdateLabelArgs) -> TodoistResult<Label> {
506 if !args.has_updates() {
507 return Err(TodoistError::ValidationError {
508 field: None,
509 message: "No fields specified for update".to_string(),
510 });
511 }
512 let body_value = serde_json::to_value(args)?;
513 self.make_post_request(&format!("/labels/{label_id}"), Some(&body_value))
514 .await
515 }
516
517 pub async fn delete_label(&self, label_id: &str) -> TodoistResult<()> {
519 self.make_delete_request(&format!("/labels/{label_id}")).await
520 }
521
522 pub async fn get_sections(
526 &self,
527 limit: Option<i32>,
528 cursor: Option<String>,
529 ) -> TodoistResult<PaginatedResponse<Section>> {
530 let mut query_params = Vec::new();
531 if let Some(l) = limit {
532 query_params.push(("limit", l.to_string()));
533 }
534 if let Some(c) = cursor {
535 query_params.push(("cursor", c));
536 }
537 self.make_get_request_paginated("/sections", &query_params).await
538 }
539
540 pub async fn get_sections_filtered(&self, args: &SectionFilterArgs) -> TodoistResult<PaginatedResponse<Section>> {
542 let mut query_params = Vec::new();
543
544 if let Some(project_id) = &args.project_id {
545 query_params.push(("project_id", project_id.clone()));
546 }
547 if let Some(limit) = args.limit {
548 query_params.push(("limit", limit.to_string()));
549 }
550 if let Some(cursor) = &args.cursor {
551 query_params.push(("cursor", cursor.clone()));
552 }
553
554 self.make_get_request_paginated("/sections", &query_params).await
555 }
556
557 pub async fn get_section(&self, section_id: &str) -> TodoistResult<Section> {
559 self.make_get_request(&format!("/sections/{section_id}")).await
560 }
561
562 pub async fn create_section(&self, args: &CreateSectionArgs) -> TodoistResult<Section> {
564 let body_value = serde_json::to_value(args)?;
565 self.make_post_request("/sections", Some(&body_value)).await
566 }
567
568 pub async fn update_section(&self, section_id: &str, args: &UpdateSectionArgs) -> TodoistResult<Section> {
570 let body_value = serde_json::to_value(args)?;
571 self.make_post_request(&format!("/sections/{section_id}"), Some(&body_value))
572 .await
573 }
574
575 pub async fn delete_section(&self, section_id: &str) -> TodoistResult<()> {
577 self.make_delete_request(&format!("/sections/{section_id}")).await
578 }
579
580 pub async fn get_comments(&self) -> TodoistResult<Vec<Comment>> {
584 self.make_get_request("/comments").await
585 }
586
587 pub async fn get_comments_filtered(&self, args: &CommentFilterArgs) -> TodoistResult<Vec<Comment>> {
589 let mut query_params = Vec::new();
590
591 if let Some(task_id) = &args.task_id {
592 query_params.push(("task_id", task_id.clone()));
593 }
594 if let Some(project_id) = &args.project_id {
595 query_params.push(("project_id", project_id.clone()));
596 }
597 if let Some(limit) = args.limit {
598 query_params.push(("limit", limit.to_string()));
599 }
600 if let Some(cursor) = &args.cursor {
601 query_params.push(("cursor", cursor.clone()));
602 }
603
604 self.make_get_request_with_params("/comments", &query_params).await
605 }
606
607 pub async fn get_comment(&self, comment_id: &str) -> TodoistResult<Comment> {
609 self.make_get_request(&format!("/comments/{comment_id}")).await
610 }
611
612 pub async fn create_comment(&self, args: &CreateCommentArgs) -> TodoistResult<Comment> {
614 let body_value = serde_json::to_value(args)?;
615 self.make_post_request("/comments", Some(&body_value)).await
616 }
617
618 pub async fn update_comment(&self, comment_id: &str, args: &UpdateCommentArgs) -> TodoistResult<Comment> {
620 if !args.has_updates() {
621 return Err(TodoistError::ValidationError {
622 field: None,
623 message: "No fields specified for update".to_string(),
624 });
625 }
626 let body_value = serde_json::to_value(args)?;
627 self.make_post_request(&format!("/comments/{comment_id}"), Some(&body_value))
628 .await
629 }
630
631 pub async fn delete_comment(&self, comment_id: &str) -> TodoistResult<()> {
633 self.make_delete_request(&format!("/comments/{comment_id}")).await
634 }
635}