bitbucket_cli/api/
client.rs

1use anyhow::{Context, Result};
2use reqwest::{Client, Response, StatusCode};
3use serde::de::DeserializeOwned;
4
5use crate::auth::{AuthManager, Credential};
6use crate::models::Paginated;
7
8const API_BASE_URL: &str = "https://api.bitbucket.org/2.0";
9
10/// Bitbucket API client
11#[derive(Clone)]
12pub struct BitbucketClient {
13    client: Client,
14    credential: Credential,
15}
16
17impl BitbucketClient {
18    /// Create a new authenticated client
19    pub fn new(credential: Credential) -> Result<Self> {
20        let client = Client::builder()
21            .user_agent("bitbucket-cli")
22            .build()
23            .context("Failed to create HTTP client")?;
24
25        Ok(Self { client, credential })
26    }
27
28    /// Get the authorization header value
29    pub fn auth_header(&self) -> String {
30        self.credential.auth_header()
31    }
32
33    /// Create a client from stored credentials
34    pub fn from_stored() -> Result<Self> {
35        let auth_manager = AuthManager::new()?;
36        let credential = auth_manager
37            .get_credentials()?
38            .context("Not authenticated. Run 'bitbucket auth login' first.")?;
39
40        Self::new(credential)
41    }
42
43    /// Get the base API URL
44    pub fn base_url(&self) -> &str {
45        API_BASE_URL
46    }
47
48    /// Build a URL for an API endpoint
49    pub fn url(&self, path: &str) -> String {
50        format!("{}{}", API_BASE_URL, path)
51    }
52
53    /// Make a GET request
54    pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
55        let response = self
56            .client
57            .get(self.url(path))
58            .header("Authorization", self.credential.auth_header())
59            .send()
60            .await
61            .context("Request failed")?;
62
63        self.handle_response(response).await
64    }
65
66    /// Make a GET request with query parameters
67    pub async fn get_with_query<T: DeserializeOwned>(
68        &self,
69        path: &str,
70        query: &[(&str, &str)],
71    ) -> Result<T> {
72        let response = self
73            .client
74            .get(self.url(path))
75            .header("Authorization", self.credential.auth_header())
76            .query(query)
77            .send()
78            .await
79            .context("Request failed")?;
80
81        self.handle_response(response).await
82    }
83
84    /// Make a POST request with JSON body
85    pub async fn post<T: DeserializeOwned, B: serde::Serialize>(
86        &self,
87        path: &str,
88        body: &B,
89    ) -> Result<T> {
90        let response = self
91            .client
92            .post(self.url(path))
93            .header("Authorization", self.credential.auth_header())
94            .json(body)
95            .send()
96            .await
97            .context("Request failed")?;
98
99        self.handle_response(response).await
100    }
101
102    /// Make a POST request without expecting a response body
103    pub async fn post_no_response<B: serde::Serialize>(&self, path: &str, body: &B) -> Result<()> {
104        let response = self
105            .client
106            .post(self.url(path))
107            .header("Authorization", self.credential.auth_header())
108            .json(body)
109            .send()
110            .await
111            .context("Request failed")?;
112
113        self.handle_empty_response(response).await
114    }
115
116    /// Make a PUT request with JSON body
117    pub async fn put<T: DeserializeOwned, B: serde::Serialize>(
118        &self,
119        path: &str,
120        body: &B,
121    ) -> Result<T> {
122        let response = self
123            .client
124            .put(self.url(path))
125            .header("Authorization", self.credential.auth_header())
126            .json(body)
127            .send()
128            .await
129            .context("Request failed")?;
130
131        self.handle_response(response).await
132    }
133
134    /// Make a DELETE request
135    pub async fn delete(&self, path: &str) -> Result<()> {
136        let response = self
137            .client
138            .delete(self.url(path))
139            .header("Authorization", self.credential.auth_header())
140            .send()
141            .await
142            .context("Request failed")?;
143
144        self.handle_empty_response(response).await
145    }
146
147    /// Fetch all pages of a paginated endpoint
148    pub async fn get_all_pages<T: DeserializeOwned>(&self, path: &str) -> Result<Vec<T>> {
149        let mut all_items = Vec::new();
150        let mut next_url: Option<String> = Some(self.url(path));
151
152        while let Some(url) = next_url {
153            let response = self
154                .client
155                .get(&url)
156                .header("Authorization", self.credential.auth_header())
157                .send()
158                .await
159                .context("Request failed")?;
160
161            let page: Paginated<T> = self.handle_response(response).await?;
162            all_items.extend(page.values);
163            next_url = page.next;
164        }
165
166        Ok(all_items)
167    }
168
169    /// Handle API response
170    async fn handle_response<T: DeserializeOwned>(&self, response: Response) -> Result<T> {
171        let status = response.status();
172
173        if status.is_success() {
174            response
175                .json()
176                .await
177                .context("Failed to parse response JSON")
178        } else {
179            self.handle_error(status, response).await
180        }
181    }
182
183    /// Handle empty response (for DELETE, etc.)
184    async fn handle_empty_response(&self, response: Response) -> Result<()> {
185        let status = response.status();
186
187        if status.is_success() {
188            Ok(())
189        } else {
190            self.handle_error(status, response).await
191        }
192    }
193
194    /// Handle API errors
195    async fn handle_error<T>(&self, status: StatusCode, response: Response) -> Result<T> {
196        let body = response.text().await.unwrap_or_default();
197
198        match status {
199            StatusCode::UNAUTHORIZED => {
200                anyhow::bail!("Authentication failed. Try running 'bitbucket auth login' again.")
201            }
202            StatusCode::FORBIDDEN => {
203                anyhow::bail!("Access denied. You don't have permission to access this resource.")
204            }
205            StatusCode::NOT_FOUND => {
206                anyhow::bail!("Resource not found.")
207            }
208            StatusCode::TOO_MANY_REQUESTS => {
209                anyhow::bail!("Rate limit exceeded. Please wait and try again.")
210            }
211            _ => {
212                // Try to parse error message from response
213                if let Ok(error) = serde_json::from_str::<ApiError>(&body) {
214                    if let Some(msg) = error.error.message {
215                        anyhow::bail!("API error: {}", msg);
216                    }
217                }
218                anyhow::bail!("API error ({}): {}", status, body)
219            }
220        }
221    }
222}
223
224#[derive(serde::Deserialize)]
225struct ApiError {
226    error: ApiErrorDetail,
227}
228
229#[derive(serde::Deserialize)]
230struct ApiErrorDetail {
231    message: Option<String>,
232}