From 5b47f3d5e9538c9808876020d79f652d586fba5f Mon Sep 17 00:00:00 2001 From: Daan Boerlage Date: Tue, 21 Jan 2025 22:36:35 +0100 Subject: [PATCH 1/4] Add libjirac --- Cargo.lock | 5 +++++ crates/jirac/Cargo.toml | 2 ++ crates/libjirac/Cargo.toml | 6 ++++++ crates/libjirac/src/lib.rs | 14 ++++++++++++++ 4 files changed, 27 insertions(+) create mode 100644 crates/libjirac/Cargo.toml create mode 100644 crates/libjirac/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 3cac3c8..8d3ec02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -939,6 +939,7 @@ dependencies = [ "crossterm", "directories", "gray_matter", + "libjirac", "open", "reqwest", "serde", @@ -976,6 +977,10 @@ version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +[[package]] +name = "libjirac" +version = "0.1.0" + [[package]] name = "libredox" version = "0.1.3" diff --git a/crates/jirac/Cargo.toml b/crates/jirac/Cargo.toml index b38327d..6c0c0e2 100644 --- a/crates/jirac/Cargo.toml +++ b/crates/jirac/Cargo.toml @@ -4,6 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] +libjirac = { path = "../libjirac" } + clap = { workspace = true } reqwest = { workspace = true } tokio = { workspace = true } diff --git a/crates/libjirac/Cargo.toml b/crates/libjirac/Cargo.toml new file mode 100644 index 0000000..3e24ca4 --- /dev/null +++ b/crates/libjirac/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "libjirac" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/crates/libjirac/src/lib.rs b/crates/libjirac/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/crates/libjirac/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} From 6102233bc585a07a80d963337028df5d6435f622 Mon Sep 17 00:00:00 2001 From: Daan Boerlage Date: Tue, 21 Jan 2025 22:48:30 +0100 Subject: [PATCH 2/4] Move the issue type to libjirac --- Cargo.lock | 4 + crates/jirac/src/cmd/search.rs | 75 ++++++++- crates/jirac/src/cmd/view.rs | 2 +- crates/jirac/src/jql.rs | 2 +- crates/jirac/src/main.rs | 1 - crates/jirac/src/types/issue.rs | 153 ------------------ crates/libjirac/Cargo.toml | 2 + .../src/types.rs => libjirac/src/entities.rs} | 0 crates/libjirac/src/entities/issue.rs | 80 +++++++++ crates/libjirac/src/lib.rs | 15 +- 10 files changed, 163 insertions(+), 171 deletions(-) delete mode 100644 crates/jirac/src/types/issue.rs rename crates/{jirac/src/types.rs => libjirac/src/entities.rs} (100%) create mode 100644 crates/libjirac/src/entities/issue.rs diff --git a/Cargo.lock b/Cargo.lock index 8d3ec02..6a7807c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -980,6 +980,10 @@ checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libjirac" version = "0.1.0" +dependencies = [ + "chrono", + "serde", +] [[package]] name = "libredox" diff --git a/crates/jirac/src/cmd/search.rs b/crates/jirac/src/cmd/search.rs index e2dd050..fd8dff2 100644 --- a/crates/jirac/src/cmd/search.rs +++ b/crates/jirac/src/cmd/search.rs @@ -1,6 +1,79 @@ 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::entities::issue::JiraIssue; +use std::io::Write; + +pub fn display_issues_pretty(issues: &[JiraIssue]) -> Result<(), Box> { + 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> { + 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> { + let j = serde_json::to_string_pretty(issues)?; + println!("{}", j); + Ok(()) +} pub async fn exec(output: FormatMode, jql: &str) -> Result<(), Box> { let config = JiraConfig::load().map_err(|e| format!("Configuration error: {}", e))?; diff --git a/crates/jirac/src/cmd/view.rs b/crates/jirac/src/cmd/view.rs index 752fe5d..b868fff 100644 --- a/crates/jirac/src/cmd/view.rs +++ b/crates/jirac/src/cmd/view.rs @@ -1,8 +1,8 @@ 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::entities::issue::JiraIssue; use std::io::Write; async fn fetch_issue( diff --git a/crates/jirac/src/jql.rs b/crates/jirac/src/jql.rs index a281f3d..de6fc6a 100644 --- a/crates/jirac/src/jql.rs +++ b/crates/jirac/src/jql.rs @@ -1,5 +1,5 @@ use crate::jira_config::JiraConfig; -use crate::types::issue::JiraIssue; +use libjirac::entities::issue::JiraIssue; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use serde::Deserialize; diff --git a/crates/jirac/src/main.rs b/crates/jirac/src/main.rs index 199f533..a2cd3c1 100644 --- a/crates/jirac/src/main.rs +++ b/crates/jirac/src/main.rs @@ -3,7 +3,6 @@ mod cmd; mod jira_config; mod jql; mod term; -mod types; use clap::Parser; use cli::{Cli, Commands}; diff --git a/crates/jirac/src/types/issue.rs b/crates/jirac/src/types/issue.rs deleted file mode 100644 index ac009a8..0000000 --- a/crates/jirac/src/types/issue.rs +++ /dev/null @@ -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, - pub status: Status, - pub created: chrono::DateTime, - pub priority: Priority, - pub assignee: Person, - pub reporter: Person, - pub creator: Person, - #[serde(rename = "duedate")] - pub due_date: Option, - pub comment: Option, - 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, -} - -#[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, -} - -#[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, -} - -#[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> { - 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> { - 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> { - let j = serde_json::to_string_pretty(issues)?; - println!("{}", j); - Ok(()) -} diff --git a/crates/libjirac/Cargo.toml b/crates/libjirac/Cargo.toml index 3e24ca4..cda4626 100644 --- a/crates/libjirac/Cargo.toml +++ b/crates/libjirac/Cargo.toml @@ -4,3 +4,5 @@ version = "0.1.0" edition = "2021" [dependencies] +serde = { workspace = true } +chrono = { workspace = true } \ No newline at end of file diff --git a/crates/jirac/src/types.rs b/crates/libjirac/src/entities.rs similarity index 100% rename from crates/jirac/src/types.rs rename to crates/libjirac/src/entities.rs diff --git a/crates/libjirac/src/entities/issue.rs b/crates/libjirac/src/entities/issue.rs new file mode 100644 index 0000000..bfdfe57 --- /dev/null +++ b/crates/libjirac/src/entities/issue.rs @@ -0,0 +1,80 @@ +use serde::{Deserialize, Serialize}; + +#[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, + pub status: Status, + pub created: chrono::DateTime, + pub priority: Priority, + pub assignee: Person, + pub reporter: Person, + pub creator: Person, + #[serde(rename = "duedate")] + pub due_date: Option, + pub comment: Option, + 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, +} + +#[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, +} + +#[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, +} + +#[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, +} diff --git a/crates/libjirac/src/lib.rs b/crates/libjirac/src/lib.rs index b93cf3f..0b8f0b5 100644 --- a/crates/libjirac/src/lib.rs +++ b/crates/libjirac/src/lib.rs @@ -1,14 +1 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub mod entities; From 8a7c989f489840317ff215b29e211d8e290b316c Mon Sep 17 00:00:00 2001 From: Daan Boerlage Date: Tue, 21 Jan 2025 23:43:35 +0100 Subject: [PATCH 3/4] Add a command client to libjirac --- Cargo.lock | 4 + Cargo.toml | 5 +- crates/jirac/src/cmd/search.rs | 13 +- crates/jirac/src/cmd/view.rs | 16 +-- crates/jirac/src/jira_config.rs | 7 ++ crates/jirac/src/jql.rs | 37 ------ crates/jirac/src/main.rs | 1 - crates/libjirac/Cargo.toml | 6 +- crates/libjirac/src/client.rs | 118 ++++++++++++++++++ crates/libjirac/src/client/commands.rs | 5 + .../src/client/commands/search_command.rs | 35 ++++++ .../src/client/commands/self_command.rs | 46 +++++++ crates/libjirac/src/entities.rs | 1 + crates/libjirac/src/entities/issue.rs | 10 +- crates/libjirac/src/entities/search.rs | 8 ++ crates/libjirac/src/lib.rs | 1 + 16 files changed, 253 insertions(+), 60 deletions(-) delete mode 100644 crates/jirac/src/jql.rs create mode 100644 crates/libjirac/src/client.rs create mode 100644 crates/libjirac/src/client/commands.rs create mode 100644 crates/libjirac/src/client/commands/search_command.rs create mode 100644 crates/libjirac/src/client/commands/self_command.rs create mode 100644 crates/libjirac/src/entities/search.rs diff --git a/Cargo.lock b/Cargo.lock index 6a7807c..100cc79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -982,7 +982,11 @@ name = "libjirac" version = "0.1.0" dependencies = [ "chrono", + "http", + "reqwest", "serde", + "thiserror", + "url", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 545ff29..467e0c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" @@ -16,4 +18,5 @@ gray_matter = { version = "0.2", default-features = false, features = ["toml"] } open = "5.2" chrono = { version = "0.4", features = ["serde"] } tabwriter = "1.4" -crossterm = "0.28" \ No newline at end of file +crossterm = "0.28" +thiserror = "2" \ No newline at end of file diff --git a/crates/jirac/src/cmd/search.rs b/crates/jirac/src/cmd/search.rs index fd8dff2..7392c15 100644 --- a/crates/jirac/src/cmd/search.rs +++ b/crates/jirac/src/cmd/search.rs @@ -2,6 +2,8 @@ use crate::cli::FormatMode; use crate::jira_config::JiraConfig; 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; @@ -81,13 +83,10 @@ pub async fn exec(output: FormatMode, jql: &str) -> Result<(), Box 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) => { diff --git a/crates/jirac/src/cmd/view.rs b/crates/jirac/src/cmd/view.rs index b868fff..f477b60 100644 --- a/crates/jirac/src/cmd/view.rs +++ b/crates/jirac/src/cmd/view.rs @@ -2,6 +2,8 @@ use crate::cli::FormatMode; use crate::jira_config::JiraConfig; use crate::term::hyperlink; use crossterm::style::{Color, Stylize}; +use libjirac::client::commands::{SearchCommand, SelfCommand}; +use libjirac::client::JiraClient; use libjirac::entities::issue::JiraIssue; use std::io::Write; @@ -124,13 +126,10 @@ pub async fn exec(output: FormatMode, issue_key: &str) -> Result<(), Box 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 +139,8 @@ pub async fn exec(output: FormatMode, issue_key: &str) -> Result<(), Box::new(&matched_issue.href); + let fetched_issue = client.exec(self_cmd).await?; match output { FormatMode::Pretty => pretty_print(&fetched_issue)?, diff --git a/crates/jirac/src/jira_config.rs b/crates/jirac/src/jira_config.rs index d2be3df..778e22b 100644 --- a/crates/jirac/src/jira_config.rs +++ b/crates/jirac/src/jira_config.rs @@ -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) + } +} diff --git a/crates/jirac/src/jql.rs b/crates/jirac/src/jql.rs deleted file mode 100644 index de6fc6a..0000000 --- a/crates/jirac/src/jql.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::jira_config::JiraConfig; -use libjirac::entities::issue::JiraIssue; -use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; -use serde::Deserialize; - -#[derive(Deserialize)] -pub struct JiraSearchResponse { - pub issues: Vec, - pub total: u32, -} - -pub async fn run( - config: &JiraConfig, - jql: &str, -) -> Result> { - 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::().await?) -} diff --git a/crates/jirac/src/main.rs b/crates/jirac/src/main.rs index a2cd3c1..d01c6a9 100644 --- a/crates/jirac/src/main.rs +++ b/crates/jirac/src/main.rs @@ -1,7 +1,6 @@ mod cli; mod cmd; mod jira_config; -mod jql; mod term; use clap::Parser; diff --git a/crates/libjirac/Cargo.toml b/crates/libjirac/Cargo.toml index cda4626..93c3e98 100644 --- a/crates/libjirac/Cargo.toml +++ b/crates/libjirac/Cargo.toml @@ -5,4 +5,8 @@ edition = "2021" [dependencies] serde = { workspace = true } -chrono = { workspace = true } \ No newline at end of file +chrono = { workspace = true } +reqwest = { workspace = true } +http = { workspace = true } +url = { workspace = true } +thiserror = { workspace = true } \ No newline at end of file diff --git a/crates/libjirac/src/client.rs b/crates/libjirac/src/client.rs new file mode 100644 index 0000000..46685ba --- /dev/null +++ b/crates/libjirac/src/client.rs @@ -0,0 +1,118 @@ +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>; +} + +pub enum JiraRequestType { + Read, + Create, + Update, + Delete, +} + +impl From 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("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( + &self, + cmd: TCommand, + ) -> Result + 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)); + } + + match response.json::().await { + Ok(x) => Ok(x), + Err(reason) => Err(JiraClientError::ReqwestError(reason)), + } + } + + fn construct_url(&self, endpoint: &str) -> Result { + 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()) + } +} diff --git a/crates/libjirac/src/client/commands.rs b/crates/libjirac/src/client/commands.rs new file mode 100644 index 0000000..3985e21 --- /dev/null +++ b/crates/libjirac/src/client/commands.rs @@ -0,0 +1,5 @@ +mod search_command; +mod self_command; + +pub use search_command::SearchCommand; +pub use self_command::SelfCommand; diff --git a/crates/libjirac/src/client/commands/search_command.rs b/crates/libjirac/src/client/commands/search_command.rs new file mode 100644 index 0000000..b44e4b8 --- /dev/null +++ b/crates/libjirac/src/client/commands/search_command.rs @@ -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> { + let params = vec![("jql".to_string(), self.jql.clone())]; + + Some(params) + } +} diff --git a/crates/libjirac/src/client/commands/self_command.rs b/crates/libjirac/src/client/commands/self_command.rs new file mode 100644 index 0000000..bfee0f9 --- /dev/null +++ b/crates/libjirac/src/client/commands/self_command.rs @@ -0,0 +1,46 @@ +use crate::client::{JiraCommand, JiraRequestType}; +use serde::de::DeserializeOwned; +use url::Url; + +#[derive(Debug)] +pub struct SelfCommand +where + T: DeserializeOwned + Clone, +{ + href: String, + _marker: std::marker::PhantomData, +} + +impl SelfCommand +where + T: DeserializeOwned + Clone, +{ + pub fn new(href: &str) -> Self { + Self { + href: href.to_owned(), + _marker: std::marker::PhantomData, + } + } +} + +impl JiraCommand for SelfCommand +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> { + None + } +} diff --git a/crates/libjirac/src/entities.rs b/crates/libjirac/src/entities.rs index d93d369..4e94758 100644 --- a/crates/libjirac/src/entities.rs +++ b/crates/libjirac/src/entities.rs @@ -1 +1,2 @@ pub mod issue; +pub mod search; diff --git a/crates/libjirac/src/entities/issue.rs b/crates/libjirac/src/entities/issue.rs index bfdfe57..a576a7b 100644 --- a/crates/libjirac/src/entities/issue.rs +++ b/crates/libjirac/src/entities/issue.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct JiraIssue { pub key: String, #[serde(rename = "self")] @@ -8,7 +8,7 @@ pub struct JiraIssue { pub fields: JiraIssueResponseFields, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct JiraIssueResponseFields { pub summary: String, pub description: Option, @@ -24,12 +24,12 @@ pub struct JiraIssueResponseFields { pub votes: Votes, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Status { pub name: String, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Priority { pub name: String, pub id: String, @@ -69,7 +69,7 @@ pub struct Comment { pub created: chrono::DateTime, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Votes { #[serde(rename = "self")] pub href: String, diff --git a/crates/libjirac/src/entities/search.rs b/crates/libjirac/src/entities/search.rs new file mode 100644 index 0000000..9ae48a3 --- /dev/null +++ b/crates/libjirac/src/entities/search.rs @@ -0,0 +1,8 @@ +use crate::entities::issue::JiraIssue; +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct JiraSearchResponse { + pub issues: Vec, + pub total: u32, +} diff --git a/crates/libjirac/src/lib.rs b/crates/libjirac/src/lib.rs index 0b8f0b5..5ea289c 100644 --- a/crates/libjirac/src/lib.rs +++ b/crates/libjirac/src/lib.rs @@ -1 +1,2 @@ +pub mod client; pub mod entities; From 194e25dc143cc833df9eb026860ed926f2354837 Mon Sep 17 00:00:00 2001 From: Daan Boerlage Date: Wed, 22 Jan 2025 01:27:00 +0100 Subject: [PATCH 4/4] Move issue creation calls into the jira client --- Cargo.lock | 2 +- crates/jirac/Cargo.toml | 1 - crates/jirac/src/cmd/create.rs | 190 ++++-------------- crates/jirac/src/cmd/search.rs | 2 +- crates/jirac/src/cmd/view.rs | 22 -- crates/libjirac/Cargo.toml | 1 + crates/libjirac/src/client.rs | 41 +++- crates/libjirac/src/client/commands.rs | 6 + .../client/commands/issue_create_command.rs | 31 +++ .../commands/issue_transitions_command.rs | 33 +++ .../issue_transitions_update_command.rs | 39 ++++ crates/libjirac/src/entities.rs | 2 + crates/libjirac/src/entities/issue_request.rs | 61 ++++++ crates/libjirac/src/entities/transitions.rs | 29 +++ 14 files changed, 280 insertions(+), 180 deletions(-) create mode 100644 crates/libjirac/src/client/commands/issue_create_command.rs create mode 100644 crates/libjirac/src/client/commands/issue_transitions_command.rs create mode 100644 crates/libjirac/src/client/commands/issue_transitions_update_command.rs create mode 100644 crates/libjirac/src/entities/issue_request.rs create mode 100644 crates/libjirac/src/entities/transitions.rs diff --git a/Cargo.lock b/Cargo.lock index 100cc79..9f19c24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -941,7 +941,6 @@ dependencies = [ "gray_matter", "libjirac", "open", - "reqwest", "serde", "serde_json", "tabwriter", @@ -985,6 +984,7 @@ dependencies = [ "http", "reqwest", "serde", + "serde_json", "thiserror", "url", ] diff --git a/crates/jirac/Cargo.toml b/crates/jirac/Cargo.toml index 6c0c0e2..667f2d1 100644 --- a/crates/jirac/Cargo.toml +++ b/crates/jirac/Cargo.toml @@ -7,7 +7,6 @@ edition = "2021" libjirac = { path = "../libjirac" } clap = { workspace = true } -reqwest = { workspace = true } tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/jirac/src/cmd/create.rs b/crates/jirac/src/cmd/create.rs index b4f7264..2861e27 100644 --- a/crates/jirac/src/cmd/create.rs +++ b/crates/jirac/src/cmd/create.rs @@ -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, } -#[derive(Debug, Serialize)] -struct JiraIssueRequest { - fields: JiraIssueFields, - update: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -struct JiraIssueFields { - project: Project, - summary: String, - description: String, - issuetype: IssueType, - #[serde(skip_serializing_if = "Option::is_none")] - assignee: Option, - #[serde(skip_serializing_if = "Option::is_none")] - status: Option, -} - -#[derive(Debug, Serialize)] -struct JiraIssueUpdate { - #[serde(skip_serializing_if = "Option::is_none")] - parent: Option>, -} - -#[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, - 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> { @@ -216,37 +142,27 @@ async fn create_jira_issue( title: &str, description: &str, metadata: &IssueMetadata, -) -> Result> { - let client = reqwest::Client::new(); +) -> Result> { + 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::().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> { - 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::() - .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( diff --git a/crates/jirac/src/cmd/search.rs b/crates/jirac/src/cmd/search.rs index 7392c15..e678582 100644 --- a/crates/jirac/src/cmd/search.rs +++ b/crates/jirac/src/cmd/search.rs @@ -85,7 +85,7 @@ pub async fn exec(output: FormatMode, jql: &str) -> Result<(), Box Result> { - 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::().await?; - - Ok(issue_response) -} - fn pretty_print(issue: &JiraIssue) -> Result<(), Box> { println!("\n== Title {:=<71}", ""); println!( diff --git a/crates/libjirac/Cargo.toml b/crates/libjirac/Cargo.toml index 93c3e98..e83b8d5 100644 --- a/crates/libjirac/Cargo.toml +++ b/crates/libjirac/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] serde = { workspace = true } +serde_json = { workspace = true } chrono = { workspace = true } reqwest = { workspace = true } http = { workspace = true } diff --git a/crates/libjirac/src/client.rs b/crates/libjirac/src/client.rs index 46685ba..5824646 100644 --- a/crates/libjirac/src/client.rs +++ b/crates/libjirac/src/client.rs @@ -47,6 +47,8 @@ pub enum JiraClientError { UrlParseError(#[from] ParseError), #[error(transparent)] ReqwestError(#[from] reqwest::Error), + #[error(transparent)] + SerdeError(#[from] serde_json::Error), #[error("API Error: {0}")] ApiError(String), } @@ -97,9 +99,19 @@ impl JiraClient { return Err(JiraClientError::ApiError(error_text)); } - match response.json::().await { + let plain = match response.bytes().await { + Ok(x) => x, + Err(reason) => return Err(JiraClientError::ReqwestError(reason)), + }; + + let value = match is_unit::() { + true => b"null", + false => plain.as_ref(), + }; + + match serde_json::from_slice::(value) { Ok(x) => Ok(x), - Err(reason) => Err(JiraClientError::ReqwestError(reason)), + Err(reason) => Err(JiraClientError::SerdeError(reason)), } } @@ -116,3 +128,28 @@ impl JiraClient { Ok(url.to_string()) } } + +const fn is_unit() -> bool { + size_of::() == 0 && align_of::() == 1 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_unit_type() { + assert!(is_unit::<()>()); + assert!(!is_unit::()); + } + + #[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(); + } +} diff --git a/crates/libjirac/src/client/commands.rs b/crates/libjirac/src/client/commands.rs index 3985e21..6f5d4f0 100644 --- a/crates/libjirac/src/client/commands.rs +++ b/crates/libjirac/src/client/commands.rs @@ -1,5 +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; diff --git a/crates/libjirac/src/client/commands/issue_create_command.rs b/crates/libjirac/src/client/commands/issue_create_command.rs new file mode 100644 index 0000000..ce19ae3 --- /dev/null +++ b/crates/libjirac/src/client/commands/issue_create_command.rs @@ -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> { + None + } +} diff --git a/crates/libjirac/src/client/commands/issue_transitions_command.rs b/crates/libjirac/src/client/commands/issue_transitions_command.rs new file mode 100644 index 0000000..1f57c00 --- /dev/null +++ b/crates/libjirac/src/client/commands/issue_transitions_command.rs @@ -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> { + None + } +} diff --git a/crates/libjirac/src/client/commands/issue_transitions_update_command.rs b/crates/libjirac/src/client/commands/issue_transitions_update_command.rs new file mode 100644 index 0000000..b68ec21 --- /dev/null +++ b/crates/libjirac/src/client/commands/issue_transitions_update_command.rs @@ -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> { + None + } +} diff --git a/crates/libjirac/src/entities.rs b/crates/libjirac/src/entities.rs index 4e94758..1f415ef 100644 --- a/crates/libjirac/src/entities.rs +++ b/crates/libjirac/src/entities.rs @@ -1,2 +1,4 @@ pub mod issue; +pub mod issue_request; pub mod search; +pub mod transitions; diff --git a/crates/libjirac/src/entities/issue_request.rs b/crates/libjirac/src/entities/issue_request.rs new file mode 100644 index 0000000..16699c0 --- /dev/null +++ b/crates/libjirac/src/entities/issue_request.rs @@ -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, +} + +#[derive(Debug, Clone, Serialize)] +pub struct IssueUpdate { + #[serde(skip_serializing_if = "Option::is_none")] + pub parent: Option>, +} + +#[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, + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, +} + +#[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, +} diff --git a/crates/libjirac/src/entities/transitions.rs b/crates/libjirac/src/entities/transitions.rs new file mode 100644 index 0000000..03282e8 --- /dev/null +++ b/crates/libjirac/src/entities/transitions.rs @@ -0,0 +1,29 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize)] +pub struct Transitions { + pub transitions: Vec, +} + +#[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, +}