161 lines
4.2 KiB
Rust
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
|
|
}
|
|
}
|