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 pub async fn get_board(&self, id: &str) -> Result<Board> {
180 self.get(&format!("/boards/{}", id)).await
181 }
182
183 pub async fn list_boards(&self) -> Result<Vec<Board>> {
185 self.get("/members/me/boards").await
186 }
187
188 pub async fn create_board(&self, input: CreateBoardInput) -> Result<Board> {
190 self.post("/boards", &input).await
191 }
192
193 pub async fn get_card(&self, id: &str) -> Result<Card> {
195 self.get(&format!("/cards/{}", id)).await
196 }
197
198 pub async fn create_card(&self, input: CreateCardInput) -> Result<Card> {
200 self.post("/cards", &input).await
201 }
202
203 pub async fn update_card(&self, id: &str, input: UpdateCardInput) -> Result<Card> {
205 self.put(&format!("/cards/{}", id), &input).await
206 }
207
208 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 pub async fn archive_card(&self, id: &str) -> Result<Card> {
216 self.update_card(id, UpdateCardInput::new().closed(true))
217 .await
218 }
219
220 pub async fn delete_card(&self, id: &str) -> Result<()> {
222 self.delete(&format!("/cards/{}", id)).await
223 }
224
225 pub async fn list_lists(&self, board_id: &str) -> Result<Vec<List>> {
227 self.get(&format!("/boards/{}/lists", board_id)).await
228 }
229
230 pub async fn create_list(&self, input: CreateListInput) -> Result<List> {
232 self.post("/lists", &input).await
233 }
234
235 pub async fn list_cards(&self, list_id: &str) -> Result<Vec<Card>> {
237 self.get(&format!("/lists/{}/cards", list_id)).await
238 }
239
240 pub async fn list_labels(&self, board_id: &str) -> Result<Vec<Label>> {
242 self.get(&format!("/boards/{}/labels", board_id)).await
243 }
244
245 pub async fn list_members(&self, board_id: &str) -> Result<Vec<Member>> {
247 self.get(&format!("/boards/{}/members", board_id)).await
248 }
249
250 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}