jirust_cli/runners/jira_cmd_runners/
link_issue_cmd_runner.rs

1use async_trait::async_trait;
2use jira_v3_openapi::apis::Error;
3use jira_v3_openapi::apis::configuration::Configuration;
4use jira_v3_openapi::apis::issue_links_api::link_issues;
5use jira_v3_openapi::models::{IssueLinkType, LinkIssueRequestJsonBean, LinkedIssue};
6use serde_json::Value;
7
8use crate::args::commands::LinkIssueArgs;
9use crate::config::config_file::{AuthData, ConfigFile};
10use crate::utils::changelog_extractor::ChangelogExtractor;
11
12#[cfg(test)]
13use mockall::automock;
14
15/// Link issue command runner
16/// This struct is responsible for running the link issue command
17/// It uses the Jira API to perform the operations
18///
19/// # Fields
20///
21/// * `cfg` - Configuration object
22pub struct LinkIssueCmdRunner {
23    /// Configuration object
24    cfg: Configuration,
25}
26
27/// Implementation of IssueCmdRunner
28///
29/// # Methods
30///
31/// * `new` - Creates a new instance of LinkIssueCmdRunner
32/// * `link_jira_issues` - Links Jira issues
33impl LinkIssueCmdRunner {
34    /// Creates a new instance of LinkIssueCmdRunner
35    ///
36    /// # Arguments
37    ///
38    /// * `cfg_file` - Configuration file
39    ///
40    /// # Returns
41    ///
42    /// * `LinkIssueCmdRunner` - Instance of LinkIssueCmdRunner
43    ///
44    /// # Examples
45    ///
46    /// ```
47    /// use jirust_cli::config::config_file::ConfigFile;
48    /// use jirust_cli::runners::jira_cmd_runners::link_issue_cmd_runner::LinkIssueCmdRunner;
49    /// use toml::Table;
50    ///
51    /// let cfg_file = ConfigFile::new("dXNlcm5hbWU6YXBpX2tleQ==".to_string(), "jira_url".to_string(), "standard_resolution".to_string(), "standard_resolution_comment".to_string(), Table::new());
52    ///
53    /// let link_issue_cmd_runner = LinkIssueCmdRunner::new(&cfg_file);
54    /// ```
55    pub fn new(cfg_file: &ConfigFile) -> LinkIssueCmdRunner {
56        let mut config = Configuration::new();
57        let auth_data = AuthData::from_base64(cfg_file.get_auth_key());
58        config.base_path = cfg_file.get_jira_url().to_string();
59        config.basic_auth = Some((auth_data.0, Some(auth_data.1)));
60        LinkIssueCmdRunner { cfg: config }
61    }
62
63    /// Links Jira issues
64    ///
65    /// # Arguments
66    ///
67    /// * `params` - LinkIssueCmdParams struct
68    ///
69    /// # Returns
70    ///
71    /// * `Result<Value, Box<dyn std::error::Error>>` - Result of the operation
72    ///
73    /// # Examples
74    ///
75    /// ```no_run
76    /// use jirust_cli::runners::jira_cmd_runners::link_issue_cmd_runner::{LinkIssueCmdRunner, LinkIssueCmdParams};
77    /// use jirust_cli::config::config_file::ConfigFile;
78    /// use toml::Table;
79    /// # use std::error::Error;
80    ///
81    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
82    /// # tokio_test::block_on(async {
83    /// let cfg_file = ConfigFile::new("dXNlcm5hbWU6YXBpX2tleQ==".to_string(), "jira_url".to_string(), "standard_resolution".to_string(), "standard_resolution_comment".to_string(), Table::new());
84    /// let link_issue_cmd_runner = LinkIssueCmdRunner::new(&cfg_file);
85    /// let mut params = LinkIssueCmdParams::new();
86    /// params.origin_issue_key = "ISSUE-1".to_string();
87    /// params.destination_issue_key = Some("ISSUE-2".to_string());
88    /// params.link_type = "Blocks".to_string();
89    ///
90    /// let result = link_issue_cmd_runner.link_jira_issues(params).await?;
91    /// # Ok(())
92    /// # })
93    /// # }
94    /// ```
95    pub async fn link_jira_issues(
96        &self,
97        params: LinkIssueCmdParams,
98    ) -> Result<Value, Box<dyn std::error::Error>> {
99        let mut link_requests: Vec<LinkIssueRequestJsonBean> = Vec::new();
100        if params.destination_issue_key.is_some() {
101            let link_request = LinkIssueRequestJsonBean {
102                comment: None,
103                inward_issue: Box::new(LinkedIssue {
104                    key: Some(params.origin_issue_key),
105                    id: None,
106                    fields: None,
107                    param_self: None,
108                }),
109                outward_issue: Box::new(LinkedIssue {
110                    key: params.destination_issue_key,
111                    id: None,
112                    fields: None,
113                    param_self: None,
114                }),
115                r#type: Box::new(IssueLinkType {
116                    name: Some(params.link_type),
117                    inward: None,
118                    outward: None,
119                    id: None,
120                    param_self: None,
121                }),
122            };
123            link_requests.push(link_request);
124        } else {
125            let p_key = if let Some(key) = &params.project_key {
126                key
127            } else {
128                return Err(Box::new(std::io::Error::other(
129                    "Error linking issues: Empty project key".to_string(),
130                )));
131            };
132            let changelog_extractor = ChangelogExtractor::new(params.changelog_file.unwrap());
133            let version_data: Option<String> = Some(
134                changelog_extractor
135                    .extract_version_changelog()
136                    .unwrap_or_default(),
137            );
138            if let Some(version_data) = version_data {
139                let issues = changelog_extractor
140                    .extract_issues_from_changelog(&version_data, p_key)
141                    .unwrap_or_default();
142                link_requests = issues
143                    .iter()
144                    .map(|issue| LinkIssueRequestJsonBean {
145                        comment: None,
146                        inward_issue: Box::new(LinkedIssue {
147                            key: Some(params.origin_issue_key.clone()),
148                            id: None,
149                            fields: None,
150                            param_self: None,
151                        }),
152                        outward_issue: Box::new(LinkedIssue {
153                            key: Some(issue.clone()),
154                            id: None,
155                            fields: None,
156                            param_self: None,
157                        }),
158                        r#type: Box::new(IssueLinkType {
159                            name: Some(params.link_type.clone()),
160                            inward: None,
161                            outward: None,
162                            id: None,
163                            param_self: None,
164                        }),
165                    })
166                    .collect();
167            } else {
168                return Err(Box::new(std::io::Error::other(
169                    "Error linking issues: No destination issue key found in changelog".to_string(),
170                )));
171            }
172        };
173
174        let mut link_result: Value = Value::String("Linking OK".to_string());
175
176        for link_issue_request_json_bean in link_requests {
177            match link_issues(&self.cfg, link_issue_request_json_bean).await {
178                Ok(_) => {}
179                Err(Error::Serde(_)) => {}
180                Err(_) => {
181                    link_result = Value::String("Linking KO".to_string());
182                }
183            };
184        }
185        Ok(link_result)
186    }
187}
188
189/// Link issue command parameters
190///
191/// # Fields
192///
193/// * `project_key` - Jira project key
194/// * `origin_issue_key` - Jira origin issue key
195/// * `destination_issue_key` - Jira destination issue key
196/// * `link_type` - Jira link type
197/// * `changelog_file` - Changelog file
198pub struct LinkIssueCmdParams {
199    /// Jira project key
200    pub project_key: Option<String>,
201    /// Jira issue key
202    pub origin_issue_key: String,
203    /// Jira issue key
204    pub destination_issue_key: Option<String>,
205    /// Jira issue fields
206    pub link_type: String,
207    /// Jira issue transition
208    pub changelog_file: Option<String>,
209}
210
211/// Implementation of LinkIssueCmdParams struct
212///
213/// # Methods
214///
215/// * `new` - Creates a new LinkIssueCmdParams instance
216impl LinkIssueCmdParams {
217    /// Creates a new LinkIssueCmdParams instance
218    ///
219    /// # Returns
220    ///
221    /// * `LinkIssueCmdParams` - Issue command parameters
222    ///
223    /// # Examples
224    ///
225    /// ```
226    /// use jirust_cli::runners::jira_cmd_runners::link_issue_cmd_runner::LinkIssueCmdParams;
227    ///
228    /// let params = LinkIssueCmdParams::new();
229    /// ```
230    pub fn new() -> LinkIssueCmdParams {
231        LinkIssueCmdParams {
232            project_key: None,
233            origin_issue_key: "".to_string(),
234            destination_issue_key: None,
235            link_type: "".to_string(),
236            changelog_file: None,
237        }
238    }
239}
240
241/// Implementation of From trait for LinkIssueCmdParams struct
242/// to convert LinkIssueArgs struct to LinkIssueCmdParams struct
243impl From<&LinkIssueArgs> for LinkIssueCmdParams {
244    /// Converts LinkIssueArgs struct to LinkIssueCmdParams struct
245    /// to create a new LinkIssueCmdParams instance
246    ///
247    /// # Arguments
248    ///
249    /// * `value` - LinkIssueArgs struct
250    ///
251    /// # Returns
252    ///
253    /// * `LinkIssueArgs` - Link issue command parameters
254    ///
255    /// # Examples
256    ///
257    /// ```
258    /// use jirust_cli::runners::jira_cmd_runners::link_issue_cmd_runner::LinkIssueCmdParams;
259    /// use jirust_cli::args::commands::{LinkIssueArgs, LinkIssueActionValues};
260    /// use std::collections::HashMap;
261    /// use serde_json::Value;
262    ///
263    /// let link_issue_args = LinkIssueArgs {
264    ///    link_act: LinkIssueActionValues::Create,
265    ///    project_key: Some("project_key".to_string()),
266    ///    origin_issue_key: "origin_issue_key".to_string(),
267    ///    destination_issue_key: Some("destination_issue_key".to_string()),
268    ///    link_type: "link_type".to_string(),
269    ///    changelog_file: None,
270    /// };
271    ///
272    /// let params = LinkIssueCmdParams::from(&link_issue_args);
273    ///
274    /// assert_eq!(params.project_key, Some("project_key".to_string()));
275    /// assert_eq!(params.origin_issue_key, "origin_issue_key".to_string());
276    /// assert_eq!(params.destination_issue_key, Some("destination_issue_key".to_string()));
277    /// assert_eq!(params.link_type, "link_type".to_string());
278    /// assert_eq!(params.changelog_file, None);
279    ///
280    /// ```
281    fn from(value: &LinkIssueArgs) -> Self {
282        if (value.destination_issue_key.is_none() && value.changelog_file.is_none())
283            || (value.destination_issue_key.is_some() && value.changelog_file.is_some())
284        {
285            panic!("Either destination issue key or changelog file is required");
286        }
287        if value.changelog_file.is_some() && value.project_key.is_none() {
288            panic!("Project key is required when changelog file is provided");
289        }
290        LinkIssueCmdParams {
291            project_key: value.project_key.clone(),
292            origin_issue_key: value.origin_issue_key.clone(),
293            destination_issue_key: value.destination_issue_key.clone(),
294            link_type: value.link_type.clone(),
295            changelog_file: value.changelog_file.clone(),
296        }
297    }
298}
299
300/// Default implementation for IssueCmdParams struct
301impl Default for LinkIssueCmdParams {
302    /// Creates a default LinkIssueCmdParams instance
303    ///
304    /// # Returns
305    ///
306    /// A LinkIssueCmdParams instance with default values
307    ///
308    /// # Examples
309    ///
310    /// ```
311    /// use jirust_cli::runners::jira_cmd_runners::link_issue_cmd_runner::LinkIssueCmdParams;
312    ///
313    /// let params = LinkIssueCmdParams::default();
314    ///
315    /// assert_eq!(params.project_key, None);
316    /// assert_eq!(params.origin_issue_key, "".to_string());
317    /// assert_eq!(params.destination_issue_key, None);
318    /// assert_eq!(params.link_type, "".to_string());
319    /// assert_eq!(params.changelog_file, None);
320    /// ```
321    fn default() -> Self {
322        LinkIssueCmdParams::new()
323    }
324}
325
326/// API contract for linking Jira issues together.
327#[cfg_attr(test, automock)]
328#[async_trait(?Send)]
329pub trait LinkIssueCmdRunnerApi: Send + Sync {
330    /// Creates a link between two Jira issues.
331    async fn link_jira_issues(
332        &self,
333        params: LinkIssueCmdParams,
334    ) -> Result<Value, Box<dyn std::error::Error>>;
335}
336
337#[async_trait(?Send)]
338impl LinkIssueCmdRunnerApi for LinkIssueCmdRunner {
339    async fn link_jira_issues(
340        &self,
341        params: LinkIssueCmdParams,
342    ) -> Result<Value, Box<dyn std::error::Error>> {
343        LinkIssueCmdRunner::link_jira_issues(self, params).await
344    }
345}