1use std::fs;
2
3use anyhow::Error;
4use futures::future;
5use futures::future::BoxFuture;
6use futures::future::FutureExt;
7use reqwest::Client as HttpClient;
8use reqwest::ClientBuilder as HttpClientBuilder;
9use reqwest::Identity;
10use serde_json::Value;
11
12use crate::client::config::PhabricatorClientConfig;
13use crate::dto::Task;
14use crate::dto::TaskFamily;
15use crate::dto::User;
16use crate::types::ResultAnyError;
17
18pub struct PhabricatorClient {
19 http: HttpClient,
20 host: String,
21 api_token: String,
22}
23
24#[derive(Debug, Clone, thiserror::Error)]
25pub enum ErrorType {
26 #[error("Certificate identity path: {pkcs12_path:?}, error: {message:?}")]
27 CertificateIdentityError {
28 pkcs12_path: String,
29 message: String,
30 },
31
32 #[error("Fail to configure http client, error: {message}")]
33 FailToConfigureHttpClient { message: String },
34
35 #[error("Validation error: {message}")]
36 ValidationError { message: String },
37
38 #[error("Fetch sub tasks error: {message}")]
39 FetchSubTasksError { message: String },
40
41 #[error("Fetch task error: {message}")]
42 FetchTaskError { message: String },
43
44 #[error("Parse error: {message}")]
45 ParseError { message: String },
46}
47
48impl PhabricatorClient {
49 pub fn clean_id(id: &str) -> &str {
59 return id.trim_start_matches('T');
60 }
61
62 pub fn new(config: PhabricatorClientConfig) -> ResultAnyError<PhabricatorClient> {
63 let mut http_client_builder = Ok(HttpClientBuilder::new());
64 let PhabricatorClientConfig {
65 host,
66 api_token,
67 cert_identity_config,
68 } = config;
69
70 let cert_identity: Option<Result<_, _>> = cert_identity_config.map(|config| {
71 return fs::read(&config.pkcs12_path)
72 .map_err(|err| ErrorType::FailToConfigureHttpClient {
73 message: format!("Failed to read pkcs12 from {}, {}", config.pkcs12_path, err),
74 })
75 .and_then(|bytes| {
76 return Identity::from_pkcs12_der(&bytes, &config.pkcs12_password).map_err(|err| {
77 ErrorType::CertificateIdentityError {
78 pkcs12_path: String::from(config.pkcs12_path),
79 message: err.to_string(),
80 }
81 });
82 });
83 });
84
85 if let Some(cert_identity) = cert_identity {
86 http_client_builder =
87 http_client_builder.and_then(|http_client_builder: HttpClientBuilder| {
88 return cert_identity.map(|cert_identity: Identity| {
89 return http_client_builder.identity(cert_identity);
90 });
91 });
92 }
93
94 return http_client_builder
95 .and_then(|http_client_builder| {
96 http_client_builder
97 .build()
98 .map_err(|err| ErrorType::FailToConfigureHttpClient {
99 message: err.to_string(),
100 })
101 })
102 .map_err(Error::new)
103 .map(|http_client| {
104 return PhabricatorClient {
105 http: http_client,
106 host: String::from(host),
107 api_token: String::from(api_token),
108 };
109 });
110 }
111}
112
113impl PhabricatorClient {
114 pub async fn get_user_by_phid(&self, user_phid: &str) -> ResultAnyError<Option<User>> {
115 return self
116 .get_users_by_phids(vec![user_phid])
117 .await
118 .map(|users| users.get(0).map(ToOwned::to_owned));
119 }
120
121 pub async fn get_task_by_id(&self, task_id: &str) -> ResultAnyError<Option<Task>> {
122 return self
123 .get_tasks_by_ids(vec![task_id])
124 .await
125 .map(|tasks| tasks.get(0).map(ToOwned::to_owned));
126 }
127
128 pub async fn get_users_by_phids(&self, user_phids: Vec<&str>) -> ResultAnyError<Vec<User>> {
129 let mut form: Vec<(String, &str)> = vec![("api.token".to_owned(), self.api_token.as_str())];
130
131 for i in 0..user_phids.len() {
132 let key = format!("constraints[phids][{}]", i);
133 let user_phid = user_phids.get(i).unwrap();
134
135 form.push((key, user_phid));
136 }
137
138 let url = format!("{}/api/user.search", self.host);
139
140 log::debug!("Getting user by id {} {:?}", url, form);
141
142 let result = self
143 .http
144 .post(&url)
145 .form(&form)
146 .send()
147 .await
148 .map_err(Error::new)?;
149
150 let response_text = result.text().await.map_err(Error::new)?;
151
152 log::debug!("Response {}", response_text);
153
154 let body: Value = serde_json::from_str(response_text.as_str()).map_err(Error::new)?;
155
156 if let Value::Array(users_json) = &body["result"]["data"] {
157 if users_json.is_empty() {
158 return Ok(vec![]);
159 }
160
161 log::debug!("Parsing {:?}", users_json);
162
163 let users: Vec<User> = users_json.iter().map(User::from_json).collect();
165
166 log::debug!("Parsed {:?}", users);
167
168 return Ok(users);
169 } else {
170 return Err(
171 ErrorType::ParseError {
172 message: format!("Cannot parse {}", &body),
173 }
174 .into(),
175 );
176 }
177 }
178
179 pub async fn get_tasks_by_ids(&self, task_ids: Vec<&str>) -> ResultAnyError<Vec<Task>> {
180 let mut form: Vec<(String, &str)> = vec![
181 ("api.token".to_owned(), self.api_token.as_str()),
182 ("order".to_owned(), "oldest"),
183 ("attachments[columns]".to_owned(), "true"),
184 ("attachments[projects]".to_owned(), "true"),
185 ];
186
187 for i in 0..task_ids.len() {
188 let key = format!("constraints[ids][{}]", i);
189 let task_id = PhabricatorClient::clean_id(task_ids.get(i).unwrap());
190
191 form.push((key, task_id));
192 }
193
194 let url = format!("{}/api/maniphest.search", self.host);
195
196 log::debug!("Getting task by id {} {:?}", url, form);
197
198 let result = self
199 .http
200 .post(&url)
201 .form(&form)
202 .send()
203 .await
204 .map_err(Error::new)?;
205
206 let response_text = result.text().await.map_err(Error::new)?;
207
208 log::debug!("Response {}", response_text);
209
210 let body: Value = serde_json::from_str(response_text.as_str()).map_err(Error::new)?;
211
212 if let Value::Array(tasks_json) = &body["result"]["data"] {
213 if tasks_json.is_empty() {
214 return Ok(vec![]);
215 }
216
217 log::debug!("Parsing {:?}", tasks_json);
218
219 let tasks: Vec<Task> = tasks_json.iter().map(Task::from_json).collect();
221
222 log::debug!("Parsed {:?}", tasks);
223
224 return Ok(tasks);
225 } else {
226 return Err(
227 ErrorType::ParseError {
228 message: format!("Cannot parse {}", &body),
229 }
230 .into(),
231 );
232 }
233 }
234
235 pub async fn get_task_family(&self, root_task_id: &str) -> ResultAnyError<Option<TaskFamily>> {
236 let parent_task = self.get_task_by_id(root_task_id).await?;
237
238 if parent_task.is_none() {
239 return Ok(None);
240 }
241
242 let parent_task = parent_task.unwrap();
243
244 let child_tasks = self.get_child_tasks(vec![root_task_id]).await?;
245 let task_family = TaskFamily {
246 parent_task,
247 children: child_tasks,
248 };
249
250 return Ok(Some(task_family));
251 }
252
253 pub fn get_child_tasks<'a>(
254 &'a self,
255 parent_task_ids: Vec<&'a str>,
256 ) -> BoxFuture<'a, ResultAnyError<Vec<TaskFamily>>> {
257 return async move {
258 if parent_task_ids.is_empty() {
259 return Err(
260 ErrorType::ValidationError {
261 message: String::from("Parent ids cannot be empty"),
262 }
263 .into(),
264 );
265 }
266
267 let mut form: Vec<(String, &str)> = vec![("api.token".to_owned(), self.api_token.as_str())];
268
269 for i in 0..parent_task_ids.len() {
270 let task_id = PhabricatorClient::clean_id(parent_task_ids.get(i).unwrap());
271 let key = format!("constraints[parentIDs][{}]", i);
272
273 form.push((key, task_id));
274 }
275
276 form.push(("order".to_owned(), "oldest"));
277 form.push(("attachments[columns]".to_owned(), "true"));
278 form.push(("attachments[projects]".to_owned(), "true"));
279
280 let url = format!("{}/api/maniphest.search", self.host);
281
282 log::debug!("Getting tasks {} {:?}", url, form);
283
284 let result = self
285 .http
286 .post(&url)
287 .form(&form)
288 .send()
289 .await
290 .map_err(Error::new)?;
291
292 let response_text = result.text().await.map_err(Error::new)?;
293
294 log::debug!("Response {}", response_text);
295
296 let body: Value = serde_json::from_str(response_text.as_str()).map_err(Error::new)?;
297
298 if let Value::Array(tasks_json) = &body["result"]["data"] {
299 let tasks: Vec<BoxFuture<ResultAnyError<TaskFamily>>> = tasks_json
300 .iter()
301 .map(|v: &Value| -> BoxFuture<ResultAnyError<TaskFamily>> {
302 return async move {
303 let parent_task = Task::from_json(&v);
304
305 let children = self
306 .get_child_tasks(vec![parent_task.id.as_str()])
307 .await
308 .map_err(|err| {
309 return ErrorType::FetchSubTasksError {
310 message: format!(
311 "Could not fetch sub tasks with parent id {}, err: {}",
312 parent_task.id, err
313 ),
314 };
315 })?;
316
317 return Ok(TaskFamily {
318 parent_task,
319 children,
320 });
321 }
322 .boxed();
323 })
324 .collect();
325
326 let (tasks, failed_tasks): (Vec<_>, Vec<_>) = future::join_all(tasks)
327 .await
328 .into_iter()
329 .partition(Result::is_ok);
330
331 if !failed_tasks.is_empty() {
332 let error = ErrorType::FetchSubTasksError {
333 message: failed_tasks
334 .into_iter()
335 .fold(String::new(), |acc, task_result| {
336 return format!("{}\n{}", acc, task_result.err().unwrap());
337 }),
338 };
339
340 return Err(error.into());
341 }
342
343 let task_families: Vec<TaskFamily> = tasks.into_iter().map(Result::unwrap).collect();
344
345 return Ok(task_families);
346 } else {
347 return Err(
348 ErrorType::ParseError {
349 message: format!("Cannot parse {}", &body),
350 }
351 .into(),
352 );
353 }
354 }
355 .boxed();
356 }
357}
358
359#[cfg(test)]
360mod test {
361 use super::*;
362 use crate::client::config::CertIdentityConfig;
363
364 fn dummy_config() -> PhabricatorClientConfig {
365 return PhabricatorClientConfig {
366 host: "http://localhost".into(),
367 api_token: "foo".into(),
368 cert_identity_config: None,
369 };
370 }
371
372 #[test]
373 fn test_create_new_client_with_invalid_pkcs12_path() {
374 let mut config = dummy_config();
375 config.cert_identity_config = Some(CertIdentityConfig {
376 pkcs12_path: "/path/to/invalid/config".into(),
377 pkcs12_password: "testpassword".into(),
378 });
379
380 let maybe_client = PhabricatorClient::new(config);
381
382 assert!(maybe_client.is_err());
383
384 let err = maybe_client.err().unwrap();
385
386 assert!(err
387 .to_string()
388 .contains("Failed to read pkcs12 from /path/to/invalid/config"));
389 }
390}