Skip to main content

bitbucket_cli/api/
client.rs

1use anyhow::{Context, Result};
2use reqwest::{Client, Response, StatusCode};
3use serde::de::DeserializeOwned;
4
5use crate::auth::{AuthManager, Credential, OAuthFlow};
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, automatically refreshing if needed
34    pub async 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        // Auto-refresh if the token is expiring soon and we have everything needed
41        let credential = if credential.needs_refresh() {
42            if let (
43                Credential::OAuth {
44                    refresh_token: Some(refresh_token),
45                    ..
46                },
47                Some((client_id, client_secret)),
48            ) = (&credential, credential.oauth_consumer_credentials())
49            {
50                let flow = OAuthFlow::new(client_id.to_string(), client_secret.to_string());
51                match flow.refresh_token(&auth_manager, refresh_token).await {
52                    Ok(refreshed) => refreshed,
53                    Err(_) => credential, // Fall back to existing credential if refresh fails
54                }
55            } else {
56                credential
57            }
58        } else {
59            credential
60        };
61
62        Self::new(credential)
63    }
64
65    /// Get the base API URL
66    pub fn base_url(&self) -> &str {
67        API_BASE_URL
68    }
69
70    /// Build a URL for an API endpoint
71    pub fn url(&self, path: &str) -> String {
72        format!("{}{}", API_BASE_URL, path)
73    }
74
75    /// Make a GET request
76    pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
77        let response = self
78            .client
79            .get(self.url(path))
80            .header("Authorization", self.credential.auth_header())
81            .send()
82            .await
83            .context("Request failed")?;
84
85        self.handle_response(response).await
86    }
87
88    /// Make a GET request with query parameters
89    pub async fn get_with_query<T: DeserializeOwned>(
90        &self,
91        path: &str,
92        query: &[(&str, &str)],
93    ) -> Result<T> {
94        let response = self
95            .client
96            .get(self.url(path))
97            .header("Authorization", self.credential.auth_header())
98            .query(query)
99            .send()
100            .await
101            .context("Request failed")?;
102
103        self.handle_response(response).await
104    }
105
106    /// Make a POST request with JSON body
107    pub async fn post<T: DeserializeOwned, B: serde::Serialize>(
108        &self,
109        path: &str,
110        body: &B,
111    ) -> Result<T> {
112        let response = self
113            .client
114            .post(self.url(path))
115            .header("Authorization", self.credential.auth_header())
116            .json(body)
117            .send()
118            .await
119            .context("Request failed")?;
120
121        self.handle_response(response).await
122    }
123
124    /// Make a POST request without expecting a response body
125    pub async fn post_no_response<B: serde::Serialize>(&self, path: &str, body: &B) -> Result<()> {
126        let response = self
127            .client
128            .post(self.url(path))
129            .header("Authorization", self.credential.auth_header())
130            .json(body)
131            .send()
132            .await
133            .context("Request failed")?;
134
135        self.handle_empty_response(response).await
136    }
137
138    /// Make a PUT request with JSON body
139    pub async fn put<T: DeserializeOwned, B: serde::Serialize>(
140        &self,
141        path: &str,
142        body: &B,
143    ) -> Result<T> {
144        let response = self
145            .client
146            .put(self.url(path))
147            .header("Authorization", self.credential.auth_header())
148            .json(body)
149            .send()
150            .await
151            .context("Request failed")?;
152
153        self.handle_response(response).await
154    }
155
156    /// Make a DELETE request
157    pub async fn delete(&self, path: &str) -> Result<()> {
158        let response = self
159            .client
160            .delete(self.url(path))
161            .header("Authorization", self.credential.auth_header())
162            .send()
163            .await
164            .context("Request failed")?;
165
166        self.handle_empty_response(response).await
167    }
168
169    /// Fetch all pages of a paginated endpoint
170    pub async fn get_all_pages<T: DeserializeOwned>(&self, path: &str) -> Result<Vec<T>> {
171        let mut all_items = Vec::new();
172        let mut next_url: Option<String> = Some(self.url(path));
173
174        while let Some(url) = next_url {
175            let response = self
176                .client
177                .get(&url)
178                .header("Authorization", self.credential.auth_header())
179                .send()
180                .await
181                .context("Request failed")?;
182
183            let page: Paginated<T> = self.handle_response(response).await?;
184            all_items.extend(page.values);
185            next_url = page.next;
186        }
187
188        Ok(all_items)
189    }
190
191    /// Handle API response
192    async fn handle_response<T: DeserializeOwned>(&self, response: Response) -> Result<T> {
193        let status = response.status();
194
195        if status.is_success() {
196            response
197                .json()
198                .await
199                .context("Failed to parse response JSON")
200        } else {
201            self.handle_error(status, response).await
202        }
203    }
204
205    /// Handle empty response (for DELETE, etc.)
206    async fn handle_empty_response(&self, response: Response) -> Result<()> {
207        let status = response.status();
208
209        if status.is_success() {
210            Ok(())
211        } else {
212            self.handle_error(status, response).await
213        }
214    }
215
216    /// Handle API errors
217    async fn handle_error<T>(&self, status: StatusCode, response: Response) -> Result<T> {
218        let body = response.text().await.unwrap_or_default();
219
220        match status {
221            StatusCode::UNAUTHORIZED => {
222                anyhow::bail!("Authentication failed. Try running 'bitbucket auth login' again.")
223            }
224            StatusCode::FORBIDDEN => {
225                anyhow::bail!("Access denied. You don't have permission to access this resource.")
226            }
227            StatusCode::NOT_FOUND => {
228                anyhow::bail!("Resource not found.")
229            }
230            StatusCode::TOO_MANY_REQUESTS => {
231                anyhow::bail!("Rate limit exceeded. Please wait and try again.")
232            }
233            _ => {
234                // Try to parse error message from response
235                if let Ok(error) = serde_json::from_str::<ApiError>(&body) {
236                    if let Some(msg) = error.error.message {
237                        anyhow::bail!("API error: {}", msg);
238                    }
239                }
240                anyhow::bail!("API error ({}): {}", status, body)
241            }
242        }
243    }
244}
245
246#[derive(serde::Deserialize)]
247struct ApiError {
248    error: ApiErrorDetail,
249}
250
251#[derive(serde::Deserialize)]
252struct ApiErrorDetail {
253    message: Option<String>,
254}