bitbucket_server_rs/
client.rs

1//! # Bitbucket Server REST API Client
2//!
3//! This module provides the core client functionality for interacting with the Bitbucket Server REST API.
4//! It includes the HTTP client, request/response handling, error types, and utility functions.
5
6use crate::api;
7use crate::Error;
8use api::Api;
9use reqwest::{RequestBuilder, Response, StatusCode};
10use serde::de::DeserializeOwned;
11use std::collections::HashMap;
12use std::future::Future;
13
14/// Configuration for the Bitbucket Server API HTTP client.
15///
16/// This struct holds all the necessary configuration for making API requests to a Bitbucket Server instance.
17#[derive(Clone, Default, Debug)]
18pub struct Client {
19    /// Base URL for the bitbucket server. It must end with `/rest`.
20    pub base_path: String,
21
22    /// The HTTP client to use for making requests.
23    pub http_client: reqwest::Client,
24
25    /// The API token to use for authentication.
26    pub api_token: String,
27}
28
29/// The Bitbucket API client implementation.
30impl Client {
31    /// Access Bitbucket's `api` API endpoints.
32    ///
33    /// This method returns an `Api` struct that provides access to all the API endpoints
34    /// under the `/rest/api` path.
35    ///
36    /// # Returns
37    ///
38    /// An `Api` struct that can be used to access API endpoints.
39    pub fn api(self) -> Api {
40        Api { client: self }
41    }
42
43    // TODO add other APIs here as needed e.g. /default-reviewers, etc
44}
45
46/// Create a new Bitbucket API client.
47///
48/// This function creates a new client with the specified base path and API token.
49///
50/// # Arguments
51///
52/// * `base_path` - The base URL for the Bitbucket server. It must end with `/rest`.
53/// * `api_token` - The API token to use for authentication.
54///
55/// # Returns
56///
57/// A new Bitbucket API client.
58///
59/// # Examples
60///
61/// Basic example of creating a new client and calling an API:
62///
63/// ```no_run
64/// use bitbucket_server_rs::Error;
65/// use bitbucket_server_rs::client::{new, ApiRequest, ApiResponse};
66/// use bitbucket_server_rs::api::build_status_get::BuildStatus;
67///
68/// async fn example() -> ApiResponse<BuildStatus> {
69///     let client = new(
70///         "https://bitbucket-server/rest",
71///         "API_TOKEN"
72///     );
73///
74///     client
75///        .api()
76///        .build_status_get(
77///            "PROJECT_KEY",
78///            "COMMIT_ID",
79///            "REPOSITORY_SLUG",
80///        )
81///        .key("ABC123")
82///        .build()
83///        .unwrap()
84///        .send()
85///        .await
86/// }
87/// ```
88pub fn new(base_path: &str, api_token: &str) -> Client {
89    Client {
90        base_path: base_path.to_string(),
91        http_client: reqwest::Client::new(),
92        api_token: api_token.to_string(),
93    }
94}
95
96/// HTTP request and response handling implementations for the Bitbucket API client.
97impl Client {
98    /// Create a request builder with authentication headers.
99    ///
100    /// This method adds the necessary authentication and content type headers to a request.
101    ///
102    /// # Arguments
103    ///
104    /// * `req` - The request builder to add headers to.
105    ///
106    /// # Returns
107    ///
108    /// A request builder with the headers added.
109    pub async fn builder(&self, req: RequestBuilder) -> RequestBuilder {
110        req.header("Authorization", format!("Bearer {}", self.api_token))
111            .header("Content-Type", "application/json")
112    }
113
114    /// Set a custom HTTP client with specific configuration.
115    ///
116    /// This method allows you to use a custom HTTP client with specific configuration
117    /// options, such as timeouts, proxies, etc.
118    ///
119    /// # Arguments
120    ///
121    /// * `http_client` - The custom HTTP client to use.
122    ///
123    /// # Example
124    ///
125    /// ```no_run
126    /// use bitbucket_server_rs::client::new;
127    /// use reqwest::ClientBuilder;
128    /// use std::time::Duration;
129    ///
130    /// let mut client = new("https://bitbucket-server/rest", "API_TOKEN");
131    ///
132    /// // Create a custom HTTP client with a 30-second timeout
133    /// let http_client = ClientBuilder::new()
134    ///     .timeout(Duration::from_secs(30))
135    ///     .build()
136    ///     .expect("Failed to build HTTP client");
137    ///
138    /// // Set the custom HTTP client
139    /// client.with_http_client(http_client);
140    /// ```
141    pub fn with_http_client(&mut self, http_client: reqwest::Client) {
142        self.http_client = http_client;
143    }
144
145    /// Send a GET request to the Bitbucket Server API.
146    ///
147    /// This method sends a GET request to the specified URI with the given query parameters.
148    ///
149    /// # Arguments
150    ///
151    /// * `uri` - The URI to send the request to, relative to the base path.
152    /// * `params` - Optional query parameters to include in the request.
153    ///
154    /// # Returns
155    ///
156    /// A Result containing either the response data or an error.
157    pub async fn get<T: ApiRequest>(
158        &self,
159        uri: &str,
160        params: Option<HashMap<String, String>>,
161    ) -> ApiResponse<T::Output> {
162        let uri = format!("{}/{}", self.base_path, uri);
163        let get = self.http_client.get(uri).query(&params);
164
165        let req = self
166            .builder(get)
167            .await
168            .build()
169            .expect("Failed to build request");
170
171        let response = self.http_client.execute(req).await.map_err(|e| {
172            Error::RequestError(format!("Error sending request: {:?}", e))
173        })?;
174
175        Self::process_response::<T>(response).await
176    }
177
178    /// Send a POST request to the Bitbucket Server API.
179    ///
180    /// This method sends a POST request to the specified URI with the given body.
181    ///
182    /// # Arguments
183    ///
184    /// * `uri` - The URI to send the request to, relative to the base path.
185    /// * `body` - The body to include in the request.
186    ///
187    /// # Returns
188    ///
189    /// A Result containing either the response data or an error.
190    pub async fn post<T: ApiRequest>(
191        &self,
192        uri: &str,
193        body: &str,
194    ) -> ApiResponse<<T as ApiRequest>::Output> {
195        let uri = format!("{}/{}", self.base_path, uri);
196        let post = self.http_client.post(uri).body(body.to_string());
197
198        let req = self
199            .builder(post)
200            .await
201            .build()
202            .expect("Failed to build request");
203
204        let response = self.http_client.execute(req).await.map_err(|e| {
205            Error::RequestError(format!("Error sending request: {:?}", e))
206        })?;
207
208        Self::process_response::<T>(response).await
209    }
210
211    /// Process the response from the Bitbucket Server API.
212    ///
213    /// This method processes the response from the API, handling different status codes
214    /// and converting the response body to the expected output type.
215    ///
216    /// # Arguments
217    ///
218    /// * `response` - The response from the API.
219    ///
220    /// # Returns
221    ///
222    /// A Result containing either the response data or an error.
223    async fn process_response<T: ApiRequest>(
224        response: Response,
225    ) -> ApiResponse<<T as ApiRequest>::Output> {
226        match response.status() {
227            status if status.is_success() => {
228                let json = response.text().await.map_err(|e| {
229                    Error::ResponseError(format!("Error reading response: {e:#?}"))
230                })?;
231
232                Self::make_api_response::<T>(json.as_str())
233            }
234            status if status == StatusCode::UNAUTHORIZED || status == StatusCode::FORBIDDEN => {
235                Err(Error::Unauthorized)
236            }
237            status if status.is_client_error() => Err(Error::ResponseError(format!(
238                "HTTP Client error [{}]: {}",
239                status.as_u16(),
240                response.text().await.unwrap_or_default(),
241            ))),
242            status if status.is_server_error() => Err(Error::ResponseError(format!(
243                "HTTP Server error [{}]: {}",
244                status.as_u16(),
245                response.text().await.unwrap_or_default(),
246            ))),
247            _ => Err(Error::Unexpected(format!(
248                "Unexpected HTTP Response [{}]: {}",
249                response.status(),
250                response.text().await.unwrap_or_default()
251            ))),
252        }
253    }
254
255    /// Convert a JSON string to an API response.
256    ///
257    /// This method converts a JSON string to an API response, handling empty responses
258    /// and deserialization errors.
259    ///
260    /// # Arguments
261    ///
262    /// * `json` - The JSON string to convert.
263    ///
264    /// # Returns
265    ///
266    /// A Result containing either the deserialized data or an error.
267    fn make_api_response<T: ApiRequest>(json: &str) -> ApiResponse<<T as ApiRequest>::Output> {
268        // if the response is empty, Ok(None) means the response was successful but empty
269        if json.len() == 0 {
270            return Ok(None);
271        }
272
273        // deserialize into the request's output type
274        let data = serde_json::from_str::<T::Output>(json)
275            .map_err(|e| Error::ResponseError(format!("Error deserializing: {e:#?}")))?;
276
277        Ok(Some(data))
278    }
279}
280
281/// The response from the API.
282///
283/// This is a `Result` type that contains an `Option` of the response data or an `ApiError`.
284/// The `Option` is used because some API responses may be empty (e.g., successful DELETE requests).
285pub type ApiResponse<T> = Result<Option<T>, Error>;
286
287/// Trait for implementing API requests.
288///
289/// This trait defines the interface for all API requests. It requires implementing
290/// the `Output` associated type and the `send` method.
291pub trait ApiRequest {
292    /// The type of the response to deserialize to.
293    type Output: DeserializeOwned;
294
295    /// Build the request and send it to the API.
296    ///
297    /// # Returns
298    ///
299    /// A Future that resolves to an ApiResponse containing either the response data or an error.
300    fn send(&self) -> impl Future<Output = ApiResponse<Self::Output>> + Send;
301}