1use reqwest::{Client, Response};
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4use url::Url;
5
6#[derive(Debug, Error)]
7pub enum BacklogError {
8 #[error("HTTP request failed: {0}")]
9 Http(#[from] reqwest::Error),
10 #[error("URL parsing error: {0}")]
11 UrlParse(#[from] url::ParseError),
12 #[error("JSON parsing error: {0}")]
13 Json(#[from] serde_json::Error),
14 #[error("API error: {status} - {message}")]
15 Api { status: u16, message: String },
16 #[error("Authentication failed")]
17 Auth,
18 #[error("Backlog error")]
19 Space(#[from] crate::types::error::ErrorResponse),
20}
21
22pub type BacklogResult<T> = std::result::Result<T, BacklogError>;
23
24#[derive(Clone)]
26pub struct BacklogClient {
27 api_key: String,
28 base_url: String,
29 client: Client,
30}
31
32impl BacklogClient {
33 pub fn new<S: Into<String>>(base_url: S, api_key: S) -> Self {
48 Self {
49 api_key: api_key.into(),
50 base_url: base_url.into(),
51 client: Client::new(),
52 }
53 }
54
55 pub fn with_client<S: Into<String>>(base_url: S, api_key: S, client: Client) -> Self {
57 Self {
58 api_key: api_key.into(),
59 base_url: base_url.into(),
60 client,
61 }
62 }
63
64 fn build_url(&self, endpoint: &str) -> BacklogResult<Url> {
66 let base_url = if self.base_url.ends_with('/') {
67 &self.base_url[..self.base_url.len() - 1]
68 } else {
69 &self.base_url
70 };
71
72 let mut url = Url::parse(&format!("{base_url}{endpoint}"))?;
73 url.query_pairs_mut().append_pair("apiKey", &self.api_key);
74 Ok(url)
75 }
76
77 pub async fn get(&self, endpoint: &str) -> BacklogResult<Response> {
79 let url = self.build_url(endpoint)?;
80 let response = self.client.get(url).send().await?;
81 self.handle_response(response).await
82 }
83
84 pub async fn post<T: Serialize>(&self, endpoint: &str, body: &T) -> BacklogResult<Response> {
86 let url = self.build_url(endpoint)?;
87 let response = self.client.post(url).json(body).send().await?;
88 self.handle_response(response).await
89 }
90
91 pub async fn put<T: Serialize>(&self, endpoint: &str, body: &T) -> BacklogResult<Response> {
93 let url = self.build_url(endpoint)?;
94 let response = self.client.put(url).json(body).send().await?;
95 self.handle_response(response).await
96 }
97
98 pub async fn patch<T: Serialize>(&self, endpoint: &str, body: &T) -> BacklogResult<Response> {
100 let url = self.build_url(endpoint)?;
101 let response = self.client.patch(url).json(body).send().await?;
102 self.handle_response(response).await
103 }
104
105 pub async fn delete(&self, endpoint: &str) -> BacklogResult<Response> {
107 let url = self.build_url(endpoint)?;
108 let response = self.client.delete(url).send().await?;
109 self.handle_response(response).await
110 }
111
112 async fn handle_response(&self, response: Response) -> BacklogResult<Response> {
114 let status = response.status();
115 if status.is_success() {
116 Ok(response)
117 } else if status == 401 {
118 Err(BacklogError::Auth)
119 } else {
120 let error_message = response
121 .text()
122 .await
123 .unwrap_or_else(|_| "Unknown error".to_string());
124 Err(BacklogError::Api {
125 status: status.as_u16(),
126 message: error_message,
127 })
128 }
129 }
130
131 pub async fn get_json<T: for<'de> Deserialize<'de>>(&self, endpoint: &str) -> BacklogResult<T> {
133 let response = self.get(endpoint).await?;
134 let json = response.json::<T>().await?;
135 Ok(json)
136 }
137
138 pub async fn post_json<T: Serialize, R: for<'de> Deserialize<'de>>(
140 &self,
141 endpoint: &str,
142 body: &T,
143 ) -> BacklogResult<R> {
144 let response = self.post(endpoint, body).await?;
145 let json = response.json::<R>().await?;
146 Ok(json)
147 }
148}
149
150impl BacklogClient {
151 pub async fn get_space(&self) -> BacklogResult<crate::types::space::Space> {
155 self.get_json("/api/v2/space").await
156 }
157}