use std::collections::HashMap;
use std::sync::Arc;
use crate::args::commands::VersionArgs;
use crate::config::config_file::{AuthData, ConfigFile};
use crate::jira_doc_std_field;
use crate::utils::changelog_extractor::ChangelogExtractor;
use async_trait::async_trait;
use chrono::Utc;
use jira_v3_openapi::apis::Error;
use jira_v3_openapi::apis::configuration::Configuration;
use jira_v3_openapi::apis::issues_api::{assign_issue, do_transition, edit_issue, get_transitions};
use jira_v3_openapi::apis::project_versions_api::*;
use jira_v3_openapi::models::user::AccountType;
use jira_v3_openapi::models::{
DeleteAndReplaceVersionBean, FieldUpdateOperation, IssueTransition, IssueUpdateDetails, User,
Version, VersionRelatedWork,
};
use serde_json::Value;
#[cfg(test)]
use mockall::automock;
#[cfg(any(windows, unix))]
use futures::StreamExt;
#[cfg(any(windows, unix))]
use futures::stream::FuturesUnordered;
#[derive(Clone)]
pub struct VersionCmdRunner {
cfg: Configuration,
resolution_value: Value,
resolution_comment: Value,
resolution_transition_name: Option<Vec<String>>,
}
impl VersionCmdRunner {
pub fn new(cfg_file: &ConfigFile) -> VersionCmdRunner {
let mut config = Configuration::new();
let auth_data = AuthData::from_base64(cfg_file.get_auth_key());
config.base_path = cfg_file.get_jira_url().to_string();
config.basic_auth = Some((auth_data.0, Some(auth_data.1)));
VersionCmdRunner {
cfg: config,
resolution_value: serde_json::from_str(cfg_file.get_standard_resolution().as_str())
.unwrap_or(Value::Null),
resolution_comment: serde_json::from_str(
format!(
"{{\"body\": {}}}",
jira_doc_std_field!(cfg_file.get_standard_resolution_comment().as_str())
)
.as_str(),
)
.unwrap_or(Value::Null),
resolution_transition_name: cfg_file.get_transition_name("resolve"),
}
}
#[cfg(any(windows, unix))]
pub async fn create_jira_version(
&self,
params: VersionCmdParams,
) -> Result<(Version, Option<Vec<(String, String, String, String)>>), Box<dyn std::error::Error>>
{
let version_description: Option<String>;
let mut resolved_issues = vec![];
let mut transitioned_issue: Arc<Vec<(String, String, String, String)>> = Arc::new(vec![]);
if Option::is_some(¶ms.changelog_file) {
let changelog_extractor = ChangelogExtractor::new(params.changelog_file.unwrap());
version_description = Some(changelog_extractor.extract_version_changelog().unwrap_or(
if Option::is_some(¶ms.version_description) {
params.version_description.unwrap()
} else {
"No changelog found for this version".to_string()
},
));
if Option::is_some(¶ms.transition_issues) && params.transition_issues.unwrap() {
resolved_issues = changelog_extractor
.extract_issues_from_changelog(
&version_description.clone().unwrap(),
¶ms.project,
)
.unwrap_or_default();
}
} else {
version_description = params.version_description;
}
let release_date =
if Option::is_some(¶ms.version_released) && params.version_released.unwrap() {
if Option::is_some(¶ms.version_release_date) {
params.version_release_date
} else {
Some(Utc::now().format("%Y-%m-%d").to_string())
}
} else {
None
};
let version = Version {
project: Some(params.project),
name: Some(
params
.version_name
.expect("VersionName is mandatory on creation!"),
),
description: version_description,
start_date: params.version_start_date,
release_date,
archived: params.version_archived,
released: params.version_released,
..Default::default()
};
let version = create_version(&self.cfg, version).await?;
if !resolved_issues.is_empty() {
let user_data = if Option::is_some(¶ms.transition_assignee) {
Some(User {
account_id: Some(params.transition_assignee.expect("Assignee is required")),
account_type: Some(AccountType::Atlassian),
..Default::default()
})
} else {
None
};
let mut handles = FuturesUnordered::new();
for issue in resolved_issues {
handles.push(self.manage_version_related_issues(issue, &user_data, &version));
}
while let Some(result) = handles.next().await {
match result {
Ok((issue, transition_result, assign_result, fixversion_result)) => {
Arc::make_mut(&mut transitioned_issue).push((
issue,
transition_result,
assign_result,
fixversion_result,
));
}
Err(err) => {
eprintln!("Error managing version related issues: {err:?}");
}
}
}
}
let transitioned_issue_owned: Vec<(String, String, String, String)> =
(*transitioned_issue).clone();
Ok((
version,
if !transitioned_issue.is_empty() {
Some(transitioned_issue_owned)
} else {
None
},
))
}
#[cfg(target_family = "wasm")]
pub async fn create_jira_version(
&self,
params: VersionCmdParams,
) -> Result<(Version, Option<Vec<(String, String, String, String)>>), Box<dyn std::error::Error>>
{
let version_description: Option<String>;
let mut resolved_issues = vec![];
let mut transitioned_issue: Vec<(String, String, String, String)> = vec![];
if Option::is_some(¶ms.changelog_file) {
let changelog_extractor = ChangelogExtractor::new(params.changelog_file.unwrap());
version_description = Some(changelog_extractor.extract_version_changelog().unwrap_or(
if Option::is_some(¶ms.version_description) {
params.version_description.unwrap()
} else {
"No changelog found for this version".to_string()
},
));
if Option::is_some(¶ms.transition_issues) && params.transition_issues.unwrap() {
resolved_issues = changelog_extractor
.extract_issues_from_changelog(
&version_description.clone().unwrap(),
¶ms.project,
)
.unwrap_or_default();
}
} else {
version_description = params.version_description;
}
let release_date =
if Option::is_some(¶ms.version_released) && params.version_released.unwrap() {
if Option::is_some(¶ms.version_release_date) {
params.version_release_date
} else {
Some(Utc::now().format("%Y-%m-%d").to_string())
}
} else {
None
};
let version = Version {
project: Some(params.project),
name: Some(
params
.version_name
.expect("VersionName is mandatory on cretion!"),
),
description: version_description,
start_date: params.version_start_date,
release_date,
archived: params.version_archived,
released: params.version_released,
..Default::default()
};
let version = create_version(&self.cfg, version).await?;
if !resolved_issues.is_empty() {
let user_data = if Option::is_some(¶ms.transition_assignee) {
Some(User {
account_id: Some(params.transition_assignee.expect("Assignee is required")),
account_type: Some(AccountType::Atlassian),
..Default::default()
})
} else {
None
};
for issue in resolved_issues {
let all_transitions: Vec<IssueTransition> = get_transitions(
&self.cfg,
issue.clone().as_str(),
None,
None,
None,
Some(false),
None,
)
.await?
.transitions
.unwrap_or_default();
let transition_names: Vec<String> = self
.resolution_transition_name
.clone()
.expect("Transition name is required and must be set in the config file");
let resolve_transitions: Vec<IssueTransition> = all_transitions
.into_iter()
.filter(|t| {
transition_names.contains(&t.name.clone().unwrap_or("".to_string()))
})
.collect();
let transition_ids = resolve_transitions
.into_iter()
.map(|t| t.id.clone().unwrap_or("".to_string()))
.collect::<Vec<String>>();
let transitions = transition_ids
.into_iter()
.map(|id| {
Some(IssueTransition {
id: Some(id),
..Default::default()
})
})
.collect::<Vec<Option<IssueTransition>>>();
let mut update_fields_hashmap: HashMap<String, Vec<FieldUpdateOperation>> =
HashMap::new();
let mut transition_fields_hashmap: HashMap<String, Vec<FieldUpdateOperation>> =
HashMap::new();
let mut version_update_op = FieldUpdateOperation::new();
let mut version_resolution_update_field = HashMap::new();
let mut version_resolution_comment_op = FieldUpdateOperation::new();
let version_json: Value =
serde_json::from_str(serde_json::to_string(&version).unwrap().as_str())
.unwrap_or(Value::Null);
let resolution_value = self.resolution_value.clone();
let comment_value = self.resolution_comment.clone();
version_update_op.add = Some(Some(version_json));
version_resolution_update_field.insert("resolution".to_string(), resolution_value);
version_resolution_comment_op.add = Some(Some(comment_value));
update_fields_hashmap.insert("fixVersions".to_string(), vec![version_update_op]);
transition_fields_hashmap
.insert("comment".to_string(), vec![version_resolution_comment_op]);
let issue_update_data = IssueUpdateDetails {
fields: None,
history_metadata: None,
properties: None,
transition: None,
update: Some(update_fields_hashmap),
};
let mut transition_result: String = "KO".to_string();
if !Vec::is_empty(&transitions) {
for transition in transitions {
let issue_transition_data = IssueUpdateDetails {
fields: Some(version_resolution_update_field.clone()),
history_metadata: None,
properties: None,
transition: Some(transition.clone().unwrap()),
update: Some(transition_fields_hashmap.clone()),
};
match do_transition(
&self.cfg,
issue.clone().as_str(),
issue_transition_data,
)
.await
{
Ok(_) => {
transition_result = "OK".to_string();
break;
}
Err(Error::Serde(e)) => {
if e.is_eof() {
transition_result = "OK".to_string();
break;
} else {
transition_result = "KO".to_string()
}
}
Err(_) => transition_result = "KO".to_string(),
}
}
}
let assign_result: String = match assign_issue(
&self.cfg,
issue.clone().as_str(),
user_data.clone().unwrap(),
)
.await
{
Ok(_) => "OK".to_string(),
Err(Error::Serde(e)) => {
if e.is_eof() {
"OK".to_string()
} else {
"KO".to_string()
}
}
Err(_) => "KO".to_string(),
};
let fixversion_result: String = match edit_issue(
&self.cfg,
issue.clone().as_str(),
issue_update_data,
Some(true),
None,
None,
Some(true),
None,
)
.await
{
Ok(_) => version.clone().name.unwrap_or("".to_string()),
Err(_) => "NO fixVersion set".to_string(),
};
transitioned_issue.push((
issue.clone(),
transition_result,
assign_result,
fixversion_result,
));
}
}
Ok((
version,
if !transitioned_issue.is_empty() {
Some(transitioned_issue)
} else {
None
},
))
}
pub async fn get_jira_version(
&self,
params: VersionCmdParams,
) -> Result<Version, Error<GetVersionError>> {
get_version(
&self.cfg,
params.version_id.expect("VersionID is mandatory!").as_str(),
None,
)
.await
}
pub async fn list_jira_versions(
&self,
params: VersionCmdParams,
) -> Result<Vec<Version>, Box<dyn std::error::Error>> {
if Option::is_some(¶ms.versions_page_size) {
match get_project_versions_paginated(
&self.cfg,
params.project.as_str(),
params.versions_page_offset,
params.versions_page_size,
None,
None,
None,
None,
)
.await?
.values
{
Some(values) => Ok(values),
None => Ok(vec![]),
}
} else {
Ok(get_project_versions(&self.cfg, params.project.as_str(), None).await?)
}
}
pub async fn update_jira_version(
&self,
params: VersionCmdParams,
) -> Result<Version, Error<UpdateVersionError>> {
let release_date =
if Option::is_some(¶ms.version_released) && params.version_released.unwrap() {
if Option::is_some(¶ms.version_release_date) {
params.version_release_date
} else {
Some(Utc::now().format("%Y-%m-%d").to_string())
}
} else {
None
};
let version = Version {
id: Some(params.version_id.clone().expect("VersionID is mandatory!")),
name: params.version_name,
description: params.version_description,
start_date: params.version_start_date,
release_date,
archived: params.version_archived,
released: params.version_released,
..Default::default()
};
update_version(
&self.cfg,
params.version_id.expect("VersionID is mandatory!").as_str(),
version,
)
.await
}
pub async fn delete_jira_version(
&self,
params: VersionCmdParams,
) -> Result<serde_json::Value, Error<DeleteAndReplaceVersionError>> {
match delete_and_replace_version(
&self.cfg,
params.version_id.expect("VersionID is mandatory!").as_str(),
DeleteAndReplaceVersionBean::new(),
)
.await
{
Ok(_) => Ok(serde_json::json!({"status": "success"})),
Err(e) => match e {
Error::Serde(_) => Ok(
serde_json::json!({"status": "success", "warning": "Version was deleted, some issues in deserializing response!"}),
),
_ => Err(e),
},
}
}
pub async fn get_jira_version_related_work(
&self,
params: VersionCmdParams,
) -> Result<Vec<VersionRelatedWork>, Error<GetRelatedWorkError>> {
get_related_work(
&self.cfg,
params.version_id.expect("VersionID is mandatory!").as_str(),
)
.await
}
#[cfg(any(windows, unix))]
async fn manage_version_related_issues(
&self,
issue: String,
user_data: &Option<User>,
version: &Version,
) -> Result<(String, String, String, String), Box<dyn std::error::Error>> {
let all_transitions: Vec<IssueTransition> = match get_transitions(
&self.cfg,
issue.clone().as_str(),
None,
None,
None,
Some(false),
None,
)
.await
{
Ok(transitions) => transitions.transitions.unwrap_or_default(),
Err(_) => {
return Ok((
issue,
"KO".to_string(),
"KO".to_string(),
"NO fixVersion set".to_string(),
));
}
};
let transition_names: Vec<String> = self
.resolution_transition_name
.clone()
.expect("Transition name is required and must be set in the config file");
let resolve_transitions: Vec<IssueTransition> = all_transitions
.into_iter()
.filter(|t| transition_names.contains(&t.name.clone().unwrap_or("".to_string())))
.collect();
let transition_ids = resolve_transitions
.into_iter()
.map(|t| t.id.clone().unwrap_or("".to_string()))
.collect::<Vec<String>>();
let transitions = transition_ids
.into_iter()
.map(|id| {
Some(IssueTransition {
id: Some(id),
..Default::default()
})
})
.collect::<Vec<Option<IssueTransition>>>();
let mut update_fields_hashmap: HashMap<String, Vec<FieldUpdateOperation>> = HashMap::new();
let mut transition_fields_hashmap: HashMap<String, Vec<FieldUpdateOperation>> =
HashMap::new();
let mut version_update_op = FieldUpdateOperation::new();
let mut version_resolution_update_field = HashMap::new();
let mut version_resolution_comment_op = FieldUpdateOperation::new();
let version_json: Value =
serde_json::from_str(serde_json::to_string(&version).unwrap().as_str())
.unwrap_or(Value::Null);
let resolution_value = self.resolution_value.clone();
let comment_value = self.resolution_comment.clone();
version_update_op.add = Some(Some(version_json));
version_resolution_update_field.insert("resolution".to_string(), resolution_value);
version_resolution_comment_op.add = Some(Some(comment_value));
update_fields_hashmap.insert("fixVersions".to_string(), vec![version_update_op]);
transition_fields_hashmap
.insert("comment".to_string(), vec![version_resolution_comment_op]);
let issue_update_data = IssueUpdateDetails {
fields: None,
history_metadata: None,
properties: None,
transition: None,
update: Some(update_fields_hashmap),
};
let mut transition_result: String = "KO".to_string();
if !Vec::is_empty(&transitions) {
for transition in transitions {
let issue_transition_data = IssueUpdateDetails {
fields: Some(version_resolution_update_field.clone()),
history_metadata: None,
properties: None,
transition: Some(transition.clone().unwrap()),
update: Some(transition_fields_hashmap.clone()),
};
match do_transition(&self.cfg, issue.clone().as_str(), issue_transition_data).await
{
Ok(_) => {
transition_result = "OK".to_string();
break;
}
Err(Error::Serde(e)) => {
if e.is_eof() {
transition_result = "OK".to_string();
break;
} else {
transition_result = "KO".to_string()
}
}
Err(_) => transition_result = "KO".to_string(),
}
}
}
let assign_result: String = match assign_issue(
&self.cfg,
issue.clone().as_str(),
user_data.clone().unwrap(),
)
.await
{
Ok(_) => "OK".to_string(),
Err(Error::Serde(e)) => {
if e.is_eof() {
"OK".to_string()
} else {
"KO".to_string()
}
}
Err(_) => "KO".to_string(),
};
let fixversion_result: String = match edit_issue(
&self.cfg,
issue.clone().as_str(),
issue_update_data,
Some(true),
None,
None,
Some(true),
None,
)
.await
{
Ok(_) => version.clone().name.unwrap_or("".to_string()),
Err(_) => "NO fixVersion set".to_string(),
};
Ok((issue, transition_result, assign_result, fixversion_result))
}
}
pub struct VersionCmdParams {
pub project: String,
pub project_id: Option<i64>,
pub version_name: Option<String>,
pub version_id: Option<String>,
pub version_description: Option<String>,
pub version_start_date: Option<String>,
pub version_release_date: Option<String>,
pub version_archived: Option<bool>,
pub version_released: Option<bool>,
pub changelog_file: Option<String>,
pub transition_issues: Option<bool>,
pub transition_assignee: Option<String>,
pub versions_page_size: Option<i32>,
pub versions_page_offset: Option<i64>,
}
impl VersionCmdParams {
pub fn new() -> VersionCmdParams {
VersionCmdParams {
project: "".to_string(),
project_id: None,
version_name: None,
version_id: None,
version_description: None,
version_start_date: None,
version_release_date: None,
version_archived: None,
version_released: None,
changelog_file: None,
transition_issues: None,
transition_assignee: None,
versions_page_size: None,
versions_page_offset: None,
}
}
pub fn merge_args(
current_version: Version,
opt_args: Option<&VersionArgs>,
) -> VersionCmdParams {
match opt_args {
Some(args) => VersionCmdParams {
project: current_version.project.clone().unwrap_or("".to_string()),
project_id: current_version.project_id,
version_id: current_version.id,
version_name: if Option::is_some(&args.version_name) {
args.version_name.clone()
} else {
current_version.name
},
version_description: if Option::is_some(&args.version_description) {
args.version_description.clone()
} else {
current_version.description
},
version_start_date: if Option::is_some(&args.version_start_date) {
args.version_start_date.clone()
} else {
current_version.start_date
},
version_release_date: if Option::is_some(&args.version_release_date) {
args.version_release_date.clone()
} else {
current_version.release_date
},
version_archived: if Option::is_some(&args.version_archived) {
args.version_archived
} else {
current_version.archived
},
version_released: if Option::is_some(&args.version_released) {
args.version_released
} else {
current_version.released
},
changelog_file: None,
transition_issues: None,
transition_assignee: None,
versions_page_size: None,
versions_page_offset: None,
},
None => VersionCmdParams {
project: current_version.project.clone().unwrap_or("".to_string()),
project_id: current_version.project_id,
version_id: current_version.id,
version_name: current_version.name,
version_description: current_version.description,
version_start_date: current_version.start_date,
version_release_date: current_version.release_date,
version_archived: current_version.archived,
version_released: current_version.released,
changelog_file: None,
transition_issues: None,
transition_assignee: None,
versions_page_size: None,
versions_page_offset: None,
},
}
}
pub fn mark_released(version: Version) -> VersionCmdParams {
let mut version_to_release = Self::merge_args(version, None);
version_to_release.version_released = Some(true);
version_to_release.version_release_date = Some(Utc::now().format("%Y-%m-%d").to_string());
version_to_release
}
pub fn mark_archived(version: Version) -> VersionCmdParams {
let mut version_to_archive = Self::merge_args(version, None);
version_to_archive.version_archived = Some(true);
version_to_archive
}
}
impl From<&VersionArgs> for VersionCmdParams {
fn from(args: &VersionArgs) -> Self {
VersionCmdParams {
project: args.project_key.clone(),
project_id: args.project_id,
version_name: args.version_name.clone(),
version_id: args.version_id.clone(),
version_description: args.version_description.clone(),
version_start_date: Some(
args.version_start_date
.clone()
.unwrap_or(Utc::now().format("%Y-%m-%d").to_string()),
),
version_release_date: args.version_release_date.clone(),
version_archived: args.version_archived,
version_released: args.version_released,
changelog_file: args.changelog_file.clone(),
transition_issues: args.transition_issues,
transition_assignee: args.transition_assignee.clone(),
versions_page_size: args.pagination.page_size,
versions_page_offset: args.pagination.page_offset,
}
}
}
impl Default for VersionCmdParams {
fn default() -> Self {
VersionCmdParams::new()
}
}
#[cfg_attr(test, automock)]
#[async_trait(?Send)]
pub trait VersionCmdRunnerApi: Send + Sync {
async fn create_jira_version(
&self,
params: VersionCmdParams,
) -> Result<(Version, Option<Vec<(String, String, String, String)>>), Box<dyn std::error::Error>>;
async fn list_jira_versions(
&self,
params: VersionCmdParams,
) -> Result<Vec<Version>, Box<dyn std::error::Error>>;
async fn get_jira_version(
&self,
params: VersionCmdParams,
) -> Result<Version, Box<dyn std::error::Error>>;
async fn update_jira_version(
&self,
params: VersionCmdParams,
) -> Result<Version, Box<dyn std::error::Error>>;
async fn delete_jira_version(
&self,
params: VersionCmdParams,
) -> Result<(), Box<dyn std::error::Error>>;
async fn get_jira_version_related_work(
&self,
params: VersionCmdParams,
) -> Result<Vec<VersionRelatedWork>, Error<GetRelatedWorkError>>;
}
#[async_trait(?Send)]
impl VersionCmdRunnerApi for VersionCmdRunner {
async fn create_jira_version(
&self,
params: VersionCmdParams,
) -> Result<(Version, Option<Vec<(String, String, String, String)>>), Box<dyn std::error::Error>>
{
VersionCmdRunner::create_jira_version(self, params).await
}
async fn list_jira_versions(
&self,
params: VersionCmdParams,
) -> Result<Vec<Version>, Box<dyn std::error::Error>> {
VersionCmdRunner::list_jira_versions(self, params).await
}
async fn get_jira_version(
&self,
params: VersionCmdParams,
) -> Result<Version, Box<dyn std::error::Error>> {
VersionCmdRunner::get_jira_version(self, params)
.await
.map_err(|err| Box::new(err) as Box<dyn std::error::Error>)
}
async fn update_jira_version(
&self,
params: VersionCmdParams,
) -> Result<Version, Box<dyn std::error::Error>> {
VersionCmdRunner::update_jira_version(self, params)
.await
.map_err(|err| Box::new(err) as Box<dyn std::error::Error>)
}
async fn delete_jira_version(
&self,
params: VersionCmdParams,
) -> Result<(), Box<dyn std::error::Error>> {
VersionCmdRunner::delete_jira_version(self, params)
.await
.map(|_| ())
.map_err(|err| Box::new(err) as Box<dyn std::error::Error>)
}
async fn get_jira_version_related_work(
&self,
params: VersionCmdParams,
) -> Result<Vec<VersionRelatedWork>, Error<GetRelatedWorkError>> {
VersionCmdRunner::get_jira_version_related_work(self, params).await
}
}