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(&params.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(&params.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(&params.transition_issues) && params.transition_issues.unwrap() {
148                resolved_issues = changelog_extractor
149                    .extract_issues_from_changelog(
150                        &version_description.clone().unwrap(),
151                        &params.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(&params.version_released) && params.version_released.unwrap() {
160                if Option::is_some(&params.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(&params.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(&params.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(&params.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(&params.transition_issues) && params.transition_issues.unwrap() {
276                resolved_issues = changelog_extractor
277                    .extract_issues_from_changelog(
278                        &version_description.clone().unwrap(),
279                        &params.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(&params.version_released) && params.version_released.unwrap() {
288                if Option::is_some(&params.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(&params.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(&params.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(&params.version_released) && params.version_released.unwrap() {
609                if Option::is_some(&params.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}