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) = ¶ms.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}