1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
use anyhow::{anyhow, Context, Result};
use dialoguer::{theme::ColorfulTheme, Select};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use url::Url;

use reqwest::blocking::Client;

#[derive(Serialize)]
struct IssueSearchRequestBody {
    jql: String,
    fields: Vec<String>,
}

#[derive(Deserialize)]
struct IssueSearchResponseBody {
    issues: Option<Vec<Issue>>,
}

#[derive(Deserialize)]
pub struct Issue {
    pub key: String,
    pub fields: HashMap<String, String>,
}

impl fmt::Display for Issue {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{} - {}",
            &self.key,
            &self.fields.get("summary").unwrap()
        )
    }
}

#[derive(Deserialize, Debug)]
pub struct Config {
    jira_host: Url,
    jira_user: String,
    jira_password: Option<String>,
}

pub fn search_issues(config: Config, query: &str) -> Result<Vec<Issue>> {
    let body = IssueSearchRequestBody {
        jql: String::from(query),
        fields: vec![String::from("summary")],
    };

    let resp = Client::new()
        .post(config.jira_host.join("/rest/api/2/search").unwrap())
        .json(&body)
        .basic_auth(config.jira_user, config.jira_password)
        .send()
        .context("Unable to search JIRA for issues")?
        .json::<IssueSearchResponseBody>()
        .context("Unable to decode JIRA response")?;

    resp.issues
        .ok_or_else(|| anyhow!("No issues found for query"))
}

pub fn select_issue(issues: &[Issue]) -> Result<&Issue> {
    let selection = Select::with_theme(&ColorfulTheme::default())
        .items(&issues)
        .default(0)
        .interact_opt()?;

    let index = selection.ok_or_else(|| anyhow!("No JIRA issue selected"))?;

    Ok(&issues[index])
}