Compare commits

..

4 commits

25 changed files with 700 additions and 388 deletions

15
Cargo.lock generated
View file

@ -939,8 +939,8 @@ dependencies = [
"crossterm",
"directories",
"gray_matter",
"libjirac",
"open",
"reqwest",
"serde",
"serde_json",
"tabwriter",
@ -976,6 +976,19 @@ version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libjirac"
version = "0.1.0"
dependencies = [
"chrono",
"http",
"reqwest",
"serde",
"serde_json",
"thiserror",
"url",
]
[[package]]
name = "libredox"
version = "0.1.3"

View file

@ -5,6 +5,8 @@ resolver = "2"
[workspace.dependencies]
clap = { version = "4.4", features = ["derive"] }
reqwest = { version = "0.12", features = ["json"] }
http = "1"
url = "2"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
@ -17,3 +19,4 @@ open = "5.2"
chrono = { version = "0.4", features = ["serde"] }
tabwriter = "1.4"
crossterm = "0.28"
thiserror = "2"

View file

@ -4,8 +4,9 @@ version = "0.1.0"
edition = "2021"
[dependencies]
libjirac = { path = "../libjirac" }
clap = { workspace = true }
reqwest = { workspace = true }
tokio = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }

View file

@ -1,7 +1,11 @@
use crate::jira_config::JiraConfig;
use gray_matter::engine::TOML;
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 std::collections::HashMap;
use std::fs;
@ -23,84 +27,6 @@ struct IssueMetadata {
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(
issue_metadata: &IssueMetadata,
) -> Result<String, Box<dyn std::error::Error>> {
@ -216,37 +142,27 @@ async fn create_jira_issue(
title: &str,
description: &str,
metadata: &IssueMetadata,
) -> Result<JiraResponse, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
) -> Result<IssueCreateResponse, Box<dyn std::error::Error>> {
let client = JiraClient::from(config);
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
let mut fields = JiraIssueFields {
let fields = IssueFields {
project: Project {
key: project_key.to_string(),
},
summary: title.to_string(),
description: description.to_string(),
issuetype: IssueType {
issue_type: IssueType {
name: "Task".to_string(),
},
assignee: None,
status: None,
};
// Add assignee if specified
if let Some(assignee) = &metadata.assignee {
fields.assignee = Some(Assignee {
name: assignee.clone(),
});
}
let mut update = None;
// Add parent if specified
if let Some(parent_key) = &metadata.parent {
update = Some(JiraIssueUpdate {
update = Some(IssueUpdate {
parent: Some(vec![ParentUpdate {
add: Parent {
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);
// Ok(JiraResponse {
// 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?;
let create_cmd = IssueCreateCommand::new(issue);
let issue_response = client.exec(create_cmd).await?;
// Update status if specified (requires a separate API call)
if let Some(status) = &metadata.status {
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)
}
@ -290,52 +197,29 @@ async fn update_issue_status(
issue_key: &str,
status: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let client = JiraClient::from(config);
let transitions = client
.get(format!(
"{}/rest/api/2/issue/{}/transitions",
config.url, issue_key
))
.basic_auth(&config.email, Some(&config.api_token))
.send()
.await?
.json::<serde_json::Value>()
.await?;
let transitions_cmd = IssueTransitionsCommand::new(issue_key);
let transitions = client.exec(transitions_cmd).await?;
// Find the transition ID for the desired status
let transition_id = transitions["transitions"]
.as_array()
.and_then(|t| {
t.iter().find(|t| {
t["to"]["name"]
.as_str()
.map_or(false, |s| s.eq_ignore_ascii_case(status))
})
let transition_id = transitions
.transitions
.iter()
.find_map(|t| {
if t.to.name.eq_ignore_ascii_case(status) {
Some(t.id.clone())
} else {
None
}
})
.and_then(|t| t["id"].as_str())
.ok_or_else(|| format!("No transition found for status: {}", status))?;
// Perform the transition
let transition_payload = serde_json::json!({
"transition": { "id": transition_id }
});
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());
let transition_cmd = IssueTransitionsUpdateCommand::new(issue_key, &transition_id);
match client.exec(transition_cmd).await {
Ok(x) => Ok(x),
Err(reason) => Err(reason.into()),
}
Ok(())
}
pub async fn create(

View file

@ -1,6 +1,81 @@
use crate::cli::FormatMode;
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>> {
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...");
}
let result = match crate::jql::run(&config, jql).await {
Ok(x) => x,
Err(reason) => {
eprintln!("Error fetching issues: {}", reason);
std::process::exit(1);
}
};
let client = JiraClient::from(&config);
let jql_cmd = SearchCommand::new(jql);
let result = client.exec(jql_cmd).await?;
match (output, result.issues.is_empty()) {
(FormatMode::Pretty, false) => {

View file

@ -1,32 +1,12 @@
use crate::cli::FormatMode;
use crate::jira_config::JiraConfig;
use crate::term::hyperlink;
use crate::types::issue::JiraIssue;
use crossterm::style::{Color, Stylize};
use libjirac::client::commands::{SearchCommand, SelfCommand};
use libjirac::client::JiraClient;
use libjirac::entities::issue::JiraIssue;
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>> {
println!("\n== Title {:=<71}", "");
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 matched_issues = match crate::jql::run(&config, &jql).await {
Ok(x) => x,
Err(reason) => {
eprintln!("Error fetching issue: {}", reason);
std::process::exit(1);
}
};
let client = JiraClient::from(&config);
let jql_cmd = SearchCommand::new(&jql);
let matched_issues = client.exec(jql_cmd).await?;
let matched_issue = match matched_issues.issues.first() {
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 {
FormatMode::Pretty => pretty_print(&fetched_issue)?,

View file

@ -1,5 +1,6 @@
use config::{Config, ConfigError, File};
use directories::ProjectDirs;
use libjirac::client::JiraClient;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
@ -66,3 +67,9 @@ impl JiraConfig {
Ok(())
}
}
impl From<&JiraConfig> for JiraClient {
fn from(value: &JiraConfig) -> Self {
Self::new(&value.url, &value.email, &value.api_token)
}
}

View file

@ -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?)
}

View file

@ -1,9 +1,7 @@
mod cli;
mod cmd;
mod jira_config;
mod jql;
mod term;
mod types;
use clap::Parser;
use cli::{Cli, Commands};

View file

@ -1 +0,0 @@
pub mod issue;

View file

@ -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(())
}

View 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 }

View 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(&params);
}
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();
}
}

View 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;

View 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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View 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)
}
}

View 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
}
}

View file

@ -0,0 +1,4 @@
pub mod issue;
pub mod issue_request;
pub mod search;
pub mod transitions;

View 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,
}

View 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,
}

View 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,
}

View 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,
}

View file

@ -0,0 +1,2 @@
pub mod client;
pub mod entities;