use reqwest::header::HeaderMap;
use serde::de::DeserializeOwned;
use std::sync::Arc;
use tracing::{debug, warn};
use crate::auth::AuthStrategy;
use crate::error::{Error, Result};
use crate::types::*;
const DEFAULT_BASE_URL: &str = "https://api.trello.com/1";
pub struct ClientBuilder<A> {
auth: A,
base_url: String,
}
impl ClientBuilder<()> {
pub fn new() -> Self {
Self {
auth: (),
base_url: DEFAULT_BASE_URL.to_string(),
}
}
pub fn auth<S: AuthStrategy + 'static>(self, auth: S) -> ClientBuilder<S> {
ClientBuilder {
auth,
base_url: self.base_url,
}
}
}
impl Default for ClientBuilder<()> {
fn default() -> Self {
Self::new()
}
}
impl<A: AuthStrategy + 'static> ClientBuilder<A> {
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
pub fn build(self) -> Client {
Client {
http: reqwest::Client::new(),
auth: Arc::new(self.auth),
base_url: self.base_url,
}
}
}
#[derive(Clone)]
pub struct Client {
http: reqwest::Client,
auth: Arc<dyn AuthStrategy>,
base_url: String,
}
impl Client {
pub fn builder() -> ClientBuilder<()> {
ClientBuilder::new()
}
fn build_url(&self, path: &str, extra_params: &[(&str, &str)]) -> String {
let auth_params = self.auth.query_params();
let mut all_params: Vec<(String, String)> = auth_params
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
for (k, v) in extra_params {
all_params.push((k.to_string(), v.to_string()));
}
let query: String = all_params
.iter()
.map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
.collect::<Vec<_>>()
.join("&");
format!("{}{}?{}", self.base_url, path, query)
}
async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
self.request(reqwest::Method::GET, path, &[], None::<&()>)
.await
}
async fn post<T: DeserializeOwned, B: serde::Serialize>(
&self,
path: &str,
body: &B,
) -> Result<T> {
self.request(reqwest::Method::POST, path, &[], Some(body))
.await
}
async fn put<T: DeserializeOwned, B: serde::Serialize>(
&self,
path: &str,
body: &B,
) -> Result<T> {
self.request(reqwest::Method::PUT, path, &[], Some(body))
.await
}
async fn delete(&self, path: &str) -> Result<()> {
let url = self.build_url(path, &[]);
debug!("Trello API request: DELETE {}", url);
let mut headers = HeaderMap::new();
self.auth.apply(&mut headers).await?;
let response = self.http.delete(&url).headers(headers).send().await?;
let status = response.status();
if status.is_success() {
Ok(())
} else {
let status_code = status.as_u16();
let body = response.text().await.unwrap_or_default();
Err(Error::Api {
status: status_code,
message: body,
})
}
}
async fn request<T: DeserializeOwned>(
&self,
method: reqwest::Method,
path: &str,
extra_params: &[(&str, &str)],
body: Option<&impl serde::Serialize>,
) -> Result<T> {
let url = self.build_url(path, extra_params);
debug!("Trello API request: {} {}", method, url);
let mut headers = HeaderMap::new();
self.auth.apply(&mut headers).await?;
let mut request = self.http.request(method, &url).headers(headers);
if let Some(body) = body {
request = request.json(body);
}
let response = request.send().await?;
self.handle_response(response).await
}
async fn handle_response<T: DeserializeOwned>(&self, response: reqwest::Response) -> Result<T> {
let status = response.status();
if status.is_success() {
let body = response.text().await?;
serde_json::from_str(&body).map_err(Error::from)
} else {
let status_code = status.as_u16();
let body = response.text().await.unwrap_or_default();
warn!("Trello API error ({}): {}", status_code, body);
match status_code {
401 => Err(Error::Unauthorized),
403 => Err(Error::Forbidden(body)),
404 => Err(Error::NotFound(body)),
429 => Err(Error::RateLimited { retry_after: 60 }),
_ => Err(Error::Api {
status: status_code,
message: body,
}),
}
}
}
pub async fn get_board(&self, id: &str) -> Result<Board> {
self.get(&format!("/boards/{}", id)).await
}
pub async fn list_boards(&self) -> Result<Vec<Board>> {
self.get("/members/me/boards").await
}
pub async fn create_board(&self, input: CreateBoardInput) -> Result<Board> {
self.post("/boards", &input).await
}
pub async fn get_card(&self, id: &str) -> Result<Card> {
self.get(&format!("/cards/{}", id)).await
}
pub async fn create_card(&self, input: CreateCardInput) -> Result<Card> {
self.post("/cards", &input).await
}
pub async fn update_card(&self, id: &str, input: UpdateCardInput) -> Result<Card> {
self.put(&format!("/cards/{}", id), &input).await
}
pub async fn move_card(&self, id: &str, list_id: &str) -> Result<Card> {
self.update_card(id, UpdateCardInput::new().id_list(list_id))
.await
}
pub async fn archive_card(&self, id: &str) -> Result<Card> {
self.update_card(id, UpdateCardInput::new().closed(true))
.await
}
pub async fn delete_card(&self, id: &str) -> Result<()> {
self.delete(&format!("/cards/{}", id)).await
}
pub async fn list_lists(&self, board_id: &str) -> Result<Vec<List>> {
self.get(&format!("/boards/{}/lists", board_id)).await
}
pub async fn create_list(&self, input: CreateListInput) -> Result<List> {
self.post("/lists", &input).await
}
pub async fn list_cards(&self, list_id: &str) -> Result<Vec<Card>> {
self.get(&format!("/lists/{}/cards", list_id)).await
}
pub async fn list_labels(&self, board_id: &str) -> Result<Vec<Label>> {
self.get(&format!("/boards/{}/labels", board_id)).await
}
pub async fn list_members(&self, board_id: &str) -> Result<Vec<Member>> {
self.get(&format!("/boards/{}/members", board_id)).await
}
pub async fn me(&self) -> Result<Member> {
self.get("/members/me").await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::ApiKeyAuth;
#[test]
fn test_builder() {
let client = Client::builder()
.auth(ApiKeyAuth::new("api-key", "token"))
.build();
assert_eq!(client.base_url, DEFAULT_BASE_URL);
}
#[test]
fn test_build_url() {
let client = Client::builder()
.auth(ApiKeyAuth::new("my-key", "my-token"))
.build();
let url = client.build_url("/boards/123", &[]);
assert!(url.contains("key=my-key"));
assert!(url.contains("token=my-token"));
}
}