jirust_cli/runners/jira_cmd_runners/version_cmd_runner.rs
1use std::collections::HashMap;
2use std::sync::Arc;
3
4use crate::args::commands::VersionArgs;
5use crate::config::config_file::{AuthData, ConfigFile};
6use crate::jira_doc_std_field;
7use crate::utils::changelog_extractor::ChangelogExtractor;
8use async_trait::async_trait;
9use chrono::Utc;
10use jira_v3_openapi::apis::Error;
11use jira_v3_openapi::apis::configuration::Configuration;
12use jira_v3_openapi::apis::issues_api::{assign_issue, do_transition, edit_issue, get_transitions};
13use jira_v3_openapi::apis::project_versions_api::*;
14use jira_v3_openapi::models::user::AccountType;
15use jira_v3_openapi::models::{
16 DeleteAndReplaceVersionBean, FieldUpdateOperation, IssueTransition, IssueUpdateDetails, User,
17 Version, VersionRelatedWork,
18};
19use serde_json::Value;
20
21#[cfg(test)]
22use mockall::automock;
23
24#[cfg(any(windows, unix))]
25use futures::StreamExt;
26#[cfg(any(windows, unix))]
27use futures::stream::FuturesUnordered;
28
29/// Version command runner struct
30///
31/// This struct is responsible for holding the version command runner parameters
32/// and it is used to pass the parameters to the version commands runner
33#[derive(Clone)]
34pub struct VersionCmdRunner {
35 cfg: Configuration,
36 resolution_value: Value,
37 resolution_comment: Value,
38 resolution_transition_name: Option<Vec<String>>,
39}
40
41/// Version command runner implementation.
42///
43///
44/// # Methods
45///
46/// * `new` - This method creates a new instance of the VersionCmdRunner struct
47/// * `create_jira_version` - This method creates a new Jira version
48/// * `get_jira_version` - This method gets a Jira version
49/// * `list_jira_versions` - This method lists Jira versions
50/// * `update_jira_version` - This method updates a Jira version
51/// * `delete_jira_version` - This method deletes a Jira version
52/// * `release_jira_version` - This method releases a Jira version
53/// * `archive_jira_version` - This method archives a Jira version
54impl VersionCmdRunner {
55 /// This method creates a new instance of the VersionCmdRunner struct
56 ///
57 /// # Arguments
58 ///
59 /// * `cfg_file` - A ConfigFile struct
60 ///
61 /// # Returns
62 ///
63 /// * A new instance of the VersionCmdRunner struct
64 ///
65 /// # Examples
66 ///
67 /// ```
68 /// use jirust_cli::config::config_file::ConfigFile;
69 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdRunner;
70 /// use toml::Table;
71 ///
72 /// let cfg_file = ConfigFile::new("dXNlcm5hbWU6YXBpX2tleQ==".to_string(), "jira_url".to_string(), "standard_resolution".to_string(), "standard_resolution_comment".to_string(), Table::new());
73 ///
74 /// let version_cmd_runner = VersionCmdRunner::new(&cfg_file);
75 /// ```
76 pub fn new(cfg_file: &ConfigFile) -> VersionCmdRunner {
77 let mut config = Configuration::new();
78 let auth_data = AuthData::from_base64(cfg_file.get_auth_key());
79 config.base_path = cfg_file.get_jira_url().to_string();
80 config.basic_auth = Some((auth_data.0, Some(auth_data.1)));
81 VersionCmdRunner {
82 cfg: config,
83 resolution_value: serde_json::from_str(cfg_file.get_standard_resolution().as_str())
84 .unwrap_or(Value::Null),
85 resolution_comment: serde_json::from_str(
86 format!(
87 "{{\"body\": {}}}",
88 jira_doc_std_field!(cfg_file.get_standard_resolution_comment().as_str())
89 )
90 .as_str(),
91 )
92 .unwrap_or(Value::Null),
93 resolution_transition_name: cfg_file.get_transition_name("resolve"),
94 }
95 }
96
97 /// This method creates a new Jira version with the given parameters
98 /// and returns the created version
99 ///
100 /// # Arguments
101 ///
102 /// * `params` - A VersionCmdParams struct
103 ///
104 /// # Returns
105 ///
106 /// * A Result containing a Version struct or a Box<dyn std::error::Error>
107 ///
108 /// # Examples
109 ///
110 /// ```no_run
111 /// use jira_v3_openapi::models::Version;
112 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdParams;
113 /// use jirust_cli::config::config_file::ConfigFile;
114 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdRunner;
115 /// use toml::Table;
116 ///
117 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
118 /// # tokio_test::block_on(async {
119 /// let cfg_file = ConfigFile::new("dXNlcm5hbWU6YXBpX2tleQ==".to_string(), "jira_url".to_string(), "standard_resolution".to_string(), "standard_resolution_comment".to_string(), Table::new());
120 ///
121 /// let version_cmd_runner = VersionCmdRunner::new(&cfg_file);
122 /// let params = VersionCmdParams::new();
123 ///
124 /// let version = version_cmd_runner.create_jira_version(params).await?;
125 /// # Ok(())
126 /// # })
127 /// # }
128 /// ```
129 #[cfg(any(windows, unix))]
130 pub async fn create_jira_version(
131 &self,
132 params: VersionCmdParams,
133 ) -> Result<(Version, Option<Vec<(String, String, String, String)>>), Box<dyn std::error::Error>>
134 {
135 let version_description: Option<String>;
136 let mut resolved_issues = vec![];
137 let mut transitioned_issue: Arc<Vec<(String, String, String, String)>> = Arc::new(vec![]);
138 if Option::is_some(¶ms.changelog_file) {
139 let changelog_extractor = ChangelogExtractor::new(params.changelog_file.unwrap());
140 version_description = Some(changelog_extractor.extract_version_changelog().unwrap_or(
141 if Option::is_some(¶ms.version_description) {
142 params.version_description.unwrap()
143 } else {
144 "No changelog found for this version".to_string()
145 },
146 ));
147 if Option::is_some(¶ms.transition_issues) && params.transition_issues.unwrap() {
148 resolved_issues = changelog_extractor
149 .extract_issues_from_changelog(
150 &version_description.clone().unwrap(),
151 ¶ms.project,
152 )
153 .unwrap_or_default();
154 }
155 } else {
156 version_description = params.version_description;
157 }
158 let release_date =
159 if Option::is_some(¶ms.version_released) && params.version_released.unwrap() {
160 if Option::is_some(¶ms.version_release_date) {
161 params.version_release_date
162 } else {
163 Some(Utc::now().format("%Y-%m-%d").to_string())
164 }
165 } else {
166 None
167 };
168 let version = Version {
169 project: Some(params.project),
170 name: Some(
171 params
172 .version_name
173 .expect("VersionName is mandatory on creation!"),
174 ),
175 description: version_description,
176 start_date: params.version_start_date,
177 release_date,
178 archived: params.version_archived,
179 released: params.version_released,
180 ..Default::default()
181 };
182 let version = create_version(&self.cfg, version).await?;
183 if !resolved_issues.is_empty() {
184 let user_data = if Option::is_some(¶ms.transition_assignee) {
185 Some(User {
186 account_id: Some(params.transition_assignee.expect("Assignee is required")),
187 account_type: Some(AccountType::Atlassian),
188 ..Default::default()
189 })
190 } else {
191 None
192 };
193 let mut handles = FuturesUnordered::new();
194 for issue in resolved_issues {
195 handles.push(self.manage_version_related_issues(issue, &user_data, &version));
196 }
197 while let Some(result) = handles.next().await {
198 match result {
199 Ok((issue, transition_result, assign_result, fixversion_result)) => {
200 Arc::make_mut(&mut transitioned_issue).push((
201 issue,
202 transition_result,
203 assign_result,
204 fixversion_result,
205 ));
206 }
207 Err(err) => {
208 eprintln!("Error managing version related issues: {err:?}");
209 }
210 }
211 }
212 }
213 let transitioned_issue_owned: Vec<(String, String, String, String)> =
214 (*transitioned_issue).clone();
215 Ok((
216 version,
217 if !transitioned_issue.is_empty() {
218 Some(transitioned_issue_owned)
219 } else {
220 None
221 },
222 ))
223 }
224
225 /// This method creates a new Jira version with the given parameters
226 /// and returns the created version
227 ///
228 /// # Arguments
229 ///
230 /// * `params` - A VersionCmdParams struct
231 ///
232 /// # Returns
233 ///
234 /// * A Result containing a Version struct or a Box<dyn std::error::Error>
235 ///
236 /// # Examples
237 ///
238 /// ```no_run
239 /// use jira_v3_openapi::models::Version;
240 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdParams;
241 /// use jirust_cli::config::config_file::ConfigFile;
242 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdRunner;
243 /// use toml::Table;
244 ///
245 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
246 /// # tokio_test::block_on(async {
247 /// let cfg_file = ConfigFile::new("dXNlcm5hbWU6YXBpX2tleQ==".to_string(), "jira_url".to_string(), "standard_resolution".to_string(), "standard_resolution_comment".to_string(), Table::new());
248 ///
249 /// let version_cmd_runner = VersionCmdRunner::new(&cfg_file);
250 /// let params = VersionCmdParams::new();
251 ///
252 /// let version = version_cmd_runner.create_jira_version(params).await?;
253 /// # Ok(())
254 /// # })
255 /// # }
256 /// ```
257 #[cfg(target_family = "wasm")]
258 pub async fn create_jira_version(
259 &self,
260 params: VersionCmdParams,
261 ) -> Result<(Version, Option<Vec<(String, String, String, String)>>), Box<dyn std::error::Error>>
262 {
263 let version_description: Option<String>;
264 let mut resolved_issues = vec![];
265 let mut transitioned_issue: Vec<(String, String, String, String)> = vec![];
266 if Option::is_some(¶ms.changelog_file) {
267 let changelog_extractor = ChangelogExtractor::new(params.changelog_file.unwrap());
268 version_description = Some(changelog_extractor.extract_version_changelog().unwrap_or(
269 if Option::is_some(¶ms.version_description) {
270 params.version_description.unwrap()
271 } else {
272 "No changelog found for this version".to_string()
273 },
274 ));
275 if Option::is_some(¶ms.transition_issues) && params.transition_issues.unwrap() {
276 resolved_issues = changelog_extractor
277 .extract_issues_from_changelog(
278 &version_description.clone().unwrap(),
279 ¶ms.project,
280 )
281 .unwrap_or_default();
282 }
283 } else {
284 version_description = params.version_description;
285 }
286 let release_date =
287 if Option::is_some(¶ms.version_released) && params.version_released.unwrap() {
288 if Option::is_some(¶ms.version_release_date) {
289 params.version_release_date
290 } else {
291 Some(Utc::now().format("%Y-%m-%d").to_string())
292 }
293 } else {
294 None
295 };
296 let version = Version {
297 project: Some(params.project),
298 name: Some(
299 params
300 .version_name
301 .expect("VersionName is mandatory on cretion!"),
302 ),
303 description: version_description,
304 start_date: params.version_start_date,
305 release_date,
306 archived: params.version_archived,
307 released: params.version_released,
308 ..Default::default()
309 };
310 let version = create_version(&self.cfg, version).await?;
311 if !resolved_issues.is_empty() {
312 let user_data = if Option::is_some(¶ms.transition_assignee) {
313 Some(User {
314 account_id: Some(params.transition_assignee.expect("Assignee is required")),
315 account_type: Some(AccountType::Atlassian),
316 ..Default::default()
317 })
318 } else {
319 None
320 };
321 for issue in resolved_issues {
322 let all_transitions: Vec<IssueTransition> = get_transitions(
323 &self.cfg,
324 issue.clone().as_str(),
325 None,
326 None,
327 None,
328 Some(false),
329 None,
330 )
331 .await?
332 .transitions
333 .unwrap_or_default();
334 let transition_names: Vec<String> = self
335 .resolution_transition_name
336 .clone()
337 .expect("Transition name is required and must be set in the config file");
338 let resolve_transitions: Vec<IssueTransition> = all_transitions
339 .into_iter()
340 .filter(|t| {
341 transition_names.contains(&t.name.clone().unwrap_or("".to_string()))
342 })
343 .collect();
344 let transition_ids = resolve_transitions
345 .into_iter()
346 .map(|t| t.id.clone().unwrap_or("".to_string()))
347 .collect::<Vec<String>>();
348 let transitions = transition_ids
349 .into_iter()
350 .map(|id| {
351 Some(IssueTransition {
352 id: Some(id),
353 ..Default::default()
354 })
355 })
356 .collect::<Vec<Option<IssueTransition>>>();
357 let mut update_fields_hashmap: HashMap<String, Vec<FieldUpdateOperation>> =
358 HashMap::new();
359 let mut transition_fields_hashmap: HashMap<String, Vec<FieldUpdateOperation>> =
360 HashMap::new();
361 let mut version_update_op = FieldUpdateOperation::new();
362 let mut version_resolution_update_field = HashMap::new();
363 let mut version_resolution_comment_op = FieldUpdateOperation::new();
364 let version_json: Value =
365 serde_json::from_str(serde_json::to_string(&version).unwrap().as_str())
366 .unwrap_or(Value::Null);
367 let resolution_value = self.resolution_value.clone();
368 let comment_value = self.resolution_comment.clone();
369 version_update_op.add = Some(Some(version_json));
370 version_resolution_update_field.insert("resolution".to_string(), resolution_value);
371 version_resolution_comment_op.add = Some(Some(comment_value));
372 update_fields_hashmap.insert("fixVersions".to_string(), vec![version_update_op]);
373 transition_fields_hashmap
374 .insert("comment".to_string(), vec![version_resolution_comment_op]);
375 let issue_update_data = IssueUpdateDetails {
376 fields: None,
377 history_metadata: None,
378 properties: None,
379 transition: None,
380 update: Some(update_fields_hashmap),
381 };
382 let mut transition_result: String = "KO".to_string();
383 if !Vec::is_empty(&transitions) {
384 for transition in transitions {
385 let issue_transition_data = IssueUpdateDetails {
386 fields: Some(version_resolution_update_field.clone()),
387 history_metadata: None,
388 properties: None,
389 transition: Some(transition.clone().unwrap()),
390 update: Some(transition_fields_hashmap.clone()),
391 };
392 match do_transition(
393 &self.cfg,
394 issue.clone().as_str(),
395 issue_transition_data,
396 )
397 .await
398 {
399 Ok(_) => {
400 transition_result = "OK".to_string();
401 break;
402 }
403 Err(Error::Serde(e)) => {
404 if e.is_eof() {
405 transition_result = "OK".to_string();
406 break;
407 } else {
408 transition_result = "KO".to_string()
409 }
410 }
411 Err(_) => transition_result = "KO".to_string(),
412 }
413 }
414 }
415 let assign_result: String = match assign_issue(
416 &self.cfg,
417 issue.clone().as_str(),
418 user_data.clone().unwrap(),
419 )
420 .await
421 {
422 Ok(_) => "OK".to_string(),
423 Err(Error::Serde(e)) => {
424 if e.is_eof() {
425 "OK".to_string()
426 } else {
427 "KO".to_string()
428 }
429 }
430 Err(_) => "KO".to_string(),
431 };
432 let fixversion_result: String = match edit_issue(
433 &self.cfg,
434 issue.clone().as_str(),
435 issue_update_data,
436 Some(true),
437 None,
438 None,
439 Some(true),
440 None,
441 )
442 .await
443 {
444 Ok(_) => version.clone().name.unwrap_or("".to_string()),
445 Err(_) => "NO fixVersion set".to_string(),
446 };
447 transitioned_issue.push((
448 issue.clone(),
449 transition_result,
450 assign_result,
451 fixversion_result,
452 ));
453 }
454 }
455 Ok((
456 version,
457 if !transitioned_issue.is_empty() {
458 Some(transitioned_issue)
459 } else {
460 None
461 },
462 ))
463 }
464
465 /// This method gets a Jira version with the given parameters
466 /// and returns the version
467 /// If the version is not found, it returns an error
468 ///
469 /// # Arguments
470 ///
471 /// * `params` - A VersionCmdParams struct
472 ///
473 /// # Returns
474 ///
475 /// * A Result containing a Version struct or an Error<GetVersionError>
476 ///
477 /// # Examples
478 ///
479 /// ```no_run
480 /// use jira_v3_openapi::models::Version;
481 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdParams;
482 /// use jirust_cli::config::config_file::ConfigFile;
483 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdRunner;
484 /// use toml::Table;
485 ///
486 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
487 /// # tokio_test::block_on(async {
488 /// let cfg_file = ConfigFile::new("dXNlcm5hbWU6YXBpX2tleQ==".to_string(), "jira_url".to_string(), "standard_resolution".to_string(), "standard_resolution_comment".to_string(), Table::new());
489 /// let version_cmd_runner = VersionCmdRunner::new(&cfg_file);
490 /// let params = VersionCmdParams::new();
491 ///
492 /// let version = version_cmd_runner.get_jira_version(params).await?;
493 /// # Ok(())
494 /// # })
495 /// # }
496 /// ```
497 pub async fn get_jira_version(
498 &self,
499 params: VersionCmdParams,
500 ) -> Result<Version, Error<GetVersionError>> {
501 get_version(
502 &self.cfg,
503 params.version_id.expect("VersionID is mandatory!").as_str(),
504 None,
505 )
506 .await
507 }
508
509 /// This method lists Jira versions with the given parameters
510 /// and returns the versions
511 /// If there are no versions, it returns an empty vector
512 /// If the version is not found, it returns an error
513 /// If the version page size is given, it returns the paginated versions
514 /// Otherwise, it returns all versions
515 ///
516 /// # Arguments
517 ///
518 /// * `params` - A VersionCmdParams struct
519 ///
520 /// # Returns
521 ///
522 /// * A Result containing a vector of Version structs or a Box<dyn std::error::Error>
523 ///
524 /// # Examples
525 ///
526 /// ```no_run
527 /// use jira_v3_openapi::models::Version;
528 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdParams;
529 /// use jirust_cli::config::config_file::ConfigFile;
530 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdRunner;
531 /// use toml::Table;
532 ///
533 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
534 /// # tokio_test::block_on(async {
535 /// let cfg_file = ConfigFile::new("dXNlcm5hbWU6YXBpX2tleQ==".to_string(), "jira_url".to_string(), "standard_resolution".to_string(), "standard_resolution_comment".to_string(), Table::new());
536 /// let version_cmd_runner = VersionCmdRunner::new(&cfg_file);
537 /// let params = VersionCmdParams::new();
538 ///
539 /// let versions = version_cmd_runner.list_jira_versions(params).await?;
540 /// # Ok(())
541 /// # })
542 /// # }
543 /// ```
544 pub async fn list_jira_versions(
545 &self,
546 params: VersionCmdParams,
547 ) -> Result<Vec<Version>, Box<dyn std::error::Error>> {
548 if Option::is_some(¶ms.versions_page_size) {
549 match get_project_versions_paginated(
550 &self.cfg,
551 params.project.as_str(),
552 params.versions_page_offset,
553 params.versions_page_size,
554 None,
555 None,
556 None,
557 None,
558 )
559 .await?
560 .values
561 {
562 Some(values) => Ok(values),
563 None => Ok(vec![]),
564 }
565 } else {
566 Ok(get_project_versions(&self.cfg, params.project.as_str(), None).await?)
567 }
568 }
569
570 /// This method updates a Jira version with the given parameters
571 /// and returns the updated version
572 /// If the version is not found, it returns an error
573 /// If the version ID is not given, it returns an error
574 ///
575 /// # Arguments
576 ///
577 /// * `params` - A VersionCmdParams struct
578 ///
579 /// # Returns
580 ///
581 /// * A Result containing a Version struct or an Error<UpdateVersionError>
582 ///
583 /// # Examples
584 ///
585 /// ```no_run
586 /// use jira_v3_openapi::models::Version;
587 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdParams;
588 /// use jirust_cli::config::config_file::ConfigFile;
589 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdRunner;
590 /// use toml::Table;
591 ///
592 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
593 /// # tokio_test::block_on(async {
594 /// let cfg_file = ConfigFile::new("dXNlcm5hbWU6YXBpX2tleQ==".to_string(), "jira_url".to_string(), "standard_resolution".to_string(), "standard_resolution_comment".to_string(), Table::new());
595 /// let version_cmd_runner = VersionCmdRunner::new(&cfg_file);
596 /// let params = VersionCmdParams::new();
597 ///
598 /// let version = version_cmd_runner.update_jira_version(params).await?;
599 /// # Ok(())
600 /// # })
601 /// # }
602 /// ```
603 pub async fn update_jira_version(
604 &self,
605 params: VersionCmdParams,
606 ) -> Result<Version, Error<UpdateVersionError>> {
607 let release_date =
608 if Option::is_some(¶ms.version_released) && params.version_released.unwrap() {
609 if Option::is_some(¶ms.version_release_date) {
610 params.version_release_date
611 } else {
612 Some(Utc::now().format("%Y-%m-%d").to_string())
613 }
614 } else {
615 None
616 };
617 let version = Version {
618 id: Some(params.version_id.clone().expect("VersionID is mandatory!")),
619 name: params.version_name,
620 description: params.version_description,
621 start_date: params.version_start_date,
622 release_date,
623 archived: params.version_archived,
624 released: params.version_released,
625 ..Default::default()
626 };
627 update_version(
628 &self.cfg,
629 params.version_id.expect("VersionID is mandatory!").as_str(),
630 version,
631 )
632 .await
633 }
634
635 /// This method deletes a Jira version with the given parameters
636 /// and returns the status of the deletion
637 ///
638 /// # Arguments
639 ///
640 /// * `params` - A VersionCmdParams struct
641 ///
642 /// # Returns
643 ///
644 /// * A Result containing a serde_json::Value or an Error<DeleteAndReplaceVersionError>
645 ///
646 /// # Examples
647 ///
648 /// ```no_run
649 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdParams;
650 /// use jirust_cli::config::config_file::ConfigFile;
651 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdRunner;
652 /// use toml::Table;
653 ///
654 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
655 /// # tokio_test::block_on(async {
656 /// let cfg_file = ConfigFile::new("dXNlcm5hbWU6YXBpX2tleQ==".to_string(), "jira_url".to_string(), "standard_resolution".to_string(), "standard_resolution_comment".to_string(), Table::new());
657 /// let version_cmd_runner = VersionCmdRunner::new(&cfg_file);
658 /// let params = VersionCmdParams::new();
659 ///
660 /// let status = version_cmd_runner.delete_jira_version(params).await?;
661 /// # Ok(())
662 /// # })
663 /// # }
664 /// ```
665 pub async fn delete_jira_version(
666 &self,
667 params: VersionCmdParams,
668 ) -> Result<serde_json::Value, Error<DeleteAndReplaceVersionError>> {
669 match delete_and_replace_version(
670 &self.cfg,
671 params.version_id.expect("VersionID is mandatory!").as_str(),
672 DeleteAndReplaceVersionBean::new(),
673 )
674 .await
675 {
676 Ok(_) => Ok(serde_json::json!({"status": "success"})),
677 Err(e) => match e {
678 Error::Serde(_) => Ok(
679 serde_json::json!({"status": "success", "warning": "Version was deleted, some issues in deserializing response!"}),
680 ),
681 _ => Err(e),
682 },
683 }
684 }
685
686 /// This method retrieves the related work for a given version.
687 ///
688 /// # Arguments
689 ///
690 /// * `params` - The parameters for the command.
691 ///
692 /// # Returns
693 ///
694 /// A `Result` containing a vector of `VersionRelatedWork` or an error.
695 ///
696 /// # Examples
697 ///
698 /// ```no_run
699 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdParams;
700 /// use jirust_cli::config::config_file::ConfigFile;
701 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdRunner;
702 /// use toml::Table;
703 ///
704 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
705 /// # tokio_test::block_on(async {
706 /// let cfg_file = ConfigFile::new("dXNlcm5hbWU6YXBpX2tleQ==".to_string(), "jira_url".to_string(), "standard_resolution".to_string(), "standard_resolution_comment".to_string(), Table::new());
707 /// let version_cmd_runner = VersionCmdRunner::new(&cfg_file);
708 /// let params = VersionCmdParams::new();
709 ///
710 /// let items = version_cmd_runner.get_jira_version_related_work(params).await?;
711 /// # Ok(())
712 /// # })
713 /// # }
714 /// ```
715 pub async fn get_jira_version_related_work(
716 &self,
717 params: VersionCmdParams,
718 ) -> Result<Vec<VersionRelatedWork>, Error<GetRelatedWorkError>> {
719 get_related_work(
720 &self.cfg,
721 params.version_id.expect("VersionID is mandatory!").as_str(),
722 )
723 .await
724 }
725
726 /// Manage version related issues helper function
727 /// Use FuturesUnordered to manage multiple operation concurrently
728 ///
729 /// # Arguments
730 /// * `issue` - The issue to manage
731 /// * `user_data` - The user data
732 /// * `version` - The version
733 ///
734 /// # Returns
735 /// A Result containing a tuple with the issue key, status, resolution, and fix version or an error
736 ///
737 /// # Example
738 /// ```ignore
739 /// use jira_v3_openapi::models::{ User, Version };
740 /// use jirust_cli::config::config_file::ConfigFile;
741 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdRunner;
742 /// use toml::Table;
743 ///
744 /// let cfg_file = ConfigFile::new("dXNlcm5hbWU6YXBpX2tleQ==".to_string(), "jira_url".to_string(), "standard_resolution".to_string(), "standard_resolution_comment".to_string(), Table::new());
745 /// let version_cmd_runner = VersionCmdRunner::new(&cfg_file);
746 /// let issue_key = "ABC-123";
747 /// let user_data = Some(User::default());
748 /// let version = Version::default();
749 /// let result = self.manage_version_related_issues(issue_key, &user_data, &version).await;
750 /// ```
751 ///
752 #[cfg(any(windows, unix))]
753 async fn manage_version_related_issues(
754 &self,
755 issue: String,
756 user_data: &Option<User>,
757 version: &Version,
758 ) -> Result<(String, String, String, String), Box<dyn std::error::Error>> {
759 let all_transitions: Vec<IssueTransition> = match get_transitions(
760 &self.cfg,
761 issue.clone().as_str(),
762 None,
763 None,
764 None,
765 Some(false),
766 None,
767 )
768 .await
769 {
770 Ok(transitions) => transitions.transitions.unwrap_or_default(),
771 Err(_) => {
772 return Ok((
773 issue,
774 "KO".to_string(),
775 "KO".to_string(),
776 "NO fixVersion set".to_string(),
777 ));
778 }
779 };
780 let transition_names: Vec<String> = self
781 .resolution_transition_name
782 .clone()
783 .expect("Transition name is required and must be set in the config file");
784 let resolve_transitions: Vec<IssueTransition> = all_transitions
785 .into_iter()
786 .filter(|t| transition_names.contains(&t.name.clone().unwrap_or("".to_string())))
787 .collect();
788 let transition_ids = resolve_transitions
789 .into_iter()
790 .map(|t| t.id.clone().unwrap_or("".to_string()))
791 .collect::<Vec<String>>();
792 let transitions = transition_ids
793 .into_iter()
794 .map(|id| {
795 Some(IssueTransition {
796 id: Some(id),
797 ..Default::default()
798 })
799 })
800 .collect::<Vec<Option<IssueTransition>>>();
801 let mut update_fields_hashmap: HashMap<String, Vec<FieldUpdateOperation>> = HashMap::new();
802 let mut transition_fields_hashmap: HashMap<String, Vec<FieldUpdateOperation>> =
803 HashMap::new();
804 let mut version_update_op = FieldUpdateOperation::new();
805 let mut version_resolution_update_field = HashMap::new();
806 let mut version_resolution_comment_op = FieldUpdateOperation::new();
807 let version_json: Value =
808 serde_json::from_str(serde_json::to_string(&version).unwrap().as_str())
809 .unwrap_or(Value::Null);
810 let resolution_value = self.resolution_value.clone();
811 let comment_value = self.resolution_comment.clone();
812 version_update_op.add = Some(Some(version_json));
813 version_resolution_update_field.insert("resolution".to_string(), resolution_value);
814 version_resolution_comment_op.add = Some(Some(comment_value));
815 update_fields_hashmap.insert("fixVersions".to_string(), vec![version_update_op]);
816 transition_fields_hashmap
817 .insert("comment".to_string(), vec![version_resolution_comment_op]);
818 let issue_update_data = IssueUpdateDetails {
819 fields: None,
820 history_metadata: None,
821 properties: None,
822 transition: None,
823 update: Some(update_fields_hashmap),
824 };
825 let mut transition_result: String = "KO".to_string();
826 if !Vec::is_empty(&transitions) {
827 for transition in transitions {
828 let issue_transition_data = IssueUpdateDetails {
829 fields: Some(version_resolution_update_field.clone()),
830 history_metadata: None,
831 properties: None,
832 transition: Some(transition.clone().unwrap()),
833 update: Some(transition_fields_hashmap.clone()),
834 };
835 match do_transition(&self.cfg, issue.clone().as_str(), issue_transition_data).await
836 {
837 Ok(_) => {
838 transition_result = "OK".to_string();
839 break;
840 }
841 Err(Error::Serde(e)) => {
842 if e.is_eof() {
843 transition_result = "OK".to_string();
844 break;
845 } else {
846 transition_result = "KO".to_string()
847 }
848 }
849 Err(_) => transition_result = "KO".to_string(),
850 }
851 }
852 }
853 let assign_result: String = match assign_issue(
854 &self.cfg,
855 issue.clone().as_str(),
856 user_data.clone().unwrap(),
857 )
858 .await
859 {
860 Ok(_) => "OK".to_string(),
861 Err(Error::Serde(e)) => {
862 if e.is_eof() {
863 "OK".to_string()
864 } else {
865 "KO".to_string()
866 }
867 }
868 Err(_) => "KO".to_string(),
869 };
870 let fixversion_result: String = match edit_issue(
871 &self.cfg,
872 issue.clone().as_str(),
873 issue_update_data,
874 Some(true),
875 None,
876 None,
877 Some(true),
878 None,
879 )
880 .await
881 {
882 Ok(_) => version.clone().name.unwrap_or("".to_string()),
883 Err(_) => "NO fixVersion set".to_string(),
884 };
885 Ok((issue, transition_result, assign_result, fixversion_result))
886 }
887}
888
889/// This struct defines the parameters for the Version commands
890///
891/// # Fields
892///
893/// * `project` - The project key, always **required**.
894/// * `project_id` - The project ID, optional.
895/// * `version_name` - The version name, optional.
896/// * `version_id` - The version ID, **required** for archive, delete, release and update.
897/// * `version_description` - The version description, optional.
898/// * `version_start_date` - The version start date, optional (default: today on create command).
899/// * `version_release_date` - The version release date, optional (default: today on release command).
900/// * `version_archived` - The version archived status, optional.
901/// * `version_released` - The version released status, optional.
902/// * `changelog_file` - The changelog file path, to be used for automatic description generation (changelog-based), optional: if set the script detects automatically the first tagged block in the changelog and use it as description
903/// * `resolve_issues` - The flag to resolve issues in the version, optional.
904/// * `versions_page_size` - The page size for the version, optional.
905/// * `versions_page_offset` - The page offset for the version, optional.
906pub struct VersionCmdParams {
907 pub project: String,
908 pub project_id: Option<i64>,
909 pub version_name: Option<String>,
910 pub version_id: Option<String>,
911 pub version_description: Option<String>,
912 pub version_start_date: Option<String>,
913 pub version_release_date: Option<String>,
914 pub version_archived: Option<bool>,
915 pub version_released: Option<bool>,
916 pub changelog_file: Option<String>,
917 pub transition_issues: Option<bool>,
918 pub transition_assignee: Option<String>,
919 pub versions_page_size: Option<i32>,
920 pub versions_page_offset: Option<i64>,
921}
922
923/// Implementation of the VersionCmdParams struct
924///
925/// # Methods
926///
927/// * `new` - returns a new VersionCmdParams struct
928/// * `merge_args` - merges the current version with the optional arguments
929/// * `mark_released` - marks the version as released
930/// * `mark_archived` - marks the version as archived
931impl VersionCmdParams {
932 /// This method returns a new VersionCmdParams struct
933 ///
934 /// # Returns
935 ///
936 /// * A VersionCmdParams struct
937 ///
938 /// # Examples
939 ///
940 /// ```
941 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdParams;
942 ///
943 /// let params = VersionCmdParams::new();
944 /// ```
945 pub fn new() -> VersionCmdParams {
946 VersionCmdParams {
947 project: "".to_string(),
948 project_id: None,
949 version_name: None,
950 version_id: None,
951 version_description: None,
952 version_start_date: None,
953 version_release_date: None,
954 version_archived: None,
955 version_released: None,
956 changelog_file: None,
957 transition_issues: None,
958 transition_assignee: None,
959 versions_page_size: None,
960 versions_page_offset: None,
961 }
962 }
963
964 /// This method merges the current version with the optional arguments
965 /// and returns a VersionCmdParams struct
966 /// If the optional arguments are not given, it uses the current version values
967 ///
968 /// # Arguments
969 ///
970 /// * `current_version` - A Version struct
971 /// * `opt_args` - An Option<&VersionArgs> struct
972 ///
973 /// # Returns
974 ///
975 /// * A VersionCmdParams struct
976 ///
977 /// # Examples
978 ///
979 /// ```
980 /// use jira_v3_openapi::models::Version;
981 /// use jirust_cli::args::commands::{VersionArgs, VersionActionValues, PaginationArgs, OutputArgs};
982 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdParams;
983 ///
984 /// let mut current_version: Version = Version::new();
985 /// current_version.id = Some("12345".to_string());
986 /// current_version.project_id = Some(9876);
987 /// current_version.project = Some("TEST_PROJECT".to_string());
988 /// current_version.name = Some("v1.0".to_string());
989 /// current_version.description = Some("This is the first version".to_string());
990 ///
991 /// let opt_args = VersionArgs {
992 /// version_act: VersionActionValues::List,
993 /// project_key: "project_key".to_string(),
994 /// project_id: None,
995 /// version_id: Some("97531".to_string()),
996 /// version_name: Some("version_name".to_string()),
997 /// version_description: Some("version_description".to_string()),
998 /// version_start_date: None,
999 /// version_release_date: None,
1000 /// version_archived: None,
1001 /// version_released: Some(true),
1002 /// changelog_file: None,
1003 /// pagination: PaginationArgs { page_size: None, page_offset: None },
1004 /// output: OutputArgs { output_format: None, output_type: None },
1005 /// transition_issues: None,
1006 /// transition_assignee: None,
1007 /// };
1008 ///
1009 /// let params = VersionCmdParams::merge_args(current_version, Some(&opt_args));
1010 ///
1011 /// assert_eq!(params.project, "TEST_PROJECT".to_string());
1012 /// assert_eq!(params.project_id, Some(9876));
1013 /// assert_eq!(params.version_id, Some("12345".to_string()));
1014 /// assert_eq!(params.version_name, Some("version_name".to_string()));
1015 /// assert_eq!(params.version_description, Some("version_description".to_string()));
1016 /// assert_eq!(params.version_released, Some(true));
1017 /// ```
1018 pub fn merge_args(
1019 current_version: Version,
1020 opt_args: Option<&VersionArgs>,
1021 ) -> VersionCmdParams {
1022 match opt_args {
1023 Some(args) => VersionCmdParams {
1024 project: current_version.project.clone().unwrap_or("".to_string()),
1025 project_id: current_version.project_id,
1026 version_id: current_version.id,
1027 version_name: if Option::is_some(&args.version_name) {
1028 args.version_name.clone()
1029 } else {
1030 current_version.name
1031 },
1032 version_description: if Option::is_some(&args.version_description) {
1033 args.version_description.clone()
1034 } else {
1035 current_version.description
1036 },
1037 version_start_date: if Option::is_some(&args.version_start_date) {
1038 args.version_start_date.clone()
1039 } else {
1040 current_version.start_date
1041 },
1042 version_release_date: if Option::is_some(&args.version_release_date) {
1043 args.version_release_date.clone()
1044 } else {
1045 current_version.release_date
1046 },
1047 version_archived: if Option::is_some(&args.version_archived) {
1048 args.version_archived
1049 } else {
1050 current_version.archived
1051 },
1052 version_released: if Option::is_some(&args.version_released) {
1053 args.version_released
1054 } else {
1055 current_version.released
1056 },
1057 changelog_file: None,
1058 transition_issues: None,
1059 transition_assignee: None,
1060 versions_page_size: None,
1061 versions_page_offset: None,
1062 },
1063 None => VersionCmdParams {
1064 project: current_version.project.clone().unwrap_or("".to_string()),
1065 project_id: current_version.project_id,
1066 version_id: current_version.id,
1067 version_name: current_version.name,
1068 version_description: current_version.description,
1069 version_start_date: current_version.start_date,
1070 version_release_date: current_version.release_date,
1071 version_archived: current_version.archived,
1072 version_released: current_version.released,
1073 changelog_file: None,
1074 transition_issues: None,
1075 transition_assignee: None,
1076 versions_page_size: None,
1077 versions_page_offset: None,
1078 },
1079 }
1080 }
1081
1082 /// This method marks the version as released
1083 /// and returns a VersionCmdParams struct
1084 /// It sets the version_released and version_release_date fields
1085 /// with the current date
1086 ///
1087 /// # Arguments
1088 ///
1089 /// * `version` - A Version struct
1090 ///
1091 /// # Returns
1092 ///
1093 /// * A VersionCmdParams struct
1094 ///
1095 /// # Examples
1096 ///
1097 /// ```
1098 /// use jira_v3_openapi::models::Version;
1099 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdParams;
1100 ///
1101 /// let mut version: Version = Version::new();
1102 /// version.id = Some("12345".to_string());
1103 /// version.project_id = Some(9876);
1104 /// version.project = Some("TEST_PROJECT".to_string());
1105 /// version.name = Some("v1.0".to_string());
1106 /// version.description = Some("This is the first version".to_string());
1107 ///
1108 /// assert_eq!(version.released, None);
1109 ///
1110 /// let params = VersionCmdParams::mark_released(version);
1111 ///
1112 /// assert_eq!(params.version_released, Some(true));
1113 /// ```
1114 pub fn mark_released(version: Version) -> VersionCmdParams {
1115 let mut version_to_release = Self::merge_args(version, None);
1116 version_to_release.version_released = Some(true);
1117 version_to_release.version_release_date = Some(Utc::now().format("%Y-%m-%d").to_string());
1118 version_to_release
1119 }
1120
1121 /// This method marks the version as archived
1122 /// and returns a VersionCmdParams struct
1123 ///
1124 /// # Arguments
1125 ///
1126 /// * `version` - A Version struct
1127 ///
1128 /// # Returns
1129 ///
1130 /// * A VersionCmdParams struct
1131 ///
1132 /// # Examples
1133 ///
1134 /// ```
1135 /// use jira_v3_openapi::models::Version;
1136 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdParams;
1137 ///
1138 /// let mut version: Version = Version::new();
1139 /// version.id = Some("12345".to_string());
1140 /// version.project_id = Some(9876);
1141 /// version.project = Some("TEST_PROJECT".to_string());
1142 /// version.name = Some("v1.0".to_string());
1143 /// version.description = Some("This is the first version".to_string());
1144 ///
1145 /// assert_eq!(version.archived, None);
1146 ///
1147 /// let params = VersionCmdParams::mark_archived(version);
1148 ///
1149 /// assert_eq!(params.version_archived, Some(true));
1150 /// ```
1151 ///
1152 pub fn mark_archived(version: Version) -> VersionCmdParams {
1153 let mut version_to_archive = Self::merge_args(version, None);
1154 version_to_archive.version_archived = Some(true);
1155 version_to_archive
1156 }
1157}
1158
1159/// Implementation of the From trait for the VersionArgs struct
1160/// This implementation allows the conversion of a VersionArgs struct to a VersionCmdParams struct.
1161impl From<&VersionArgs> for VersionCmdParams {
1162 /// This method converts the VersionArgs struct to a VersionCmdParams struct
1163 /// and returns a VersionCmdParams struct
1164 ///
1165 /// # Arguments
1166 ///
1167 /// * `args` - A VersionArgs struct
1168 ///
1169 /// # Returns
1170 ///
1171 /// * A VersionCmdParams struct
1172 ///
1173 /// # Examples
1174 ///
1175 /// ```
1176 /// use jirust_cli::args::commands::{VersionActionValues, VersionArgs, PaginationArgs, OutputArgs};
1177 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdParams;
1178 ///
1179 /// let version_args = VersionArgs {
1180 /// version_act: VersionActionValues::List,
1181 /// project_key: "project_key".to_string(),
1182 /// project_id: None,
1183 /// version_id: None,
1184 /// version_name: Some("version_name".to_string()),
1185 /// version_description: Some("version_description".to_string()),
1186 /// version_start_date: None,
1187 /// version_release_date: None,
1188 /// version_archived: None,
1189 /// version_released: None,
1190 /// changelog_file: None,
1191 /// pagination: PaginationArgs { page_size: Some(10), page_offset: Some(0) },
1192 /// output: OutputArgs { output_format: None, output_type: None },
1193 /// transition_issues: None,
1194 /// transition_assignee: None,
1195 /// };
1196 ///
1197 /// let params = VersionCmdParams::from(&version_args);
1198 ///
1199 /// assert_eq!(params.project, "project_key".to_string());
1200 /// assert_eq!(params.version_name, Some("version_name".to_string()));
1201 /// assert_eq!(params.version_description, Some("version_description".to_string()));
1202 /// assert_eq!(params.versions_page_size, Some(10));
1203 /// assert_eq!(params.versions_page_offset, Some(0));
1204 /// ```
1205 fn from(args: &VersionArgs) -> Self {
1206 VersionCmdParams {
1207 project: args.project_key.clone(),
1208 project_id: args.project_id,
1209 version_name: args.version_name.clone(),
1210 version_id: args.version_id.clone(),
1211 version_description: args.version_description.clone(),
1212 version_start_date: Some(
1213 args.version_start_date
1214 .clone()
1215 .unwrap_or(Utc::now().format("%Y-%m-%d").to_string()),
1216 ),
1217 version_release_date: args.version_release_date.clone(),
1218 version_archived: args.version_archived,
1219 version_released: args.version_released,
1220 changelog_file: args.changelog_file.clone(),
1221 transition_issues: args.transition_issues,
1222 transition_assignee: args.transition_assignee.clone(),
1223 versions_page_size: args.pagination.page_size,
1224 versions_page_offset: args.pagination.page_offset,
1225 }
1226 }
1227}
1228
1229/// Implementation of the Default trait for the VersionCmdParams struct
1230impl Default for VersionCmdParams {
1231 /// This method returns a VersionCmdParams struct with default values
1232 /// and returns a VersionCmdParams struct
1233 ///
1234 /// # Returns
1235 ///
1236 /// * A VersionCmdParams struct initialized with default values
1237 ///
1238 /// # Examples
1239 ///
1240 /// ```
1241 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdParams;
1242 ///
1243 /// let params = VersionCmdParams::default();
1244 ///
1245 /// assert_eq!(params.project, "".to_string());
1246 /// assert_eq!(params.project_id, None);
1247 /// assert_eq!(params.version_name, None);
1248 /// assert_eq!(params.version_id, None);
1249 /// assert_eq!(params.version_description, None);
1250 /// assert_eq!(params.version_start_date, None);
1251 /// assert_eq!(params.version_release_date, None);
1252 /// assert_eq!(params.version_archived, None);
1253 /// assert_eq!(params.version_released, None);
1254 /// assert_eq!(params.changelog_file, None);
1255 /// assert_eq!(params.transition_issues, None);
1256 /// assert_eq!(params.transition_assignee, None);
1257 /// assert_eq!(params.versions_page_size, None);
1258 /// assert_eq!(params.versions_page_offset, None);
1259 /// ```
1260 fn default() -> Self {
1261 VersionCmdParams::new()
1262 }
1263}
1264
1265#[cfg_attr(test, automock)]
1266#[async_trait(?Send)]
1267pub trait VersionCmdRunnerApi: Send + Sync {
1268 async fn create_jira_version(
1269 &self,
1270 params: VersionCmdParams,
1271 ) -> Result<(Version, Option<Vec<(String, String, String, String)>>), Box<dyn std::error::Error>>;
1272
1273 async fn list_jira_versions(
1274 &self,
1275 params: VersionCmdParams,
1276 ) -> Result<Vec<Version>, Box<dyn std::error::Error>>;
1277
1278 async fn get_jira_version(
1279 &self,
1280 params: VersionCmdParams,
1281 ) -> Result<Version, Box<dyn std::error::Error>>;
1282
1283 async fn update_jira_version(
1284 &self,
1285 params: VersionCmdParams,
1286 ) -> Result<Version, Box<dyn std::error::Error>>;
1287
1288 async fn delete_jira_version(
1289 &self,
1290 params: VersionCmdParams,
1291 ) -> Result<(), Box<dyn std::error::Error>>;
1292
1293 async fn get_jira_version_related_work(
1294 &self,
1295 params: VersionCmdParams,
1296 ) -> Result<Vec<VersionRelatedWork>, Error<GetRelatedWorkError>>;
1297}
1298
1299#[async_trait(?Send)]
1300impl VersionCmdRunnerApi for VersionCmdRunner {
1301 async fn create_jira_version(
1302 &self,
1303 params: VersionCmdParams,
1304 ) -> Result<(Version, Option<Vec<(String, String, String, String)>>), Box<dyn std::error::Error>>
1305 {
1306 VersionCmdRunner::create_jira_version(self, params).await
1307 }
1308
1309 async fn list_jira_versions(
1310 &self,
1311 params: VersionCmdParams,
1312 ) -> Result<Vec<Version>, Box<dyn std::error::Error>> {
1313 VersionCmdRunner::list_jira_versions(self, params).await
1314 }
1315
1316 async fn get_jira_version(
1317 &self,
1318 params: VersionCmdParams,
1319 ) -> Result<Version, Box<dyn std::error::Error>> {
1320 VersionCmdRunner::get_jira_version(self, params)
1321 .await
1322 .map_err(|err| Box::new(err) as Box<dyn std::error::Error>)
1323 }
1324
1325 async fn update_jira_version(
1326 &self,
1327 params: VersionCmdParams,
1328 ) -> Result<Version, Box<dyn std::error::Error>> {
1329 VersionCmdRunner::update_jira_version(self, params)
1330 .await
1331 .map_err(|err| Box::new(err) as Box<dyn std::error::Error>)
1332 }
1333
1334 async fn delete_jira_version(
1335 &self,
1336 params: VersionCmdParams,
1337 ) -> Result<(), Box<dyn std::error::Error>> {
1338 VersionCmdRunner::delete_jira_version(self, params)
1339 .await
1340 .map(|_| ())
1341 .map_err(|err| Box::new(err) as Box<dyn std::error::Error>)
1342 }
1343
1344 async fn get_jira_version_related_work(
1345 &self,
1346 params: VersionCmdParams,
1347 ) -> Result<Vec<VersionRelatedWork>, Error<GetRelatedWorkError>> {
1348 VersionCmdRunner::get_jira_version_related_work(self, params).await
1349 }
1350}