backlog_client/
client.rs

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/// Backlog client with API key authentication
25#[derive(Clone)]
26pub struct BacklogClient {
27    api_key: String,
28    base_url: String,
29    client: Client,
30}
31
32impl BacklogClient {
33    /// Create a new Backlog client with API key authentication
34    ///
35    /// # Arguments
36    ///
37    /// * `base_url` - The base URL of your Backlog instance (e.g., "https://yourspace.backlog.com" or "https://yourspace.backlogtool.com")
38    /// * `api_key` - Your Backlog API key
39    ///
40    /// # Example
41    ///
42    /// ```
43    /// use backlog_client::BacklogClient;
44    ///
45    /// let client = BacklogClient::new("https://yourspace.backlog.com", "your_api_key");
46    /// ```
47    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    /// Create a new Backlog client with custom reqwest client
56    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    /// Build URL with API key parameter
65    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    /// Make a GET request to the Backlog API
78    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    /// Make a POST request to the Backlog API
85    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    /// Make a PUT request to the Backlog API
92    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    /// Make a PATCH request to the Backlog API
99    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    /// Make a DELETE request to the Backlog API
106    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    /// Handle API response and check for errors
113    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    /// Get JSON response from endpoint
132    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    /// Post JSON and get JSON response
139    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    // Space API methods
152
153    /// Get space information
154    pub async fn get_space(&self) -> BacklogResult<crate::types::space::Space> {
155        self.get_json("/api/v2/space").await
156    }
157}