1use reqwest::Client;
2use serde_json::Value;
3
4use crate::models::*;
5
6const TODOIST_API_BASE: &str = "https://api.todoist.com/rest/v2";
7
8#[derive(Clone)]
10pub struct TodoistWrapper {
11 client: Client,
12 api_token: String,
13}
14
15impl TodoistWrapper {
16 #[must_use]
18 pub fn new(api_token: String) -> Self {
19 let client = Client::builder()
20 .timeout(std::time::Duration::from_secs(10))
21 .build()
22 .unwrap_or_else(|_| Client::new());
23 Self { client, api_token }
24 }
25
26 async fn make_get_request<T>(&self, endpoint: &str) -> TodoistResult<T>
28 where
29 T: serde::de::DeserializeOwned,
30 {
31 self.make_get_request_with_params(endpoint, &[] as &[(&str, String)])
32 .await
33 }
34
35 async fn make_get_request_with_params<T>(&self, endpoint: &str, query_params: &[(&str, String)]) -> TodoistResult<T>
37 where
38 T: serde::de::DeserializeOwned,
39 {
40 let url = format!("{TODOIST_API_BASE}{endpoint}");
41
42 let response = self
43 .client
44 .get(&url)
45 .query(query_params)
46 .bearer_auth(&self.api_token)
47 .send()
48 .await
49 .map_err(|e| TodoistError::NetworkError {
50 message: format!("Failed to send request: {}", e),
51 })?;
52
53 self.handle_response("GET", endpoint, response).await
54 }
55
56 async fn make_post_request<T>(&self, endpoint: &str, body: Option<&Value>) -> TodoistResult<T>
58 where
59 T: serde::de::DeserializeOwned,
60 {
61 let url = format!("{TODOIST_API_BASE}{endpoint}");
62 let mut request = self
63 .client
64 .post(&url)
65 .bearer_auth(&self.api_token)
66 .header("Content-Type", "application/json");
67
68 if let Some(body_value) = body {
69 request = request.json(body_value);
70 }
71
72 let response = request.send().await.map_err(|e| TodoistError::NetworkError {
73 message: format!("Failed to send request: {}", e),
74 })?;
75
76 self.handle_response("POST", endpoint, response).await
77 }
78
79 async fn make_delete_request<T>(&self, endpoint: &str) -> TodoistResult<T>
81 where
82 T: serde::de::DeserializeOwned,
83 {
84 let url = format!("{TODOIST_API_BASE}{endpoint}");
85 let response = self
86 .client
87 .delete(&url)
88 .bearer_auth(&self.api_token)
89 .send()
90 .await
91 .map_err(|e| TodoistError::NetworkError {
92 message: format!("Failed to send request: {}", e),
93 })?;
94
95 self.handle_response("DELETE", endpoint, response).await
96 }
97
98 async fn handle_response<T>(
100 &self,
101 http_method: &str,
102 endpoint: &str,
103 response: reqwest::Response,
104 ) -> TodoistResult<T>
105 where
106 T: serde::de::DeserializeOwned,
107 {
108 let status = response.status();
109 let headers = response.headers().clone();
110
111 if status.is_success() {
112 let text = response.text().await.map_err(|e| TodoistError::NetworkError {
114 message: format!("Failed to read response body: {}", e),
115 })?;
116
117 if http_method == "DELETE" && text.trim().is_empty() {
119 return serde_json::from_str::<T>("null").map_err(|e| TodoistError::ParseError {
121 message: format!("Failed to deserialize empty DELETE response: {}", e),
122 });
123 }
124
125 if http_method == "POST" && (status.as_u16() == 204 || text.trim().is_empty()) {
127 return serde_json::from_str::<T>("null").map_err(|e| TodoistError::ParseError {
129 message: format!("Failed to deserialize empty POST response: {}", e),
130 });
131 }
132
133 if text.trim().is_empty() {
135 return Err(empty_response_error(endpoint, "API returned empty response body"));
136 }
137
138 serde_json::from_str::<T>(&text).map_err(|e| TodoistError::ParseError {
140 message: format!("Failed to parse response: {}", e),
141 })
142 } else {
143 let error_text = response
145 .text()
146 .await
147 .unwrap_or_else(|_| format!("Unknown error occurred (HTTP {})", status));
148
149 let error = match status.as_u16() {
150 401 => TodoistError::AuthenticationError { message: error_text },
151 403 => TodoistError::AuthorizationError { message: error_text },
152 404 => TodoistError::NotFound {
153 resource_type: "Resource".to_string(),
154 resource_id: None,
155 message: error_text,
156 },
157 429 => {
158 let retry_after = headers
159 .get("Retry-After")
160 .and_then(|v| v.to_str().ok())
161 .and_then(|s| s.parse::<u64>().ok());
162 TodoistError::RateLimited {
163 retry_after,
164 message: error_text,
165 }
166 }
167 400 => TodoistError::ValidationError {
168 field: None,
169 message: error_text,
170 },
171 500..=599 => TodoistError::ServerError {
172 status_code: status.as_u16(),
173 message: error_text,
174 },
175 _ => TodoistError::Generic {
176 status_code: Some(status.as_u16()),
177 message: error_text,
178 },
179 };
180
181 Err(error)
182 }
183 }
184
185 pub async fn get_projects(&self) -> TodoistResult<Vec<Project>> {
189 self.make_get_request("/projects").await
190 }
191
192 pub async fn get_projects_filtered(&self, args: &ProjectFilterArgs) -> TodoistResult<Vec<Project>> {
194 let mut query_params = Vec::new();
195
196 if let Some(limit) = args.limit {
197 query_params.push(("limit", limit.to_string()));
198 }
199 if let Some(cursor) = &args.cursor {
200 query_params.push(("cursor", cursor.clone()));
201 }
202
203 self.make_get_request_with_params("/projects", &query_params).await
204 }
205
206 pub async fn get_project(&self, project_id: &str) -> TodoistResult<Project> {
208 self.make_get_request(&format!("/projects/{project_id}")).await
209 }
210
211 pub async fn create_project(&self, args: &CreateProjectArgs) -> TodoistResult<Project> {
213 let body_value = serde_json::to_value(args)?;
214 self.make_post_request("/projects", Some(&body_value)).await
215 }
216
217 pub async fn update_project(&self, project_id: &str, args: &UpdateProjectArgs) -> TodoistResult<Project> {
219 if !args.has_updates() {
220 return Err(TodoistError::ValidationError {
221 field: None,
222 message: "No fields specified for update".to_string(),
223 });
224 }
225 let body_value = serde_json::to_value(args)?;
226 self.make_post_request(&format!("/projects/{project_id}"), Some(&body_value))
227 .await
228 }
229
230 pub async fn delete_project(&self, project_id: &str) -> TodoistResult<()> {
232 self.make_delete_request(&format!("/projects/{project_id}")).await
233 }
234
235 pub async fn get_tasks(&self) -> TodoistResult<Vec<Task>> {
239 self.make_get_request("/tasks").await
240 }
241
242 pub async fn get_tasks_for_project(&self, project_id: &str) -> TodoistResult<Vec<Task>> {
244 let query_params = vec![("project_id", project_id.to_string())];
245 self.make_get_request_with_params("/tasks", &query_params).await
246 }
247
248 pub async fn get_task(&self, task_id: &str) -> TodoistResult<Task> {
250 self.make_get_request(&format!("/tasks/{task_id}")).await
251 }
252
253 pub async fn get_tasks_by_filter(&self, args: &TaskFilterArgs) -> TodoistResult<Vec<Task>> {
255 let mut query_params = vec![("query", args.query.clone())];
256
257 if let Some(lang) = &args.lang {
258 query_params.push(("lang", lang.clone()));
259 }
260 if let Some(limit) = args.limit {
261 query_params.push(("limit", limit.to_string()));
262 }
263 if let Some(cursor) = &args.cursor {
264 query_params.push(("cursor", cursor.clone()));
265 }
266
267 self.make_get_request_with_params("/tasks", &query_params).await
268 }
269
270 pub async fn create_task(&self, args: &CreateTaskArgs) -> TodoistResult<Task> {
272 let body_value = serde_json::to_value(args)?;
273 self.make_post_request("/tasks", Some(&body_value)).await
274 }
275
276 pub async fn update_task(&self, task_id: &str, args: &UpdateTaskArgs) -> TodoistResult<Task> {
278 if !args.has_updates() {
279 return Err(TodoistError::ValidationError {
280 field: None,
281 message: "No fields specified for update".to_string(),
282 });
283 }
284 let body_value = serde_json::to_value(args)?;
285 self.make_post_request(&format!("/tasks/{task_id}"), Some(&body_value))
286 .await
287 }
288
289 pub async fn complete_task(&self, task_id: &str) -> TodoistResult<()> {
291 self.make_post_request(&format!("/tasks/{task_id}/close"), None).await
292 }
293
294 pub async fn reopen_task(&self, task_id: &str) -> TodoistResult<()> {
296 self.make_post_request(&format!("/tasks/{task_id}/reopen"), None).await
297 }
298
299 pub async fn delete_task(&self, task_id: &str) -> TodoistResult<()> {
301 self.make_delete_request(&format!("/tasks/{task_id}")).await
302 }
303
304 pub async fn get_labels(&self) -> TodoistResult<Vec<Label>> {
308 self.make_get_request("/labels").await
309 }
310
311 pub async fn get_labels_filtered(&self, args: &LabelFilterArgs) -> TodoistResult<Vec<Label>> {
313 let mut query_params = Vec::new();
314
315 if let Some(limit) = args.limit {
316 query_params.push(("limit", limit.to_string()));
317 }
318 if let Some(cursor) = &args.cursor {
319 query_params.push(("cursor", cursor.clone()));
320 }
321
322 self.make_get_request_with_params("/labels", &query_params).await
323 }
324
325 pub async fn get_label(&self, label_id: &str) -> TodoistResult<Label> {
327 self.make_get_request(&format!("/labels/{label_id}")).await
328 }
329
330 pub async fn create_label(&self, args: &CreateLabelArgs) -> TodoistResult<Label> {
332 let body_value = serde_json::to_value(args)?;
333 self.make_post_request("/labels", Some(&body_value)).await
334 }
335
336 pub async fn update_label(&self, label_id: &str, args: &UpdateLabelArgs) -> TodoistResult<Label> {
338 if !args.has_updates() {
339 return Err(TodoistError::ValidationError {
340 field: None,
341 message: "No fields specified for update".to_string(),
342 });
343 }
344 let body_value = serde_json::to_value(args)?;
345 self.make_post_request(&format!("/labels/{label_id}"), Some(&body_value))
346 .await
347 }
348
349 pub async fn delete_label(&self, label_id: &str) -> TodoistResult<()> {
351 self.make_delete_request(&format!("/labels/{label_id}")).await
352 }
353
354 pub async fn get_sections(&self) -> TodoistResult<Vec<Section>> {
358 self.make_get_request("/sections").await
359 }
360
361 pub async fn get_sections_filtered(&self, args: &SectionFilterArgs) -> TodoistResult<Vec<Section>> {
363 let mut query_params = Vec::new();
364
365 if let Some(project_id) = &args.project_id {
366 query_params.push(("project_id", project_id.clone()));
367 }
368 if let Some(limit) = args.limit {
369 query_params.push(("limit", limit.to_string()));
370 }
371 if let Some(cursor) = &args.cursor {
372 query_params.push(("cursor", cursor.clone()));
373 }
374
375 self.make_get_request_with_params("/sections", &query_params).await
376 }
377
378 pub async fn get_section(&self, section_id: &str) -> TodoistResult<Section> {
380 self.make_get_request(&format!("/sections/{section_id}")).await
381 }
382
383 pub async fn create_section(&self, args: &CreateSectionArgs) -> TodoistResult<Section> {
385 let body_value = serde_json::to_value(args)?;
386 self.make_post_request("/sections", Some(&body_value)).await
387 }
388
389 pub async fn update_section(&self, section_id: &str, args: &UpdateSectionArgs) -> TodoistResult<Section> {
391 let body_value = serde_json::to_value(args)?;
392 self.make_post_request(&format!("/sections/{section_id}"), Some(&body_value))
393 .await
394 }
395
396 pub async fn delete_section(&self, section_id: &str) -> TodoistResult<()> {
398 self.make_delete_request(&format!("/sections/{section_id}")).await
399 }
400
401 pub async fn get_comments(&self) -> TodoistResult<Vec<Comment>> {
405 self.make_get_request("/comments").await
406 }
407
408 pub async fn get_comments_filtered(&self, args: &CommentFilterArgs) -> TodoistResult<Vec<Comment>> {
410 let mut query_params = Vec::new();
411
412 if let Some(task_id) = &args.task_id {
413 query_params.push(("task_id", task_id.clone()));
414 }
415 if let Some(project_id) = &args.project_id {
416 query_params.push(("project_id", project_id.clone()));
417 }
418 if let Some(limit) = args.limit {
419 query_params.push(("limit", limit.to_string()));
420 }
421 if let Some(cursor) = &args.cursor {
422 query_params.push(("cursor", cursor.clone()));
423 }
424
425 self.make_get_request_with_params("/comments", &query_params).await
426 }
427
428 pub async fn get_comment(&self, comment_id: &str) -> TodoistResult<Comment> {
430 self.make_get_request(&format!("/comments/{comment_id}")).await
431 }
432
433 pub async fn create_comment(&self, args: &CreateCommentArgs) -> TodoistResult<Comment> {
435 let body_value = serde_json::to_value(args)?;
436 self.make_post_request("/comments", Some(&body_value)).await
437 }
438
439 pub async fn update_comment(&self, comment_id: &str, args: &UpdateCommentArgs) -> TodoistResult<Comment> {
441 if !args.has_updates() {
442 return Err(TodoistError::ValidationError {
443 field: None,
444 message: "No fields specified for update".to_string(),
445 });
446 }
447 let body_value = serde_json::to_value(args)?;
448 self.make_post_request(&format!("/comments/{comment_id}"), Some(&body_value))
449 .await
450 }
451
452 pub async fn delete_comment(&self, comment_id: &str) -> TodoistResult<()> {
454 self.make_delete_request(&format!("/comments/{comment_id}")).await
455 }
456}