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 /// Jira project key (required for all version operations).
908 pub project: String,
909 /// Jira project id when key is not available.
910 pub project_id: Option<i64>,
911 /// Version name to create or update.
912 pub version_name: Option<String>,
913 /// Version id required for archive, delete, release, and update operations.
914 pub version_id: Option<String>,
915 /// Human readable version description.
916 pub version_description: Option<String>,
917 /// Version start date in yyyy-mm-dd format.
918 pub version_start_date: Option<String>,
919 /// Version release date in yyyy-mm-dd format.
920 pub version_release_date: Option<String>,
921 /// Flag indicating whether the version is archived.
922 pub version_archived: Option<bool>,
923 /// Flag indicating whether the version is released.
924 pub version_released: Option<bool>,
925 /// Optional changelog file path used to build the description.
926 pub changelog_file: Option<String>,
927 /// Whether to transition issues found in the changelog.
928 pub transition_issues: Option<bool>,
929 /// Account id used when reassigning transitioned issues.
930 pub transition_assignee: Option<String>,
931 /// Page size used when listing versions.
932 pub versions_page_size: Option<i32>,
933 /// Page offset used when listing versions.
934 pub versions_page_offset: Option<i64>,
935}
936
937/// Implementation of the VersionCmdParams struct
938///
939/// # Methods
940///
941/// * `new` - returns a new VersionCmdParams struct
942/// * `merge_args` - merges the current version with the optional arguments
943/// * `mark_released` - marks the version as released
944/// * `mark_archived` - marks the version as archived
945impl VersionCmdParams {
946 /// This method returns a new VersionCmdParams struct
947 ///
948 /// # Returns
949 ///
950 /// * A VersionCmdParams struct
951 ///
952 /// # Examples
953 ///
954 /// ```
955 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdParams;
956 ///
957 /// let params = VersionCmdParams::new();
958 /// ```
959 pub fn new() -> VersionCmdParams {
960 VersionCmdParams {
961 project: "".to_string(),
962 project_id: None,
963 version_name: None,
964 version_id: None,
965 version_description: None,
966 version_start_date: None,
967 version_release_date: None,
968 version_archived: None,
969 version_released: None,
970 changelog_file: None,
971 transition_issues: None,
972 transition_assignee: None,
973 versions_page_size: None,
974 versions_page_offset: None,
975 }
976 }
977
978 /// This method merges the current version with the optional arguments
979 /// and returns a VersionCmdParams struct
980 /// If the optional arguments are not given, it uses the current version values
981 ///
982 /// # Arguments
983 ///
984 /// * `current_version` - A Version struct
985 /// * `opt_args` - An Option<&VersionArgs> struct
986 ///
987 /// # Returns
988 ///
989 /// * A VersionCmdParams struct
990 ///
991 /// # Examples
992 ///
993 /// ```
994 /// use jira_v3_openapi::models::Version;
995 /// use jirust_cli::args::commands::{VersionArgs, VersionActionValues, PaginationArgs, OutputArgs};
996 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdParams;
997 ///
998 /// let mut current_version: Version = Version::new();
999 /// current_version.id = Some("12345".to_string());
1000 /// current_version.project_id = Some(9876);
1001 /// current_version.project = Some("TEST_PROJECT".to_string());
1002 /// current_version.name = Some("v1.0".to_string());
1003 /// current_version.description = Some("This is the first version".to_string());
1004 ///
1005 /// let opt_args = VersionArgs {
1006 /// version_act: VersionActionValues::List,
1007 /// project_key: "project_key".to_string(),
1008 /// project_id: None,
1009 /// version_id: Some("97531".to_string()),
1010 /// version_name: Some("version_name".to_string()),
1011 /// version_description: Some("version_description".to_string()),
1012 /// version_start_date: None,
1013 /// version_release_date: None,
1014 /// version_archived: None,
1015 /// version_released: Some(true),
1016 /// changelog_file: None,
1017 /// pagination: PaginationArgs { page_size: None, page_offset: None },
1018 /// output: OutputArgs { output_format: None, output_type: None },
1019 /// transition_issues: None,
1020 /// transition_assignee: None,
1021 /// };
1022 ///
1023 /// let params = VersionCmdParams::merge_args(current_version, Some(&opt_args));
1024 ///
1025 /// assert_eq!(params.project, "TEST_PROJECT".to_string());
1026 /// assert_eq!(params.project_id, Some(9876));
1027 /// assert_eq!(params.version_id, Some("12345".to_string()));
1028 /// assert_eq!(params.version_name, Some("version_name".to_string()));
1029 /// assert_eq!(params.version_description, Some("version_description".to_string()));
1030 /// assert_eq!(params.version_released, Some(true));
1031 /// ```
1032 pub fn merge_args(
1033 current_version: Version,
1034 opt_args: Option<&VersionArgs>,
1035 ) -> VersionCmdParams {
1036 match opt_args {
1037 Some(args) => VersionCmdParams {
1038 project: current_version.project.clone().unwrap_or("".to_string()),
1039 project_id: current_version.project_id,
1040 version_id: current_version.id,
1041 version_name: if Option::is_some(&args.version_name) {
1042 args.version_name.clone()
1043 } else {
1044 current_version.name
1045 },
1046 version_description: if Option::is_some(&args.version_description) {
1047 args.version_description.clone()
1048 } else {
1049 current_version.description
1050 },
1051 version_start_date: if Option::is_some(&args.version_start_date) {
1052 args.version_start_date.clone()
1053 } else {
1054 current_version.start_date
1055 },
1056 version_release_date: if Option::is_some(&args.version_release_date) {
1057 args.version_release_date.clone()
1058 } else {
1059 current_version.release_date
1060 },
1061 version_archived: if Option::is_some(&args.version_archived) {
1062 args.version_archived
1063 } else {
1064 current_version.archived
1065 },
1066 version_released: if Option::is_some(&args.version_released) {
1067 args.version_released
1068 } else {
1069 current_version.released
1070 },
1071 changelog_file: None,
1072 transition_issues: None,
1073 transition_assignee: None,
1074 versions_page_size: None,
1075 versions_page_offset: None,
1076 },
1077 None => VersionCmdParams {
1078 project: current_version.project.clone().unwrap_or("".to_string()),
1079 project_id: current_version.project_id,
1080 version_id: current_version.id,
1081 version_name: current_version.name,
1082 version_description: current_version.description,
1083 version_start_date: current_version.start_date,
1084 version_release_date: current_version.release_date,
1085 version_archived: current_version.archived,
1086 version_released: current_version.released,
1087 changelog_file: None,
1088 transition_issues: None,
1089 transition_assignee: None,
1090 versions_page_size: None,
1091 versions_page_offset: None,
1092 },
1093 }
1094 }
1095
1096 /// This method marks the version as released
1097 /// and returns a VersionCmdParams struct
1098 /// It sets the version_released and version_release_date fields
1099 /// with the current date
1100 ///
1101 /// # Arguments
1102 ///
1103 /// * `version` - A Version struct
1104 ///
1105 /// # Returns
1106 ///
1107 /// * A VersionCmdParams struct
1108 ///
1109 /// # Examples
1110 ///
1111 /// ```
1112 /// use jira_v3_openapi::models::Version;
1113 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdParams;
1114 ///
1115 /// let mut version: Version = Version::new();
1116 /// version.id = Some("12345".to_string());
1117 /// version.project_id = Some(9876);
1118 /// version.project = Some("TEST_PROJECT".to_string());
1119 /// version.name = Some("v1.0".to_string());
1120 /// version.description = Some("This is the first version".to_string());
1121 ///
1122 /// assert_eq!(version.released, None);
1123 ///
1124 /// let params = VersionCmdParams::mark_released(version);
1125 ///
1126 /// assert_eq!(params.version_released, Some(true));
1127 /// ```
1128 pub fn mark_released(version: Version) -> VersionCmdParams {
1129 let mut version_to_release = Self::merge_args(version, None);
1130 version_to_release.version_released = Some(true);
1131 version_to_release.version_release_date = Some(Utc::now().format("%Y-%m-%d").to_string());
1132 version_to_release
1133 }
1134
1135 /// This method marks the version as archived
1136 /// and returns a VersionCmdParams struct
1137 ///
1138 /// # Arguments
1139 ///
1140 /// * `version` - A Version struct
1141 ///
1142 /// # Returns
1143 ///
1144 /// * A VersionCmdParams struct
1145 ///
1146 /// # Examples
1147 ///
1148 /// ```
1149 /// use jira_v3_openapi::models::Version;
1150 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdParams;
1151 ///
1152 /// let mut version: Version = Version::new();
1153 /// version.id = Some("12345".to_string());
1154 /// version.project_id = Some(9876);
1155 /// version.project = Some("TEST_PROJECT".to_string());
1156 /// version.name = Some("v1.0".to_string());
1157 /// version.description = Some("This is the first version".to_string());
1158 ///
1159 /// assert_eq!(version.archived, None);
1160 ///
1161 /// let params = VersionCmdParams::mark_archived(version);
1162 ///
1163 /// assert_eq!(params.version_archived, Some(true));
1164 /// ```
1165 ///
1166 pub fn mark_archived(version: Version) -> VersionCmdParams {
1167 let mut version_to_archive = Self::merge_args(version, None);
1168 version_to_archive.version_archived = Some(true);
1169 version_to_archive
1170 }
1171}
1172
1173/// Implementation of the From trait for the VersionArgs struct
1174/// This implementation allows the conversion of a VersionArgs struct to a VersionCmdParams struct.
1175impl From<&VersionArgs> for VersionCmdParams {
1176 /// This method converts the VersionArgs struct to a VersionCmdParams struct
1177 /// and returns a VersionCmdParams struct
1178 ///
1179 /// # Arguments
1180 ///
1181 /// * `args` - A VersionArgs struct
1182 ///
1183 /// # Returns
1184 ///
1185 /// * A VersionCmdParams struct
1186 ///
1187 /// # Examples
1188 ///
1189 /// ```
1190 /// use jirust_cli::args::commands::{VersionActionValues, VersionArgs, PaginationArgs, OutputArgs};
1191 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdParams;
1192 ///
1193 /// let version_args = VersionArgs {
1194 /// version_act: VersionActionValues::List,
1195 /// project_key: "project_key".to_string(),
1196 /// project_id: None,
1197 /// version_id: None,
1198 /// version_name: Some("version_name".to_string()),
1199 /// version_description: Some("version_description".to_string()),
1200 /// version_start_date: None,
1201 /// version_release_date: None,
1202 /// version_archived: None,
1203 /// version_released: None,
1204 /// changelog_file: None,
1205 /// pagination: PaginationArgs { page_size: Some(10), page_offset: Some(0) },
1206 /// output: OutputArgs { output_format: None, output_type: None },
1207 /// transition_issues: None,
1208 /// transition_assignee: None,
1209 /// };
1210 ///
1211 /// let params = VersionCmdParams::from(&version_args);
1212 ///
1213 /// assert_eq!(params.project, "project_key".to_string());
1214 /// assert_eq!(params.version_name, Some("version_name".to_string()));
1215 /// assert_eq!(params.version_description, Some("version_description".to_string()));
1216 /// assert_eq!(params.versions_page_size, Some(10));
1217 /// assert_eq!(params.versions_page_offset, Some(0));
1218 /// ```
1219 fn from(args: &VersionArgs) -> Self {
1220 VersionCmdParams {
1221 project: args.project_key.clone(),
1222 project_id: args.project_id,
1223 version_name: args.version_name.clone(),
1224 version_id: args.version_id.clone(),
1225 version_description: args.version_description.clone(),
1226 version_start_date: Some(
1227 args.version_start_date
1228 .clone()
1229 .unwrap_or(Utc::now().format("%Y-%m-%d").to_string()),
1230 ),
1231 version_release_date: args.version_release_date.clone(),
1232 version_archived: args.version_archived,
1233 version_released: args.version_released,
1234 changelog_file: args.changelog_file.clone(),
1235 transition_issues: args.transition_issues,
1236 transition_assignee: args.transition_assignee.clone(),
1237 versions_page_size: args.pagination.page_size,
1238 versions_page_offset: args.pagination.page_offset,
1239 }
1240 }
1241}
1242
1243/// Implementation of the Default trait for the VersionCmdParams struct
1244impl Default for VersionCmdParams {
1245 /// This method returns a VersionCmdParams struct with default values
1246 /// and returns a VersionCmdParams struct
1247 ///
1248 /// # Returns
1249 ///
1250 /// * A VersionCmdParams struct initialized with default values
1251 ///
1252 /// # Examples
1253 ///
1254 /// ```
1255 /// use jirust_cli::runners::jira_cmd_runners::version_cmd_runner::VersionCmdParams;
1256 ///
1257 /// let params = VersionCmdParams::default();
1258 ///
1259 /// assert_eq!(params.project, "".to_string());
1260 /// assert_eq!(params.project_id, None);
1261 /// assert_eq!(params.version_name, None);
1262 /// assert_eq!(params.version_id, None);
1263 /// assert_eq!(params.version_description, None);
1264 /// assert_eq!(params.version_start_date, None);
1265 /// assert_eq!(params.version_release_date, None);
1266 /// assert_eq!(params.version_archived, None);
1267 /// assert_eq!(params.version_released, None);
1268 /// assert_eq!(params.changelog_file, None);
1269 /// assert_eq!(params.transition_issues, None);
1270 /// assert_eq!(params.transition_assignee, None);
1271 /// assert_eq!(params.versions_page_size, None);
1272 /// assert_eq!(params.versions_page_offset, None);
1273 /// ```
1274 fn default() -> Self {
1275 VersionCmdParams::new()
1276 }
1277}
1278
1279/// API contract for performing Jira version operations.
1280#[cfg_attr(test, automock)]
1281#[async_trait(?Send)]
1282pub trait VersionCmdRunnerApi: Send + Sync {
1283 /// Creates a Jira version and optionally transitions referenced issues.
1284 async fn create_jira_version(
1285 &self,
1286 params: VersionCmdParams,
1287 ) -> Result<(Version, Option<Vec<(String, String, String, String)>>), Box<dyn std::error::Error>>;
1288
1289 /// Lists versions for a project with optional pagination.
1290 async fn list_jira_versions(
1291 &self,
1292 params: VersionCmdParams,
1293 ) -> Result<Vec<Version>, Box<dyn std::error::Error>>;
1294
1295 /// Retrieves a Jira version by id or name.
1296 async fn get_jira_version(
1297 &self,
1298 params: VersionCmdParams,
1299 ) -> Result<Version, Box<dyn std::error::Error>>;
1300
1301 /// Updates an existing Jira version.
1302 async fn update_jira_version(
1303 &self,
1304 params: VersionCmdParams,
1305 ) -> Result<Version, Box<dyn std::error::Error>>;
1306
1307 /// Deletes a Jira version, optionally replacing it.
1308 async fn delete_jira_version(
1309 &self,
1310 params: VersionCmdParams,
1311 ) -> Result<(), Box<dyn std::error::Error>>;
1312
1313 /// Retrieves work items related to a Jira version.
1314 async fn get_jira_version_related_work(
1315 &self,
1316 params: VersionCmdParams,
1317 ) -> Result<Vec<VersionRelatedWork>, Error<GetRelatedWorkError>>;
1318}
1319
1320#[async_trait(?Send)]
1321impl VersionCmdRunnerApi for VersionCmdRunner {
1322 async fn create_jira_version(
1323 &self,
1324 params: VersionCmdParams,
1325 ) -> Result<(Version, Option<Vec<(String, String, String, String)>>), Box<dyn std::error::Error>>
1326 {
1327 VersionCmdRunner::create_jira_version(self, params).await
1328 }
1329
1330 async fn list_jira_versions(
1331 &self,
1332 params: VersionCmdParams,
1333 ) -> Result<Vec<Version>, Box<dyn std::error::Error>> {
1334 VersionCmdRunner::list_jira_versions(self, params).await
1335 }
1336
1337 async fn get_jira_version(
1338 &self,
1339 params: VersionCmdParams,
1340 ) -> Result<Version, Box<dyn std::error::Error>> {
1341 VersionCmdRunner::get_jira_version(self, params)
1342 .await
1343 .map_err(|err| Box::new(err) as Box<dyn std::error::Error>)
1344 }
1345
1346 async fn update_jira_version(
1347 &self,
1348 params: VersionCmdParams,
1349 ) -> Result<Version, Box<dyn std::error::Error>> {
1350 VersionCmdRunner::update_jira_version(self, params)
1351 .await
1352 .map_err(|err| Box::new(err) as Box<dyn std::error::Error>)
1353 }
1354
1355 async fn delete_jira_version(
1356 &self,
1357 params: VersionCmdParams,
1358 ) -> Result<(), Box<dyn std::error::Error>> {
1359 VersionCmdRunner::delete_jira_version(self, params)
1360 .await
1361 .map(|_| ())
1362 .map_err(|err| Box::new(err) as Box<dyn std::error::Error>)
1363 }
1364
1365 async fn get_jira_version_related_work(
1366 &self,
1367 params: VersionCmdParams,
1368 ) -> Result<Vec<VersionRelatedWork>, Error<GetRelatedWorkError>> {
1369 VersionCmdRunner::get_jira_version_related_work(self, params).await
1370 }
1371}