bitbucket_cli/api/
client.rs1use 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#[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 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 pub fn base_url(&self) -> &str {
45 API_BASE_URL
46 }
47
48 pub fn url(&self, path: &str) -> String {
50 format!("{}{}", API_BASE_URL, path)
51 }
52
53 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 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 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 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 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 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 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 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 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 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 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}