taskr/src/jira.rs

161 lines
4.2 KiB
Rust

use std::collections::HashMap;
use crate::{
client::{Client, ResourceRequest},
result::Result,
};
use reqwest::{
header::{HeaderMap, HeaderValue},
Client as RClient, Method,
};
use serde::Deserialize;
use tokio::spawn;
use tracing::debug;
#[derive(Debug)]
pub struct Jira {
client: Client,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct User {
pub account_id: String,
pub email_address: String,
pub account_type: String,
pub display_name: String,
pub active: bool,
pub time_zone: String,
pub locale: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Component {
pub name: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Priority {
pub id: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct IssueStatusCategory {
pub id: u64,
pub key: String,
pub name: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct IssueStatus {
pub name: String,
pub status_category: IssueStatusCategory,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct IssueFields {
pub components: Option<Vec<Component>>,
pub labels: Vec<String>,
pub summary: String,
pub status: IssueStatus,
pub priority: Priority,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Issue {
pub id: String,
pub key: String,
pub fields: IssueFields,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct IssueSearch {
issues: Vec<Issue>,
max_results: usize,
pub start_at: usize,
total: usize,
}
impl Jira {
pub fn try_new(url: &str, token: &str) -> Result<Self> {
let mut headers = HeaderMap::new();
// TODO: ensure this token cannot be leaked to logs?
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Basic {}", token.trim()))?,
);
headers.insert("content-type", HeaderValue::from_str("application/json")?);
headers.insert("accepts", HeaderValue::from_str("application/json")?);
let base_client = RClient::builder().default_headers(headers).build()?;
let client = Client::try_new(base_client, url)?;
Ok(Self { client })
}
pub async fn me(&self) -> Result<User> {
self.client.get("/rest/api/3/myself").await
}
pub async fn jql(&self, jql: &str) -> Result<Vec<Issue>> {
debug!("Fetching issues for jql {}", jql);
let mut results: IssueSearch = self
.client
.build(Method::GET, "/rest/api/3/search")?
.query(&[("jql", jql)])
.res()
.await?;
// TODO: are there rate limits to be concerned about?
let mut issues = Vec::with_capacity(results.total);
issues.append(&mut results.issues);
let mut futures = Vec::with_capacity((results.total / results.max_results) + 1);
for i in 1..futures.capacity() {
let start_at = i * results.max_results;
debug!("Fetching issues for jql {} (page {})", jql, i);
futures.push(spawn(
self.client
.build(Method::GET, "/rest/api/3/search")?
.query(&[("jql", jql), ("startAt", &start_at.to_string())])
.res::<IssueSearch>(),
));
}
for task in futures {
let mut result = task.await??;
issues.append(&mut result.issues);
}
Ok(issues)
}
pub async fn issue(&self, key: &str) -> Result<Issue> {
self.client
.build(Method::GET, &format!("/rest/api/3/issue/{key}"))?
.res::<Issue>()
.await
}
// TODO: move this somewhere nicer?
pub fn by_key(issues: Vec<Issue>) -> HashMap<String, Issue> {
issues.into_iter().map(|i| (i.key.to_owned(), i)).collect()
}
pub async fn assigned_open_issues(&self) -> Result<Vec<Issue>> {
let me = self.me().await?;
let jql = format!("assignee = {0} and statusCategory != Done", me.account_id);
self.jql(&jql).await
}
}