bitbucket_cli/api/
client.rs1use 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#[derive(Clone)]
12pub struct BitbucketClient {
13 client: Client,
14 credential: Credential,
15}
16
17impl BitbucketClient {
18 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 pub fn auth_header(&self) -> String {
30 self.credential.auth_header()
31 }
32
33 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 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, }
55 } else {
56 credential
57 }
58 } else {
59 credential
60 };
61
62 Self::new(credential)
63 }
64
65 pub fn base_url(&self) -> &str {
67 API_BASE_URL
68 }
69
70 pub fn url(&self, path: &str) -> String {
72 format!("{}{}", API_BASE_URL, path)
73 }
74
75 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 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 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 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 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 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 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 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 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 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 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}