1use reqwest::header::HeaderMap;
2use serde::de::DeserializeOwned;
3use std::sync::Arc;
4use tracing::{debug, warn};
5
6use crate::auth::AuthStrategy;
7use crate::error::{Error, Result};
8use crate::types::*;
9
10pub struct ClientBuilder<A> {
11 auth: A,
12 base_url: Option<String>,
13}
14
15impl ClientBuilder<()> {
16 pub fn new() -> Self {
17 Self {
18 auth: (),
19 base_url: None,
20 }
21 }
22
23 pub fn auth<S: AuthStrategy + 'static>(self, auth: S) -> ClientBuilder<S> {
24 ClientBuilder {
25 auth,
26 base_url: self.base_url,
27 }
28 }
29}
30
31impl Default for ClientBuilder<()> {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37impl<A: AuthStrategy + 'static> ClientBuilder<A> {
38 pub fn base_url(mut self, url: impl Into<String>) -> Self {
40 self.base_url = Some(url.into());
41 self
42 }
43
44 pub fn build(self) -> Result<Client> {
45 let base_url = self
46 .base_url
47 .ok_or_else(|| Error::InvalidRequest("base_url is required".to_string()))?;
48
49 Ok(Client {
50 http: reqwest::Client::new(),
51 auth: Arc::new(self.auth),
52 base_url,
53 })
54 }
55}
56
57#[derive(Clone)]
58pub struct Client {
59 http: reqwest::Client,
60 auth: Arc<dyn AuthStrategy>,
61 base_url: String,
62}
63
64impl Client {
65 pub fn builder() -> ClientBuilder<()> {
66 ClientBuilder::new()
67 }
68
69 async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
70 self.request(reqwest::Method::GET, path, None::<&()>).await
71 }
72
73 async fn post<T: DeserializeOwned, B: serde::Serialize>(
74 &self,
75 path: &str,
76 body: &B,
77 ) -> Result<T> {
78 self.request(reqwest::Method::POST, path, Some(body)).await
79 }
80
81 async fn put<B: serde::Serialize>(&self, path: &str, body: &B) -> Result<()> {
82 self.request_no_response(reqwest::Method::PUT, path, Some(body))
83 .await
84 }
85
86 async fn request<T: DeserializeOwned>(
87 &self,
88 method: reqwest::Method,
89 path: &str,
90 body: Option<&impl serde::Serialize>,
91 ) -> Result<T> {
92 let url = format!("{}/rest/api/3{}", self.base_url, path);
93 debug!("Jira API request: {} {}", method, url);
94
95 let mut headers = HeaderMap::new();
96 self.auth.apply(&mut headers).await?;
97 headers.insert("Content-Type", "application/json".parse().unwrap());
98
99 let mut request = self.http.request(method, &url).headers(headers);
100
101 if let Some(body) = body {
102 request = request.json(body);
103 }
104
105 let response = request.send().await?;
106 self.handle_response(response).await
107 }
108
109 async fn request_no_response(
110 &self,
111 method: reqwest::Method,
112 path: &str,
113 body: Option<&impl serde::Serialize>,
114 ) -> Result<()> {
115 let url = format!("{}/rest/api/3{}", self.base_url, path);
116 debug!("Jira API request: {} {}", method, url);
117
118 let mut headers = HeaderMap::new();
119 self.auth.apply(&mut headers).await?;
120 headers.insert("Content-Type", "application/json".parse().unwrap());
121
122 let mut request = self.http.request(method, &url).headers(headers);
123
124 if let Some(body) = body {
125 request = request.json(body);
126 }
127
128 let response = request.send().await?;
129 let status = response.status();
130
131 if status.is_success() {
132 Ok(())
133 } else {
134 let status_code = status.as_u16();
135 let body = response.text().await.unwrap_or_default();
136 warn!("Jira API error ({}): {}", status_code, body);
137
138 match status_code {
139 401 => Err(Error::Unauthorized),
140 403 => Err(Error::Forbidden(body)),
141 404 => Err(Error::NotFound(body)),
142 429 => Err(Error::RateLimited { retry_after: 60 }),
143 _ => Err(Error::Api {
144 status: status_code,
145 message: body,
146 }),
147 }
148 }
149 }
150
151 async fn handle_response<T: DeserializeOwned>(&self, response: reqwest::Response) -> Result<T> {
152 let status = response.status();
153
154 if status.is_success() {
155 let body = response.text().await?;
156 serde_json::from_str(&body).map_err(Error::from)
157 } else {
158 let status_code = status.as_u16();
159 let body = response.text().await.unwrap_or_default();
160 warn!("Jira API error ({}): {}", status_code, body);
161
162 match status_code {
163 401 => Err(Error::Unauthorized),
164 403 => Err(Error::Forbidden(body)),
165 404 => Err(Error::NotFound(body)),
166 429 => Err(Error::RateLimited { retry_after: 60 }),
167 _ => Err(Error::Api {
168 status: status_code,
169 message: body,
170 }),
171 }
172 }
173 }
174
175 pub async fn get_issue(&self, key: &str) -> Result<Issue> {
177 self.get(&format!("/issue/{}", key)).await
178 }
179
180 pub async fn create_issue(&self, input: CreateIssueInput) -> Result<CreatedIssue> {
182 self.post("/issue", &input).await
183 }
184
185 pub async fn update_issue(&self, key: &str, input: UpdateIssueInput) -> Result<()> {
187 self.put(&format!("/issue/{}", key), &input).await
188 }
189
190 pub async fn search_issues(&self, jql: &str, max_results: Option<u32>) -> Result<SearchResult> {
192 let max = max_results.unwrap_or(50);
193 let encoded_jql = urlencoding::encode(jql);
194 self.get(&format!("/search?jql={}&maxResults={}", encoded_jql, max))
195 .await
196 }
197
198 pub async fn list_projects(&self) -> Result<Vec<Project>> {
200 self.get("/project").await
201 }
202
203 pub async fn get_project(&self, key: &str) -> Result<Project> {
205 self.get(&format!("/project/{}", key)).await
206 }
207
208 pub async fn add_comment(&self, issue_key: &str, input: AddCommentInput) -> Result<Comment> {
210 self.post(&format!("/issue/{}/comment", issue_key), &input)
211 .await
212 }
213
214 pub async fn get_transitions(&self, issue_key: &str) -> Result<Vec<Transition>> {
216 #[derive(serde::Deserialize)]
217 struct Response {
218 transitions: Vec<Transition>,
219 }
220
221 let resp: Response = self.get(&format!("/issue/{}/transitions", issue_key)).await?;
222 Ok(resp.transitions)
223 }
224
225 pub async fn transition_issue(&self, issue_key: &str, transition_id: &str) -> Result<()> {
227 let input = TransitionInput::new(transition_id);
228 self.request_no_response(
229 reqwest::Method::POST,
230 &format!("/issue/{}/transitions", issue_key),
231 Some(&input),
232 )
233 .await
234 }
235
236 pub async fn myself(&self) -> Result<User> {
238 self.get("/myself").await
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use crate::auth::BasicAuth;
246
247 #[test]
248 fn test_builder_requires_base_url() {
249 let result = Client::builder()
250 .auth(BasicAuth::new("email@example.com", "api_token"))
251 .build();
252 assert!(result.is_err());
253 }
254
255 #[test]
256 fn test_builder_with_base_url() {
257 let result = Client::builder()
258 .auth(BasicAuth::new("email@example.com", "api_token"))
259 .base_url("https://example.atlassian.net")
260 .build();
261 assert!(result.is_ok());
262 }
263}