Skip to main content

lib_client_asana/
client.rs

1use reqwest::header::HeaderMap;
2use serde::{de::DeserializeOwned, Deserialize};
3use std::sync::Arc;
4use tracing::{debug, warn};
5
6use crate::auth::AuthStrategy;
7use crate::error::{Error, Result};
8use crate::types::*;
9
10const DEFAULT_BASE_URL: &str = "https://app.asana.com/api/1.0";
11
12pub struct ClientBuilder<A> {
13    auth: A,
14    base_url: String,
15}
16
17impl ClientBuilder<()> {
18    pub fn new() -> Self {
19        Self {
20            auth: (),
21            base_url: DEFAULT_BASE_URL.to_string(),
22        }
23    }
24
25    pub fn auth<S: AuthStrategy + 'static>(self, auth: S) -> ClientBuilder<S> {
26        ClientBuilder {
27            auth,
28            base_url: self.base_url,
29        }
30    }
31}
32
33impl Default for ClientBuilder<()> {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl<A: AuthStrategy + 'static> ClientBuilder<A> {
40    pub fn base_url(mut self, url: impl Into<String>) -> Self {
41        self.base_url = url.into();
42        self
43    }
44
45    pub fn build(self) -> Client {
46        Client {
47            http: reqwest::Client::new(),
48            auth: Arc::new(self.auth),
49            base_url: self.base_url,
50        }
51    }
52}
53
54#[derive(Clone)]
55pub struct Client {
56    http: reqwest::Client,
57    auth: Arc<dyn AuthStrategy>,
58    base_url: String,
59}
60
61impl Client {
62    pub fn builder() -> ClientBuilder<()> {
63        ClientBuilder::new()
64    }
65
66    async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
67        self.request(reqwest::Method::GET, path, None::<&()>).await
68    }
69
70    async fn post<T: DeserializeOwned, B: serde::Serialize>(
71        &self,
72        path: &str,
73        body: &B,
74    ) -> Result<T> {
75        self.request(reqwest::Method::POST, path, Some(body)).await
76    }
77
78    async fn put<T: DeserializeOwned, B: serde::Serialize>(
79        &self,
80        path: &str,
81        body: &B,
82    ) -> Result<T> {
83        self.request(reqwest::Method::PUT, path, Some(body)).await
84    }
85
86    async fn request<T: DeserializeOwned>(
87        &self,
88        method: reqwest::Method,
89        path: &str,
90        body: Option<&impl serde::Serialize>,
91    ) -> Result<T> {
92        let url = format!("{}{}", self.base_url, path);
93        debug!("Asana API request: {} {}", method, url);
94
95        let mut headers = HeaderMap::new();
96        self.auth.apply(&mut headers).await?;
97        headers.insert("Content-Type", "application/json".parse().unwrap());
98
99        let mut request = self.http.request(method, &url).headers(headers);
100
101        if let Some(body) = body {
102            request = request.json(&serde_json::json!({ "data": body }));
103        }
104
105        let response = request.send().await?;
106        self.handle_response(response).await
107    }
108
109    async fn handle_response<T: DeserializeOwned>(&self, response: reqwest::Response) -> Result<T> {
110        let status = response.status();
111
112        if status.is_success() {
113            let body = response.text().await?;
114            let resp: AsanaResponse<T> = serde_json::from_str(&body)?;
115            Ok(resp.data)
116        } else {
117            let status_code = status.as_u16();
118            let body = response.text().await.unwrap_or_default();
119            warn!("Asana API error ({}): {}", status_code, body);
120
121            match status_code {
122                401 => Err(Error::Unauthorized),
123                403 => Err(Error::Forbidden(body)),
124                404 => Err(Error::NotFound(body)),
125                429 => {
126                    let retry_after = 60;
127                    Err(Error::RateLimited { retry_after })
128                }
129                _ => Err(Error::Api {
130                    status: status_code,
131                    message: body,
132                }),
133            }
134        }
135    }
136
137    /// Get a task by GID.
138    pub async fn get_task(&self, gid: &str) -> Result<Task> {
139        self.get(&format!("/tasks/{}", gid)).await
140    }
141
142    /// Create a task.
143    pub async fn create_task(&self, input: CreateTaskInput) -> Result<Task> {
144        self.post("/tasks", &input).await
145    }
146
147    /// Update a task.
148    pub async fn update_task(&self, gid: &str, input: UpdateTaskInput) -> Result<Task> {
149        self.put(&format!("/tasks/{}", gid), &input).await
150    }
151
152    /// Complete a task.
153    pub async fn complete_task(&self, gid: &str) -> Result<Task> {
154        self.update_task(gid, UpdateTaskInput::new().completed(true))
155            .await
156    }
157
158    /// List tasks in a project.
159    pub async fn list_tasks(&self, project_gid: &str) -> Result<Vec<Task>> {
160        #[derive(Deserialize)]
161        struct Response {
162            data: Vec<Task>,
163        }
164
165        let url = format!("{}/projects/{}/tasks?opt_fields=name,notes,completed,due_on,assignee,created_at,modified_at,permalink_url", self.base_url, project_gid);
166
167        let mut headers = HeaderMap::new();
168        self.auth.apply(&mut headers).await?;
169
170        let response = self.http.get(&url).headers(headers).send().await?;
171
172        if response.status().is_success() {
173            let body = response.text().await?;
174            let resp: Response = serde_json::from_str(&body)?;
175            Ok(resp.data)
176        } else {
177            let status = response.status().as_u16();
178            let body = response.text().await.unwrap_or_default();
179            Err(Error::Api { status, message: body })
180        }
181    }
182
183    /// List projects in a workspace.
184    pub async fn list_projects(&self, workspace_gid: &str) -> Result<Vec<Project>> {
185        #[derive(Deserialize)]
186        struct Response {
187            data: Vec<Project>,
188        }
189
190        let url = format!("{}/workspaces/{}/projects", self.base_url, workspace_gid);
191
192        let mut headers = HeaderMap::new();
193        self.auth.apply(&mut headers).await?;
194
195        let response = self.http.get(&url).headers(headers).send().await?;
196
197        if response.status().is_success() {
198            let body = response.text().await?;
199            let resp: Response = serde_json::from_str(&body)?;
200            Ok(resp.data)
201        } else {
202            let status = response.status().as_u16();
203            let body = response.text().await.unwrap_or_default();
204            Err(Error::Api { status, message: body })
205        }
206    }
207
208    /// List workspaces.
209    pub async fn list_workspaces(&self) -> Result<Vec<Workspace>> {
210        #[derive(Deserialize)]
211        struct Response {
212            data: Vec<Workspace>,
213        }
214
215        let url = format!("{}/workspaces", self.base_url);
216
217        let mut headers = HeaderMap::new();
218        self.auth.apply(&mut headers).await?;
219
220        let response = self.http.get(&url).headers(headers).send().await?;
221
222        if response.status().is_success() {
223            let body = response.text().await?;
224            let resp: Response = serde_json::from_str(&body)?;
225            Ok(resp.data)
226        } else {
227            let status = response.status().as_u16();
228            let body = response.text().await.unwrap_or_default();
229            Err(Error::Api { status, message: body })
230        }
231    }
232
233    /// Add task to a project.
234    pub async fn add_task_to_project(&self, task_gid: &str, input: AddToProjectInput) -> Result<()> {
235        let url = format!("{}/tasks/{}/addProject", self.base_url, task_gid);
236
237        let mut headers = HeaderMap::new();
238        self.auth.apply(&mut headers).await?;
239        headers.insert("Content-Type", "application/json".parse().unwrap());
240
241        let response = self
242            .http
243            .post(&url)
244            .headers(headers)
245            .json(&serde_json::json!({ "data": input }))
246            .send()
247            .await?;
248
249        if response.status().is_success() {
250            Ok(())
251        } else {
252            let status = response.status().as_u16();
253            let body = response.text().await.unwrap_or_default();
254            Err(Error::Api { status, message: body })
255        }
256    }
257
258    /// Get the authenticated user.
259    pub async fn me(&self) -> Result<User> {
260        self.get("/users/me").await
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use crate::auth::BearerAuth;
268
269    #[test]
270    fn test_builder() {
271        let client = Client::builder()
272            .auth(BearerAuth::new("test-token"))
273            .build();
274        assert_eq!(client.base_url, DEFAULT_BASE_URL);
275    }
276}