use reqwest::{Client, Response};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use url::Url;
#[derive(Debug, Error)]
pub enum BacklogError {
#[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error),
#[error("URL parsing error: {0}")]
UrlParse(#[from] url::ParseError),
#[error("JSON parsing error: {0}")]
Json(#[from] serde_json::Error),
#[error("API error: {status} - {message}")]
Api { status: u16, message: String },
#[error("Authentication failed")]
Auth,
#[error("Backlog error")]
Space(#[from] crate::types::error::ErrorResponse),
}
pub type BacklogResult<T> = std::result::Result<T, BacklogError>;
#[derive(Clone)]
pub struct BacklogClient {
api_key: String,
base_url: String,
client: Client,
}
impl BacklogClient {
pub fn new<S: Into<String>>(base_url: S, api_key: S) -> Self {
Self {
api_key: api_key.into(),
base_url: base_url.into(),
client: Client::new(),
}
}
pub fn with_client<S: Into<String>>(base_url: S, api_key: S, client: Client) -> Self {
Self {
api_key: api_key.into(),
base_url: base_url.into(),
client,
}
}
fn build_url(&self, endpoint: &str) -> BacklogResult<Url> {
let base_url = if self.base_url.ends_with('/') {
&self.base_url[..self.base_url.len() - 1]
} else {
&self.base_url
};
let mut url = Url::parse(&format!("{base_url}{endpoint}"))?;
url.query_pairs_mut().append_pair("apiKey", &self.api_key);
Ok(url)
}
pub async fn get(&self, endpoint: &str) -> BacklogResult<Response> {
let url = self.build_url(endpoint)?;
let response = self.client.get(url).send().await?;
self.handle_response(response).await
}
pub async fn post<T: Serialize>(&self, endpoint: &str, body: &T) -> BacklogResult<Response> {
let url = self.build_url(endpoint)?;
let response = self.client.post(url).json(body).send().await?;
self.handle_response(response).await
}
pub async fn put<T: Serialize>(&self, endpoint: &str, body: &T) -> BacklogResult<Response> {
let url = self.build_url(endpoint)?;
let response = self.client.put(url).json(body).send().await?;
self.handle_response(response).await
}
pub async fn patch<T: Serialize>(&self, endpoint: &str, body: &T) -> BacklogResult<Response> {
let url = self.build_url(endpoint)?;
let response = self.client.patch(url).json(body).send().await?;
self.handle_response(response).await
}
pub async fn delete(&self, endpoint: &str) -> BacklogResult<Response> {
let url = self.build_url(endpoint)?;
let response = self.client.delete(url).send().await?;
self.handle_response(response).await
}
async fn handle_response(&self, response: Response) -> BacklogResult<Response> {
let status = response.status();
if status.is_success() {
Ok(response)
} else if status == 401 {
Err(BacklogError::Auth)
} else {
let error_message = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
Err(BacklogError::Api {
status: status.as_u16(),
message: error_message,
})
}
}
pub async fn get_json<T: for<'de> Deserialize<'de>>(&self, endpoint: &str) -> BacklogResult<T> {
let response = self.get(endpoint).await?;
let json = response.json::<T>().await?;
Ok(json)
}
pub async fn post_json<T: Serialize, R: for<'de> Deserialize<'de>>(
&self,
endpoint: &str,
body: &T,
) -> BacklogResult<R> {
let response = self.post(endpoint, body).await?;
let json = response.json::<R>().await?;
Ok(json)
}
}
impl BacklogClient {
pub async fn get_space(&self) -> BacklogResult<crate::types::space::Space> {
self.get_json("/api/v2/space").await
}
}