Skip to main content

lib_client_trello/
client.rs

1use reqwest::header::HeaderMap;
2use serde::de::DeserializeOwned;
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://api.trello.com/1";
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    fn build_url(&self, path: &str, extra_params: &[(&str, &str)]) -> String {
67        let auth_params = self.auth.query_params();
68        let mut all_params: Vec<(String, String)> = auth_params
69            .into_iter()
70            .map(|(k, v)| (k.to_string(), v))
71            .collect();
72
73        for (k, v) in extra_params {
74            all_params.push((k.to_string(), v.to_string()));
75        }
76
77        let query: String = all_params
78            .iter()
79            .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
80            .collect::<Vec<_>>()
81            .join("&");
82
83        format!("{}{}?{}", self.base_url, path, query)
84    }
85
86    async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
87        self.request(reqwest::Method::GET, path, &[], None::<&()>)
88            .await
89    }
90
91    async fn post<T: DeserializeOwned, B: serde::Serialize>(
92        &self,
93        path: &str,
94        body: &B,
95    ) -> Result<T> {
96        self.request(reqwest::Method::POST, path, &[], Some(body))
97            .await
98    }
99
100    async fn put<T: DeserializeOwned, B: serde::Serialize>(
101        &self,
102        path: &str,
103        body: &B,
104    ) -> Result<T> {
105        self.request(reqwest::Method::PUT, path, &[], Some(body))
106            .await
107    }
108
109    async fn delete(&self, path: &str) -> Result<()> {
110        let url = self.build_url(path, &[]);
111        debug!("Trello API request: DELETE {}", url);
112
113        let mut headers = HeaderMap::new();
114        self.auth.apply(&mut headers).await?;
115
116        let response = self.http.delete(&url).headers(headers).send().await?;
117        let status = response.status();
118
119        if status.is_success() {
120            Ok(())
121        } else {
122            let status_code = status.as_u16();
123            let body = response.text().await.unwrap_or_default();
124            Err(Error::Api {
125                status: status_code,
126                message: body,
127            })
128        }
129    }
130
131    async fn request<T: DeserializeOwned>(
132        &self,
133        method: reqwest::Method,
134        path: &str,
135        extra_params: &[(&str, &str)],
136        body: Option<&impl serde::Serialize>,
137    ) -> Result<T> {
138        let url = self.build_url(path, extra_params);
139        debug!("Trello API request: {} {}", method, url);
140
141        let mut headers = HeaderMap::new();
142        self.auth.apply(&mut headers).await?;
143
144        let mut request = self.http.request(method, &url).headers(headers);
145
146        if let Some(body) = body {
147            request = request.json(body);
148        }
149
150        let response = request.send().await?;
151        self.handle_response(response).await
152    }
153
154    async fn handle_response<T: DeserializeOwned>(&self, response: reqwest::Response) -> Result<T> {
155        let status = response.status();
156
157        if status.is_success() {
158            let body = response.text().await?;
159            serde_json::from_str(&body).map_err(Error::from)
160        } else {
161            let status_code = status.as_u16();
162            let body = response.text().await.unwrap_or_default();
163            warn!("Trello API error ({}): {}", status_code, body);
164
165            match status_code {
166                401 => Err(Error::Unauthorized),
167                403 => Err(Error::Forbidden(body)),
168                404 => Err(Error::NotFound(body)),
169                429 => Err(Error::RateLimited { retry_after: 60 }),
170                _ => Err(Error::Api {
171                    status: status_code,
172                    message: body,
173                }),
174            }
175        }
176    }
177
178    /// Get a board by ID.
179    pub async fn get_board(&self, id: &str) -> Result<Board> {
180        self.get(&format!("/boards/{}", id)).await
181    }
182
183    /// List boards for the authenticated member.
184    pub async fn list_boards(&self) -> Result<Vec<Board>> {
185        self.get("/members/me/boards").await
186    }
187
188    /// Create a board.
189    pub async fn create_board(&self, input: CreateBoardInput) -> Result<Board> {
190        self.post("/boards", &input).await
191    }
192
193    /// Get a card by ID.
194    pub async fn get_card(&self, id: &str) -> Result<Card> {
195        self.get(&format!("/cards/{}", id)).await
196    }
197
198    /// Create a card.
199    pub async fn create_card(&self, input: CreateCardInput) -> Result<Card> {
200        self.post("/cards", &input).await
201    }
202
203    /// Update a card.
204    pub async fn update_card(&self, id: &str, input: UpdateCardInput) -> Result<Card> {
205        self.put(&format!("/cards/{}", id), &input).await
206    }
207
208    /// Move a card to a different list.
209    pub async fn move_card(&self, id: &str, list_id: &str) -> Result<Card> {
210        self.update_card(id, UpdateCardInput::new().id_list(list_id))
211            .await
212    }
213
214    /// Archive a card.
215    pub async fn archive_card(&self, id: &str) -> Result<Card> {
216        self.update_card(id, UpdateCardInput::new().closed(true))
217            .await
218    }
219
220    /// Delete a card.
221    pub async fn delete_card(&self, id: &str) -> Result<()> {
222        self.delete(&format!("/cards/{}", id)).await
223    }
224
225    /// List lists on a board.
226    pub async fn list_lists(&self, board_id: &str) -> Result<Vec<List>> {
227        self.get(&format!("/boards/{}/lists", board_id)).await
228    }
229
230    /// Create a list.
231    pub async fn create_list(&self, input: CreateListInput) -> Result<List> {
232        self.post("/lists", &input).await
233    }
234
235    /// List cards in a list.
236    pub async fn list_cards(&self, list_id: &str) -> Result<Vec<Card>> {
237        self.get(&format!("/lists/{}/cards", list_id)).await
238    }
239
240    /// List labels on a board.
241    pub async fn list_labels(&self, board_id: &str) -> Result<Vec<Label>> {
242        self.get(&format!("/boards/{}/labels", board_id)).await
243    }
244
245    /// List members of a board.
246    pub async fn list_members(&self, board_id: &str) -> Result<Vec<Member>> {
247        self.get(&format!("/boards/{}/members", board_id)).await
248    }
249
250    /// Get the authenticated member.
251    pub async fn me(&self) -> Result<Member> {
252        self.get("/members/me").await
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use crate::auth::ApiKeyAuth;
260
261    #[test]
262    fn test_builder() {
263        let client = Client::builder()
264            .auth(ApiKeyAuth::new("api-key", "token"))
265            .build();
266        assert_eq!(client.base_url, DEFAULT_BASE_URL);
267    }
268
269    #[test]
270    fn test_build_url() {
271        let client = Client::builder()
272            .auth(ApiKeyAuth::new("my-key", "my-token"))
273            .build();
274
275        let url = client.build_url("/boards/123", &[]);
276        assert!(url.contains("key=my-key"));
277        assert!(url.contains("token=my-token"));
278    }
279}