1pub mod error;
2pub mod pagination;
3pub mod ratelimit;
4pub mod retry;
5
6use error::{ApiError, Result};
7use ratelimit::RateLimiter;
8use reqwest::{Client, Method, RequestBuilder, StatusCode};
9use retry::{retry_with_backoff, RetryConfig};
10use serde::de::DeserializeOwned;
11use serde::Serialize;
12use std::time::Duration;
13use tracing::{debug, error, warn};
14use url::Url;
15
16#[derive(Clone, Debug)]
17pub enum AuthMethod {
18 Basic { username: String, token: String },
19 Bearer { token: String },
20}
21
22#[derive(Clone)]
23pub struct ApiClient {
24 client: Client,
25 base_url: Url,
26 auth: Option<AuthMethod>,
27 retry_config: RetryConfig,
28 rate_limiter: RateLimiter,
29}
30
31impl ApiClient {
32 pub fn new(base_url: impl AsRef<str>) -> Result<Self> {
33 let url = Url::parse(base_url.as_ref()).map_err(ApiError::InvalidUrl)?;
34
35 let client = Client::builder()
36 .user_agent(format!("atlassian-cli/{}", env!("CARGO_PKG_VERSION")))
37 .timeout(Duration::from_secs(30))
38 .build()
39 .map_err(ApiError::RequestFailed)?;
40
41 Ok(Self {
42 client,
43 base_url: url,
44 auth: None,
45 retry_config: RetryConfig::default(),
46 rate_limiter: RateLimiter::new(),
47 })
48 }
49
50 pub fn with_basic_auth(
51 mut self,
52 username: impl Into<String>,
53 token: impl Into<String>,
54 ) -> Self {
55 self.auth = Some(AuthMethod::Basic {
56 username: username.into(),
57 token: token.into(),
58 });
59 self
60 }
61
62 pub fn with_bearer_token(mut self, token: impl Into<String>) -> Self {
63 self.auth = Some(AuthMethod::Bearer {
64 token: token.into(),
65 });
66 self
67 }
68
69 pub fn with_retry_config(mut self, config: RetryConfig) -> Self {
70 self.retry_config = config;
71 self
72 }
73
74 pub fn base_url(&self) -> &str {
75 self.base_url.as_str()
76 }
77
78 pub async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
79 self.request(Method::GET, path, Option::<&()>::None).await
80 }
81
82 pub async fn post<T: DeserializeOwned, B: Serialize + ?Sized>(
83 &self,
84 path: &str,
85 body: &B,
86 ) -> Result<T> {
87 self.request(Method::POST, path, Some(body)).await
88 }
89
90 pub async fn put<T: DeserializeOwned, B: Serialize + ?Sized>(
91 &self,
92 path: &str,
93 body: &B,
94 ) -> Result<T> {
95 self.request(Method::PUT, path, Some(body)).await
96 }
97
98 pub async fn delete<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
99 self.request(Method::DELETE, path, Option::<&()>::None)
100 .await
101 }
102
103 pub async fn request<T: DeserializeOwned, B: Serialize + ?Sized>(
104 &self,
105 method: Method,
106 path: &str,
107 body: Option<&B>,
108 ) -> Result<T> {
109 if let Some(wait_secs) = self.rate_limiter.check_limit().await {
110 warn!(wait_secs, "Rate limit reached, waiting");
111 tokio::time::sleep(Duration::from_secs(wait_secs)).await;
112 }
113
114 let url = self.base_url.clone();
115 let joined = url
116 .join(path.strip_prefix('/').unwrap_or(path))
117 .map_err(ApiError::InvalidUrl)?;
118
119 debug!(method = %method, url = %joined, "Sending request");
120
121 let result = retry_with_backoff(&self.retry_config, || async {
122 let mut req = self.client.request(method.clone(), joined.clone());
123 req = self.apply_auth(req);
124
125 if let Some(body) = body {
126 req = req.json(body);
127 }
128
129 let response = req.send().await.map_err(ApiError::RequestFailed)?;
130
131 self.rate_limiter.update_from_response(&response).await;
132
133 let status = response.status();
134
135 match status {
136 StatusCode::UNAUTHORIZED => Err(ApiError::AuthenticationFailed {
137 message: "Invalid or expired credentials".to_string(),
138 }),
139 StatusCode::NOT_FOUND => {
140 let resource = joined.path().to_string();
141 Err(ApiError::NotFound { resource })
142 }
143 StatusCode::BAD_REQUEST => {
144 let message = response
145 .text()
146 .await
147 .unwrap_or_else(|_| "Bad request".to_string());
148 Err(ApiError::BadRequest { message })
149 }
150 StatusCode::TOO_MANY_REQUESTS => {
151 let retry_after = response
152 .headers()
153 .get("retry-after")
154 .and_then(|v| v.to_str().ok())
155 .and_then(|s| s.parse().ok())
156 .unwrap_or(60);
157 Err(ApiError::RateLimitExceeded { retry_after })
158 }
159 status if status.is_server_error() => {
160 let message = response
161 .text()
162 .await
163 .unwrap_or_else(|_| "Server error".to_string());
164 Err(ApiError::ServerError {
165 status: status.as_u16(),
166 message,
167 })
168 }
169 status if status.is_success() => response.json::<T>().await.map_err(|e| {
170 error!("Failed to parse JSON response: {}", e);
171 ApiError::InvalidResponse(e.to_string())
172 }),
173 _ => {
174 let message = response
175 .text()
176 .await
177 .unwrap_or_else(|_| format!("Unexpected status: {}", status));
178 Err(ApiError::ServerError {
179 status: status.as_u16(),
180 message,
181 })
182 }
183 }
184 })
185 .await?;
186
187 Ok(result)
188 }
189
190 pub fn apply_auth(&self, request: RequestBuilder) -> RequestBuilder {
191 match &self.auth {
192 Some(AuthMethod::Basic { username, token }) => {
193 request.basic_auth(username, Some(token))
194 }
195 Some(AuthMethod::Bearer { token }) => request.bearer_auth(token),
196 None => request,
197 }
198 }
199
200 pub fn rate_limiter(&self) -> &RateLimiter {
201 &self.rate_limiter
202 }
203}