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