phab_lib/client/
phabricator.rs

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  /// This function will trim 'T' at the start of phabricator id.
50  /// This is to cover case when you copy-paste the phabricator id from url,
51  /// e.g. yourphabhost.com/T1234
52  /// ```
53  /// # use phab_lib::client::phabricator::PhabricatorClient;
54  ///
55  /// let phabricator_id  = PhabricatorClient::clean_id("T1234");
56  /// assert_eq!(phabricator_id, "1234");
57  /// ```
58  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      // We only have 1 possible assignment
164      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      // We only have 1 possible assignment
220      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}