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(¶ms);
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}