Compare commits
4 commits
e20f9e6a9d
...
194e25dc14
Author | SHA1 | Date | |
---|---|---|---|
194e25dc14 | |||
8a7c989f48 | |||
6102233bc5 | |||
5b47f3d5e9 |
25 changed files with 700 additions and 388 deletions
15
Cargo.lock
generated
15
Cargo.lock
generated
|
@ -939,8 +939,8 @@ dependencies = [
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"directories",
|
"directories",
|
||||||
"gray_matter",
|
"gray_matter",
|
||||||
|
"libjirac",
|
||||||
"open",
|
"open",
|
||||||
"reqwest",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tabwriter",
|
"tabwriter",
|
||||||
|
@ -976,6 +976,19 @@ version = "0.2.169"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libjirac"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"http",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
|
|
|
@ -5,6 +5,8 @@ resolver = "2"
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
clap = { version = "4.4", features = ["derive"] }
|
clap = { version = "4.4", features = ["derive"] }
|
||||||
reqwest = { version = "0.12", features = ["json"] }
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
|
http = "1"
|
||||||
|
url = "2"
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
@ -17,3 +19,4 @@ open = "5.2"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
tabwriter = "1.4"
|
tabwriter = "1.4"
|
||||||
crossterm = "0.28"
|
crossterm = "0.28"
|
||||||
|
thiserror = "2"
|
|
@ -4,8 +4,9 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
libjirac = { path = "../libjirac" }
|
||||||
|
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
use crate::jira_config::JiraConfig;
|
use crate::jira_config::JiraConfig;
|
||||||
use gray_matter::engine::TOML;
|
use gray_matter::engine::TOML;
|
||||||
use gray_matter::Matter;
|
use gray_matter::Matter;
|
||||||
use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
|
use libjirac::client::commands::{
|
||||||
|
IssueCreateCommand, IssueTransitionsCommand, IssueTransitionsUpdateCommand,
|
||||||
|
};
|
||||||
|
use libjirac::client::JiraClient;
|
||||||
|
use libjirac::entities::issue_request::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
@ -23,84 +27,6 @@ struct IssueMetadata {
|
||||||
extra: HashMap<String, serde_json::Value>,
|
extra: HashMap<String, serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
struct JiraIssueRequest {
|
|
||||||
fields: JiraIssueFields,
|
|
||||||
update: Option<JiraIssueUpdate>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct JiraIssueFields {
|
|
||||||
project: Project,
|
|
||||||
summary: String,
|
|
||||||
description: String,
|
|
||||||
issuetype: IssueType,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
assignee: Option<Assignee>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
status: Option<Status>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
struct JiraIssueUpdate {
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
parent: Option<Vec<ParentUpdate>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
struct ParentUpdate {
|
|
||||||
add: Parent,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
struct Parent {
|
|
||||||
key: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct Assignee {
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct Project {
|
|
||||||
key: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct IssueType {
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct JiraResponse {
|
|
||||||
key: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct JiraSearchResponse {
|
|
||||||
issues: Vec<JiraIssue>,
|
|
||||||
total: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct JiraIssue {
|
|
||||||
key: String,
|
|
||||||
fields: JiraIssueResponseFields,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct JiraIssueResponseFields {
|
|
||||||
summary: String,
|
|
||||||
status: Status,
|
|
||||||
created: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct Status {
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_issue_template(
|
fn get_issue_template(
|
||||||
issue_metadata: &IssueMetadata,
|
issue_metadata: &IssueMetadata,
|
||||||
) -> Result<String, Box<dyn std::error::Error>> {
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
@ -216,37 +142,27 @@ async fn create_jira_issue(
|
||||||
title: &str,
|
title: &str,
|
||||||
description: &str,
|
description: &str,
|
||||||
metadata: &IssueMetadata,
|
metadata: &IssueMetadata,
|
||||||
) -> Result<JiraResponse, Box<dyn std::error::Error>> {
|
) -> Result<IssueCreateResponse, Box<dyn std::error::Error>> {
|
||||||
let client = reqwest::Client::new();
|
let client = JiraClient::from(config);
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
let fields = IssueFields {
|
||||||
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
|
|
||||||
|
|
||||||
let mut fields = JiraIssueFields {
|
|
||||||
project: Project {
|
project: Project {
|
||||||
key: project_key.to_string(),
|
key: project_key.to_string(),
|
||||||
},
|
},
|
||||||
summary: title.to_string(),
|
summary: title.to_string(),
|
||||||
description: description.to_string(),
|
description: description.to_string(),
|
||||||
issuetype: IssueType {
|
issue_type: IssueType {
|
||||||
name: "Task".to_string(),
|
name: "Task".to_string(),
|
||||||
},
|
},
|
||||||
assignee: None,
|
assignee: None,
|
||||||
status: None,
|
status: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add assignee if specified
|
|
||||||
if let Some(assignee) = &metadata.assignee {
|
|
||||||
fields.assignee = Some(Assignee {
|
|
||||||
name: assignee.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut update = None;
|
let mut update = None;
|
||||||
|
|
||||||
// Add parent if specified
|
// Add parent if specified
|
||||||
if let Some(parent_key) = &metadata.parent {
|
if let Some(parent_key) = &metadata.parent {
|
||||||
update = Some(JiraIssueUpdate {
|
update = Some(IssueUpdate {
|
||||||
parent: Some(vec![ParentUpdate {
|
parent: Some(vec![ParentUpdate {
|
||||||
add: Parent {
|
add: Parent {
|
||||||
key: parent_key.clone(),
|
key: parent_key.clone(),
|
||||||
|
@ -255,33 +171,24 @@ async fn create_jira_issue(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let issue = JiraIssueRequest { fields, update };
|
let issue = IssueCreateRequest { fields, update };
|
||||||
|
|
||||||
// println!("{:#?}", issue);
|
let create_cmd = IssueCreateCommand::new(issue);
|
||||||
// Ok(JiraResponse {
|
let issue_response = client.exec(create_cmd).await?;
|
||||||
// key: format!("{}-1234", project_key),
|
|
||||||
// })
|
|
||||||
|
|
||||||
let response = client
|
|
||||||
.post(format!("{}/rest/api/2/issue", config.url))
|
|
||||||
.basic_auth(&config.email, Some(&config.api_token))
|
|
||||||
.headers(headers)
|
|
||||||
.json(&issue)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
let error_text = response.text().await?;
|
|
||||||
return Err(format!("Failed to create issue: {}", error_text).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let issue_response = response.json::<JiraResponse>().await?;
|
|
||||||
|
|
||||||
// Update status if specified (requires a separate API call)
|
// Update status if specified (requires a separate API call)
|
||||||
if let Some(status) = &metadata.status {
|
if let Some(status) = &metadata.status {
|
||||||
update_issue_status(config, &issue_response.key, status).await?;
|
update_issue_status(config, &issue_response.key, status).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update assignee if specified
|
||||||
|
if let Some(_assignee) = &metadata.assignee {
|
||||||
|
// Need to look up users by username, because the api wants an account id
|
||||||
|
// fields.assignee = Some(Assignee {
|
||||||
|
// id: assignee.clone(),
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
Ok(issue_response)
|
Ok(issue_response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,52 +197,29 @@ async fn update_issue_status(
|
||||||
issue_key: &str,
|
issue_key: &str,
|
||||||
status: &str,
|
status: &str,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let client = reqwest::Client::new();
|
let client = JiraClient::from(config);
|
||||||
|
|
||||||
let transitions = client
|
let transitions_cmd = IssueTransitionsCommand::new(issue_key);
|
||||||
.get(format!(
|
let transitions = client.exec(transitions_cmd).await?;
|
||||||
"{}/rest/api/2/issue/{}/transitions",
|
|
||||||
config.url, issue_key
|
|
||||||
))
|
|
||||||
.basic_auth(&config.email, Some(&config.api_token))
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json::<serde_json::Value>()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Find the transition ID for the desired status
|
// Find the transition ID for the desired status
|
||||||
let transition_id = transitions["transitions"]
|
let transition_id = transitions
|
||||||
.as_array()
|
.transitions
|
||||||
.and_then(|t| {
|
.iter()
|
||||||
t.iter().find(|t| {
|
.find_map(|t| {
|
||||||
t["to"]["name"]
|
if t.to.name.eq_ignore_ascii_case(status) {
|
||||||
.as_str()
|
Some(t.id.clone())
|
||||||
.map_or(false, |s| s.eq_ignore_ascii_case(status))
|
} else {
|
||||||
})
|
None
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.and_then(|t| t["id"].as_str())
|
|
||||||
.ok_or_else(|| format!("No transition found for status: {}", status))?;
|
.ok_or_else(|| format!("No transition found for status: {}", status))?;
|
||||||
|
|
||||||
// Perform the transition
|
let transition_cmd = IssueTransitionsUpdateCommand::new(issue_key, &transition_id);
|
||||||
let transition_payload = serde_json::json!({
|
match client.exec(transition_cmd).await {
|
||||||
"transition": { "id": transition_id }
|
Ok(x) => Ok(x),
|
||||||
});
|
Err(reason) => Err(reason.into()),
|
||||||
|
|
||||||
let response = client
|
|
||||||
.post(format!(
|
|
||||||
"{}/rest/api/2/issue/{}/transitions",
|
|
||||||
config.url, issue_key
|
|
||||||
))
|
|
||||||
.basic_auth(&config.email, Some(&config.api_token))
|
|
||||||
.json(&transition_payload)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
return Err(format!("Failed to update status: {}", response.text().await?).into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
|
|
|
@ -1,6 +1,81 @@
|
||||||
use crate::cli::FormatMode;
|
use crate::cli::FormatMode;
|
||||||
use crate::jira_config::JiraConfig;
|
use crate::jira_config::JiraConfig;
|
||||||
use crate::types::issue::{display_issues_compact, display_issues_json, display_issues_pretty};
|
use crate::term::hyperlink;
|
||||||
|
use crossterm::style::{Color, Stylize};
|
||||||
|
use libjirac::client::commands::SearchCommand;
|
||||||
|
use libjirac::client::JiraClient;
|
||||||
|
use libjirac::entities::issue::JiraIssue;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
pub fn display_issues_pretty(issues: &[JiraIssue]) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("Found {} issues:", issues.len());
|
||||||
|
println!("{:-<80}", "");
|
||||||
|
|
||||||
|
for issue in issues {
|
||||||
|
let mut tw = tabwriter::TabWriter::new(vec![]);
|
||||||
|
writeln!(tw, "{}:\t{}", "Key".blue(), issue.key)?;
|
||||||
|
writeln!(tw, "{}:\t{}", "Summary".blue(), issue.fields.summary)?;
|
||||||
|
writeln!(tw, "{}:\t{}", "Status".blue(), issue.fields.status.name)?;
|
||||||
|
writeln!(
|
||||||
|
tw,
|
||||||
|
"{}:\t{}",
|
||||||
|
"Created".blue(),
|
||||||
|
issue.fields.created.with_timezone(&chrono::Local)
|
||||||
|
)?;
|
||||||
|
writeln!(
|
||||||
|
tw,
|
||||||
|
"{}:\t{}",
|
||||||
|
"Due Date".blue(),
|
||||||
|
match issue.fields.due_date {
|
||||||
|
None => "None".to_string(),
|
||||||
|
Some(x) => x.to_string(),
|
||||||
|
}
|
||||||
|
)?;
|
||||||
|
writeln!(
|
||||||
|
tw,
|
||||||
|
"{}",
|
||||||
|
&hyperlink(&issue.href, &"Open Issue".green().to_string())
|
||||||
|
)?;
|
||||||
|
|
||||||
|
tw.flush().unwrap();
|
||||||
|
|
||||||
|
let written = String::from_utf8(tw.into_inner().unwrap()).unwrap();
|
||||||
|
print!("{}", written);
|
||||||
|
println!("{:-<80}", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display_issues_compact(issues: &[JiraIssue]) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("Found {} issues:", issues.len());
|
||||||
|
println!("{:-<80}", "");
|
||||||
|
|
||||||
|
let mut tw = tabwriter::TabWriter::new(vec![]);
|
||||||
|
for issue in issues {
|
||||||
|
writeln!(
|
||||||
|
tw,
|
||||||
|
"{}:\t{}",
|
||||||
|
hyperlink(
|
||||||
|
&issue.href,
|
||||||
|
&issue.key.clone().blue().underline(Color::Blue).to_string()
|
||||||
|
),
|
||||||
|
issue.fields.summary.clone().green()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
tw.flush().unwrap();
|
||||||
|
let written = String::from_utf8(tw.into_inner().unwrap()).unwrap();
|
||||||
|
print!("{}", written);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display_issues_json(issues: &[JiraIssue]) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let j = serde_json::to_string_pretty(issues)?;
|
||||||
|
println!("{}", j);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn exec(output: FormatMode, jql: &str) -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn exec(output: FormatMode, jql: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let config = JiraConfig::load().map_err(|e| format!("Configuration error: {}", e))?;
|
let config = JiraConfig::load().map_err(|e| format!("Configuration error: {}", e))?;
|
||||||
|
@ -8,13 +83,10 @@ pub async fn exec(output: FormatMode, jql: &str) -> Result<(), Box<dyn std::erro
|
||||||
println!("Searching for issues...");
|
println!("Searching for issues...");
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = match crate::jql::run(&config, jql).await {
|
let client = JiraClient::from(&config);
|
||||||
Ok(x) => x,
|
|
||||||
Err(reason) => {
|
let jql_cmd = SearchCommand::new(jql);
|
||||||
eprintln!("Error fetching issues: {}", reason);
|
let result = client.exec(jql_cmd).await?;
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match (output, result.issues.is_empty()) {
|
match (output, result.issues.is_empty()) {
|
||||||
(FormatMode::Pretty, false) => {
|
(FormatMode::Pretty, false) => {
|
||||||
|
|
|
@ -1,32 +1,12 @@
|
||||||
use crate::cli::FormatMode;
|
use crate::cli::FormatMode;
|
||||||
use crate::jira_config::JiraConfig;
|
use crate::jira_config::JiraConfig;
|
||||||
use crate::term::hyperlink;
|
use crate::term::hyperlink;
|
||||||
use crate::types::issue::JiraIssue;
|
|
||||||
use crossterm::style::{Color, Stylize};
|
use crossterm::style::{Color, Stylize};
|
||||||
|
use libjirac::client::commands::{SearchCommand, SelfCommand};
|
||||||
|
use libjirac::client::JiraClient;
|
||||||
|
use libjirac::entities::issue::JiraIssue;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
async fn fetch_issue(
|
|
||||||
config: &JiraConfig,
|
|
||||||
href: &str,
|
|
||||||
) -> Result<JiraIssue, Box<dyn std::error::Error>> {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
|
|
||||||
let response = client
|
|
||||||
.get(href)
|
|
||||||
.basic_auth(&config.email, Some(&config.api_token))
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
let error_text = response.text().await?;
|
|
||||||
return Err(format!("Failed to fetch issue: {}", error_text).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let issue_response = response.json::<JiraIssue>().await?;
|
|
||||||
|
|
||||||
Ok(issue_response)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pretty_print(issue: &JiraIssue) -> Result<(), Box<dyn std::error::Error>> {
|
fn pretty_print(issue: &JiraIssue) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("\n== Title {:=<71}", "");
|
println!("\n== Title {:=<71}", "");
|
||||||
println!(
|
println!(
|
||||||
|
@ -124,13 +104,10 @@ pub async fn exec(output: FormatMode, issue_key: &str) -> Result<(), Box<dyn std
|
||||||
|
|
||||||
let jql = format!("key = '{}'", issue_key);
|
let jql = format!("key = '{}'", issue_key);
|
||||||
|
|
||||||
let matched_issues = match crate::jql::run(&config, &jql).await {
|
let client = JiraClient::from(&config);
|
||||||
Ok(x) => x,
|
|
||||||
Err(reason) => {
|
let jql_cmd = SearchCommand::new(&jql);
|
||||||
eprintln!("Error fetching issue: {}", reason);
|
let matched_issues = client.exec(jql_cmd).await?;
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let matched_issue = match matched_issues.issues.first() {
|
let matched_issue = match matched_issues.issues.first() {
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
|
@ -140,7 +117,8 @@ pub async fn exec(output: FormatMode, issue_key: &str) -> Result<(), Box<dyn std
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let fetched_issue = fetch_issue(&config, &matched_issue.href).await?;
|
let self_cmd = SelfCommand::<JiraIssue>::new(&matched_issue.href);
|
||||||
|
let fetched_issue = client.exec(self_cmd).await?;
|
||||||
|
|
||||||
match output {
|
match output {
|
||||||
FormatMode::Pretty => pretty_print(&fetched_issue)?,
|
FormatMode::Pretty => pretty_print(&fetched_issue)?,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use config::{Config, ConfigError, File};
|
use config::{Config, ConfigError, File};
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
|
use libjirac::client::JiraClient;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
@ -66,3 +67,9 @@ impl JiraConfig {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&JiraConfig> for JiraClient {
|
||||||
|
fn from(value: &JiraConfig) -> Self {
|
||||||
|
Self::new(&value.url, &value.email, &value.api_token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
use crate::jira_config::JiraConfig;
|
|
||||||
use crate::types::issue::JiraIssue;
|
|
||||||
use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct JiraSearchResponse {
|
|
||||||
pub issues: Vec<JiraIssue>,
|
|
||||||
pub total: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run(
|
|
||||||
config: &JiraConfig,
|
|
||||||
jql: &str,
|
|
||||||
) -> Result<JiraSearchResponse, Box<dyn std::error::Error>> {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
|
||||||
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
|
|
||||||
|
|
||||||
let query = [("jql", jql)];
|
|
||||||
|
|
||||||
let response = client
|
|
||||||
.get(format!("{}/rest/api/2/search", config.url))
|
|
||||||
.basic_auth(&config.email, Some(&config.api_token))
|
|
||||||
.headers(headers)
|
|
||||||
.query(&query)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
let error_text = response.text().await?;
|
|
||||||
return Err(format!("Failed to fetch issues: {}", error_text).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(response.json::<JiraSearchResponse>().await?)
|
|
||||||
}
|
|
|
@ -1,9 +1,7 @@
|
||||||
mod cli;
|
mod cli;
|
||||||
mod cmd;
|
mod cmd;
|
||||||
mod jira_config;
|
mod jira_config;
|
||||||
mod jql;
|
|
||||||
mod term;
|
mod term;
|
||||||
mod types;
|
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::{Cli, Commands};
|
use cli::{Cli, Commands};
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
pub mod issue;
|
|
|
@ -1,153 +0,0 @@
|
||||||
use crate::term::hyperlink;
|
|
||||||
use crossterm::style::{Color, Stylize};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct JiraIssue {
|
|
||||||
pub key: String,
|
|
||||||
#[serde(rename = "self")]
|
|
||||||
pub href: String,
|
|
||||||
pub fields: JiraIssueResponseFields,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct JiraIssueResponseFields {
|
|
||||||
pub summary: String,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub status: Status,
|
|
||||||
pub created: chrono::DateTime<chrono::Utc>,
|
|
||||||
pub priority: Priority,
|
|
||||||
pub assignee: Person,
|
|
||||||
pub reporter: Person,
|
|
||||||
pub creator: Person,
|
|
||||||
#[serde(rename = "duedate")]
|
|
||||||
pub due_date: Option<chrono::NaiveDate>,
|
|
||||||
pub comment: Option<Comments>,
|
|
||||||
pub votes: Votes,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Status {
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Priority {
|
|
||||||
pub name: String,
|
|
||||||
pub id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct Person {
|
|
||||||
#[serde(rename = "self")]
|
|
||||||
pub href: String,
|
|
||||||
#[serde(rename = "displayName")]
|
|
||||||
pub display_name: String,
|
|
||||||
#[serde(rename = "accountId")]
|
|
||||||
pub account_id: String,
|
|
||||||
#[serde(rename = "emailAddress")]
|
|
||||||
pub email_address: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct Comments {
|
|
||||||
pub total: u32,
|
|
||||||
#[serde(rename = "maxResults")]
|
|
||||||
pub max_results: u32,
|
|
||||||
#[serde(rename = "startAt")]
|
|
||||||
pub start_at: u32,
|
|
||||||
pub comments: Vec<Comment>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
||||||
pub struct Comment {
|
|
||||||
#[serde(rename = "self")]
|
|
||||||
pub href: String,
|
|
||||||
pub id: String,
|
|
||||||
pub author: Person,
|
|
||||||
pub body: String,
|
|
||||||
#[serde(rename = "updateAuthor")]
|
|
||||||
pub update_author: Person,
|
|
||||||
pub created: chrono::DateTime<chrono::Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Votes {
|
|
||||||
#[serde(rename = "self")]
|
|
||||||
pub href: String,
|
|
||||||
#[serde(rename = "votes")]
|
|
||||||
pub count: i32,
|
|
||||||
#[serde(rename = "hasVoted")]
|
|
||||||
pub has_voted: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn display_issues_pretty(issues: &[JiraIssue]) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
println!("Found {} issues:", issues.len());
|
|
||||||
println!("{:-<80}", "");
|
|
||||||
|
|
||||||
for issue in issues {
|
|
||||||
let mut tw = tabwriter::TabWriter::new(vec![]);
|
|
||||||
writeln!(tw, "{}:\t{}", "Key".blue(), issue.key)?;
|
|
||||||
writeln!(tw, "{}:\t{}", "Summary".blue(), issue.fields.summary)?;
|
|
||||||
writeln!(tw, "{}:\t{}", "Status".blue(), issue.fields.status.name)?;
|
|
||||||
writeln!(
|
|
||||||
tw,
|
|
||||||
"{}:\t{}",
|
|
||||||
"Created".blue(),
|
|
||||||
issue.fields.created.with_timezone(&chrono::Local)
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
tw,
|
|
||||||
"{}:\t{}",
|
|
||||||
"Due Date".blue(),
|
|
||||||
match issue.fields.due_date {
|
|
||||||
None => "None".to_string(),
|
|
||||||
Some(x) => x.to_string(),
|
|
||||||
}
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
tw,
|
|
||||||
"{}",
|
|
||||||
&hyperlink(&issue.href, &"Open Issue".green().to_string())
|
|
||||||
)?;
|
|
||||||
|
|
||||||
tw.flush().unwrap();
|
|
||||||
|
|
||||||
let written = String::from_utf8(tw.into_inner().unwrap()).unwrap();
|
|
||||||
print!("{}", written);
|
|
||||||
println!("{:-<80}", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn display_issues_compact(issues: &[JiraIssue]) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
println!("Found {} issues:", issues.len());
|
|
||||||
println!("{:-<80}", "");
|
|
||||||
|
|
||||||
let mut tw = tabwriter::TabWriter::new(vec![]);
|
|
||||||
for issue in issues {
|
|
||||||
writeln!(
|
|
||||||
tw,
|
|
||||||
"{}:\t{}",
|
|
||||||
hyperlink(
|
|
||||||
&issue.href,
|
|
||||||
&issue.key.clone().blue().underline(Color::Blue).to_string()
|
|
||||||
),
|
|
||||||
issue.fields.summary.clone().green()
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
tw.flush().unwrap();
|
|
||||||
let written = String::from_utf8(tw.into_inner().unwrap()).unwrap();
|
|
||||||
print!("{}", written);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn display_issues_json(issues: &[JiraIssue]) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let j = serde_json::to_string_pretty(issues)?;
|
|
||||||
println!("{}", j);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
13
crates/libjirac/Cargo.toml
Normal file
13
crates/libjirac/Cargo.toml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "libjirac"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
chrono = { workspace = true }
|
||||||
|
reqwest = { workspace = true }
|
||||||
|
http = { workspace = true }
|
||||||
|
url = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
155
crates/libjirac/src/client.rs
Normal file
155
crates/libjirac/src/client.rs
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
pub mod commands;
|
||||||
|
|
||||||
|
use http::header::CONTENT_TYPE;
|
||||||
|
use http::{HeaderMap, HeaderValue};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use thiserror::Error;
|
||||||
|
use url::{ParseError, Url};
|
||||||
|
|
||||||
|
pub struct JiraClient {
|
||||||
|
pub url: String,
|
||||||
|
pub email: String,
|
||||||
|
pub api_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait JiraCommand {
|
||||||
|
type TResponse: DeserializeOwned + Clone;
|
||||||
|
type TPayload: Serialize;
|
||||||
|
const REQUEST_TYPE: JiraRequestType;
|
||||||
|
fn endpoint(&self) -> String;
|
||||||
|
fn request_body(&self) -> Option<&Self::TPayload>;
|
||||||
|
fn query_params(&self) -> Option<Vec<(String, String)>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum JiraRequestType {
|
||||||
|
Read,
|
||||||
|
Create,
|
||||||
|
Update,
|
||||||
|
Delete,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<JiraRequestType> for http::Method {
|
||||||
|
fn from(value: JiraRequestType) -> Self {
|
||||||
|
match value {
|
||||||
|
JiraRequestType::Read => Self::GET,
|
||||||
|
JiraRequestType::Create => Self::POST,
|
||||||
|
JiraRequestType::Update => Self::PATCH,
|
||||||
|
JiraRequestType::Delete => Self::DELETE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum JiraClientError {
|
||||||
|
#[error(transparent)]
|
||||||
|
UrlParseError(#[from] ParseError),
|
||||||
|
#[error(transparent)]
|
||||||
|
ReqwestError(#[from] reqwest::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
SerdeError(#[from] serde_json::Error),
|
||||||
|
#[error("API Error: {0}")]
|
||||||
|
ApiError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JiraClient {
|
||||||
|
pub fn new(url: &str, email: &str, api_token: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
url: url.to_string(),
|
||||||
|
email: email.to_string(),
|
||||||
|
api_token: api_token.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn exec<TCommand>(
|
||||||
|
&self,
|
||||||
|
cmd: TCommand,
|
||||||
|
) -> Result<TCommand::TResponse, JiraClientError>
|
||||||
|
where
|
||||||
|
TCommand: JiraCommand + Debug,
|
||||||
|
{
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
|
||||||
|
|
||||||
|
let url = self.construct_url(&cmd.endpoint())?;
|
||||||
|
|
||||||
|
let mut request_builder = client
|
||||||
|
.request(TCommand::REQUEST_TYPE.into(), url)
|
||||||
|
.basic_auth(&self.email, Some(&self.api_token))
|
||||||
|
.headers(headers);
|
||||||
|
|
||||||
|
if let Some(params) = cmd.query_params() {
|
||||||
|
request_builder = request_builder.query(¶ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(body) = cmd.request_body() {
|
||||||
|
request_builder = request_builder.json(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = match request_builder.send().await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(reason) => return Err(JiraClientError::ReqwestError(reason)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let error_text = response.text().await?;
|
||||||
|
return Err(JiraClientError::ApiError(error_text));
|
||||||
|
}
|
||||||
|
|
||||||
|
let plain = match response.bytes().await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(reason) => return Err(JiraClientError::ReqwestError(reason)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = match is_unit::<TCommand::TResponse>() {
|
||||||
|
true => b"null",
|
||||||
|
false => plain.as_ref(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match serde_json::from_slice::<TCommand::TResponse>(value) {
|
||||||
|
Ok(x) => Ok(x),
|
||||||
|
Err(reason) => Err(JiraClientError::SerdeError(reason)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn construct_url(&self, endpoint: &str) -> Result<String, JiraClientError> {
|
||||||
|
let mut url = match Url::parse(&self.url) {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(reason) => {
|
||||||
|
return Err(JiraClientError::UrlParseError(reason));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
url.set_path(endpoint);
|
||||||
|
|
||||||
|
Ok(url.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn is_unit<T>() -> bool {
|
||||||
|
size_of::<T>() == 0 && align_of::<T>() == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_unit_type() {
|
||||||
|
assert!(is_unit::<()>());
|
||||||
|
assert!(!is_unit::<String>());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_unit_type() {
|
||||||
|
let value = match is_unit::<()>() {
|
||||||
|
true => b"null".as_ref(),
|
||||||
|
false => b"{}".as_ref(),
|
||||||
|
};
|
||||||
|
|
||||||
|
serde_json::from_slice::<()>(value).unwrap();
|
||||||
|
}
|
||||||
|
}
|
11
crates/libjirac/src/client/commands.rs
Normal file
11
crates/libjirac/src/client/commands.rs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
mod issue_create_command;
|
||||||
|
mod issue_transitions_command;
|
||||||
|
mod issue_transitions_update_command;
|
||||||
|
mod search_command;
|
||||||
|
mod self_command;
|
||||||
|
|
||||||
|
pub use issue_create_command::IssueCreateCommand;
|
||||||
|
pub use issue_transitions_command::IssueTransitionsCommand;
|
||||||
|
pub use issue_transitions_update_command::IssueTransitionsUpdateCommand;
|
||||||
|
pub use search_command::SearchCommand;
|
||||||
|
pub use self_command::SelfCommand;
|
31
crates/libjirac/src/client/commands/issue_create_command.rs
Normal file
31
crates/libjirac/src/client/commands/issue_create_command.rs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
use crate::client::{JiraCommand, JiraRequestType};
|
||||||
|
use crate::entities::issue_request::{IssueCreateRequest, IssueCreateResponse};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct IssueCreateCommand {
|
||||||
|
issue: IssueCreateRequest,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IssueCreateCommand {
|
||||||
|
pub fn new(issue: IssueCreateRequest) -> Self {
|
||||||
|
Self { issue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JiraCommand for IssueCreateCommand {
|
||||||
|
type TResponse = IssueCreateResponse;
|
||||||
|
type TPayload = IssueCreateRequest;
|
||||||
|
const REQUEST_TYPE: JiraRequestType = JiraRequestType::Create;
|
||||||
|
|
||||||
|
fn endpoint(&self) -> String {
|
||||||
|
"/rest/api/2/issue".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_body(&self) -> Option<&Self::TPayload> {
|
||||||
|
Some(&self.issue)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_params(&self) -> Option<Vec<(String, String)>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
use crate::client::{JiraCommand, JiraRequestType};
|
||||||
|
use crate::entities::transitions::Transitions;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct IssueTransitionsCommand {
|
||||||
|
issue_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IssueTransitionsCommand {
|
||||||
|
pub fn new(issue_key: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
issue_key: issue_key.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JiraCommand for IssueTransitionsCommand {
|
||||||
|
type TResponse = Transitions;
|
||||||
|
type TPayload = ();
|
||||||
|
const REQUEST_TYPE: JiraRequestType = JiraRequestType::Read;
|
||||||
|
|
||||||
|
fn endpoint(&self) -> String {
|
||||||
|
format!("/rest/api/3/issue/{}/transitions", self.issue_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_body(&self) -> Option<&Self::TPayload> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_params(&self) -> Option<Vec<(String, String)>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
use crate::client::{JiraCommand, JiraRequestType};
|
||||||
|
use crate::entities::transitions::{IssueTransitionUpdatePayload, IssueTransitionUpdatePayloadId};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct IssueTransitionsUpdateCommand {
|
||||||
|
pub issue_key: String,
|
||||||
|
pub transition: IssueTransitionUpdatePayload,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IssueTransitionsUpdateCommand {
|
||||||
|
pub fn new(issue_key: &str, transition_id: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
issue_key: issue_key.to_string(),
|
||||||
|
transition: IssueTransitionUpdatePayload {
|
||||||
|
transition: IssueTransitionUpdatePayloadId {
|
||||||
|
id: transition_id.to_string(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JiraCommand for IssueTransitionsUpdateCommand {
|
||||||
|
type TResponse = ();
|
||||||
|
type TPayload = IssueTransitionUpdatePayload;
|
||||||
|
const REQUEST_TYPE: JiraRequestType = JiraRequestType::Create;
|
||||||
|
|
||||||
|
fn endpoint(&self) -> String {
|
||||||
|
format!("/rest/api/3/issue/{}/transitions", self.issue_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_body(&self) -> Option<&Self::TPayload> {
|
||||||
|
Some(&self.transition)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_params(&self) -> Option<Vec<(String, String)>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
35
crates/libjirac/src/client/commands/search_command.rs
Normal file
35
crates/libjirac/src/client/commands/search_command.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
use crate::client::{JiraCommand, JiraRequestType};
|
||||||
|
use crate::entities::search::JiraSearchResponse;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SearchCommand {
|
||||||
|
jql: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchCommand {
|
||||||
|
pub fn new(jql: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
jql: jql.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JiraCommand for SearchCommand {
|
||||||
|
type TResponse = JiraSearchResponse;
|
||||||
|
type TPayload = ();
|
||||||
|
const REQUEST_TYPE: JiraRequestType = JiraRequestType::Read;
|
||||||
|
|
||||||
|
fn endpoint(&self) -> String {
|
||||||
|
"/rest/api/2/search".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_body(&self) -> Option<&Self::TPayload> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_params(&self) -> Option<Vec<(String, String)>> {
|
||||||
|
let params = vec![("jql".to_string(), self.jql.clone())];
|
||||||
|
|
||||||
|
Some(params)
|
||||||
|
}
|
||||||
|
}
|
46
crates/libjirac/src/client/commands/self_command.rs
Normal file
46
crates/libjirac/src/client/commands/self_command.rs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
use crate::client::{JiraCommand, JiraRequestType};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SelfCommand<T>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned + Clone,
|
||||||
|
{
|
||||||
|
href: String,
|
||||||
|
_marker: std::marker::PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> SelfCommand<T>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned + Clone,
|
||||||
|
{
|
||||||
|
pub fn new(href: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
href: href.to_owned(),
|
||||||
|
_marker: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> JiraCommand for SelfCommand<T>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned + Clone,
|
||||||
|
{
|
||||||
|
type TResponse = T;
|
||||||
|
type TPayload = ();
|
||||||
|
const REQUEST_TYPE: JiraRequestType = JiraRequestType::Read;
|
||||||
|
|
||||||
|
fn endpoint(&self) -> String {
|
||||||
|
let url = Url::parse(&self.href).unwrap();
|
||||||
|
url.path().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_body(&self) -> Option<&Self::TPayload> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_params(&self) -> Option<Vec<(String, String)>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
4
crates/libjirac/src/entities.rs
Normal file
4
crates/libjirac/src/entities.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod issue;
|
||||||
|
pub mod issue_request;
|
||||||
|
pub mod search;
|
||||||
|
pub mod transitions;
|
80
crates/libjirac/src/entities/issue.rs
Normal file
80
crates/libjirac/src/entities/issue.rs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct JiraIssue {
|
||||||
|
pub key: String,
|
||||||
|
#[serde(rename = "self")]
|
||||||
|
pub href: String,
|
||||||
|
pub fields: JiraIssueResponseFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct JiraIssueResponseFields {
|
||||||
|
pub summary: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub status: Status,
|
||||||
|
pub created: chrono::DateTime<chrono::Utc>,
|
||||||
|
pub priority: Priority,
|
||||||
|
pub assignee: Person,
|
||||||
|
pub reporter: Person,
|
||||||
|
pub creator: Person,
|
||||||
|
#[serde(rename = "duedate")]
|
||||||
|
pub due_date: Option<chrono::NaiveDate>,
|
||||||
|
pub comment: Option<Comments>,
|
||||||
|
pub votes: Votes,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct Status {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct Priority {
|
||||||
|
pub name: String,
|
||||||
|
pub id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct Person {
|
||||||
|
#[serde(rename = "self")]
|
||||||
|
pub href: String,
|
||||||
|
#[serde(rename = "displayName")]
|
||||||
|
pub display_name: String,
|
||||||
|
#[serde(rename = "accountId")]
|
||||||
|
pub account_id: String,
|
||||||
|
#[serde(rename = "emailAddress")]
|
||||||
|
pub email_address: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct Comments {
|
||||||
|
pub total: u32,
|
||||||
|
#[serde(rename = "maxResults")]
|
||||||
|
pub max_results: u32,
|
||||||
|
#[serde(rename = "startAt")]
|
||||||
|
pub start_at: u32,
|
||||||
|
pub comments: Vec<Comment>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct Comment {
|
||||||
|
#[serde(rename = "self")]
|
||||||
|
pub href: String,
|
||||||
|
pub id: String,
|
||||||
|
pub author: Person,
|
||||||
|
pub body: String,
|
||||||
|
#[serde(rename = "updateAuthor")]
|
||||||
|
pub update_author: Person,
|
||||||
|
pub created: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct Votes {
|
||||||
|
#[serde(rename = "self")]
|
||||||
|
pub href: String,
|
||||||
|
#[serde(rename = "votes")]
|
||||||
|
pub count: i32,
|
||||||
|
#[serde(rename = "hasVoted")]
|
||||||
|
pub has_voted: bool,
|
||||||
|
}
|
61
crates/libjirac/src/entities/issue_request.rs
Normal file
61
crates/libjirac/src/entities/issue_request.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct IssueCreateResponse {
|
||||||
|
pub key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct IssueCreateRequest {
|
||||||
|
pub fields: IssueFields,
|
||||||
|
pub update: Option<IssueUpdate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct IssueUpdate {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub parent: Option<Vec<ParentUpdate>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct ParentUpdate {
|
||||||
|
pub add: Parent,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct Parent {
|
||||||
|
pub key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct IssueFields {
|
||||||
|
pub project: Project,
|
||||||
|
pub summary: String,
|
||||||
|
pub description: String,
|
||||||
|
#[serde(rename = "issuetype")]
|
||||||
|
pub issue_type: IssueType,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub assignee: Option<Assignee>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub status: Option<Status>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Assignee {
|
||||||
|
pub id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Project {
|
||||||
|
pub key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct IssueType {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Status {
|
||||||
|
pub name: String,
|
||||||
|
}
|
8
crates/libjirac/src/entities/search.rs
Normal file
8
crates/libjirac/src/entities/search.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
use crate::entities::issue::JiraIssue;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct JiraSearchResponse {
|
||||||
|
pub issues: Vec<JiraIssue>,
|
||||||
|
pub total: u32,
|
||||||
|
}
|
29
crates/libjirac/src/entities/transitions.rs
Normal file
29
crates/libjirac/src/entities/transitions.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct Transitions {
|
||||||
|
pub transitions: Vec<Transition>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct Transition {
|
||||||
|
pub id: String,
|
||||||
|
pub to: TransitionTarget,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct TransitionTarget {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct IssueTransitionUpdatePayload {
|
||||||
|
pub transition: IssueTransitionUpdatePayloadId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct IssueTransitionUpdatePayloadId {
|
||||||
|
pub id: String,
|
||||||
|
}
|
2
crates/libjirac/src/lib.rs
Normal file
2
crates/libjirac/src/lib.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod client;
|
||||||
|
pub mod entities;
|
Loading…
Reference in a new issue