1use derive_builder::Builder;
13use reqwest::Method;
14use std::borrow::Cow;
15
16use crate::api::{Endpoint, NoPagination, ReturnsJsonResponse};
17use serde::Serialize;
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
23pub struct IssueRelation {
24 pub id: u64,
26 pub issue_id: u64,
28 pub issue_to_id: u64,
30 pub relation_type: IssueRelationType,
32 pub delay: Option<u64>,
34}
35
36#[derive(Debug, Clone, Builder)]
38#[builder(setter(strip_option))]
39pub struct ListIssueRelations {
40 issue_id: u64,
42}
43
44impl ReturnsJsonResponse for ListIssueRelations {}
45impl NoPagination for ListIssueRelations {}
46
47impl ListIssueRelations {
48 #[must_use]
50 pub fn builder() -> ListIssueRelationsBuilder {
51 ListIssueRelationsBuilder::default()
52 }
53}
54
55impl Endpoint for ListIssueRelations {
56 fn method(&self) -> Method {
57 Method::GET
58 }
59
60 fn endpoint(&self) -> Cow<'static, str> {
61 format!("issues/{}/relations.json", self.issue_id).into()
62 }
63}
64
65#[derive(Debug, Clone, Builder)]
67#[builder(setter(strip_option))]
68pub struct GetIssueRelation {
69 id: u64,
71}
72
73impl ReturnsJsonResponse for GetIssueRelation {}
74impl NoPagination for GetIssueRelation {}
75
76impl GetIssueRelation {
77 #[must_use]
79 pub fn builder() -> GetIssueRelationBuilder {
80 GetIssueRelationBuilder::default()
81 }
82}
83
84impl Endpoint for GetIssueRelation {
85 fn method(&self) -> Method {
86 Method::GET
87 }
88
89 fn endpoint(&self) -> Cow<'static, str> {
90 format!("relations/{}.json", self.id).into()
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize, Serialize)]
96#[serde(rename_all = "snake_case")]
97pub enum IssueRelationType {
98 Relates,
100 Duplicates,
102 Duplicated,
104 Blocks,
106 Blocked,
108 Precedes,
110 Follows,
112 CopiedTo,
114 CopiedFrom,
116}
117
118#[serde_with::skip_serializing_none]
120#[derive(Debug, Clone, Builder, Serialize)]
121#[builder(setter(strip_option))]
122pub struct CreateIssueRelation {
123 #[serde(skip_serializing)]
125 issue_id: u64,
126 issue_to_id: u64,
128 relation_type: IssueRelationType,
130 #[builder(default)]
132 delay: Option<u64>,
133}
134
135impl ReturnsJsonResponse for CreateIssueRelation {}
136impl NoPagination for CreateIssueRelation {}
137
138impl CreateIssueRelation {
139 #[must_use]
141 pub fn builder() -> CreateIssueRelationBuilder {
142 CreateIssueRelationBuilder::default()
143 }
144}
145
146impl Endpoint for CreateIssueRelation {
147 fn method(&self) -> Method {
148 Method::POST
149 }
150
151 fn endpoint(&self) -> Cow<'static, str> {
152 format!("issues/{}/relations.json", self.issue_id).into()
153 }
154
155 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
156 Ok(Some((
157 "application/json",
158 serde_json::to_vec(&RelationWrapper::<CreateIssueRelation> {
159 relation: (*self).to_owned(),
160 })?,
161 )))
162 }
163}
164
165#[derive(Debug, Clone, Builder)]
167#[builder(setter(strip_option))]
168pub struct DeleteIssueRelation {
169 id: u64,
171}
172
173impl DeleteIssueRelation {
174 #[must_use]
176 pub fn builder() -> DeleteIssueRelationBuilder {
177 DeleteIssueRelationBuilder::default()
178 }
179}
180
181impl Endpoint for DeleteIssueRelation {
182 fn method(&self) -> Method {
183 Method::DELETE
184 }
185
186 fn endpoint(&self) -> Cow<'static, str> {
187 format!("relations/{}.json", self.id).into()
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
193pub struct RelationsWrapper<T> {
194 pub relations: Vec<T>,
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
201pub struct RelationWrapper<T> {
202 pub relation: T,
204}
205
206#[cfg(test)]
207mod test {
208 use super::*;
209 use crate::api::issues::test::ISSUES_LOCK;
210 use crate::api::issues::{CreateIssue, Issue, IssueWrapper};
211 use crate::api::test_helpers::with_project;
212 use pretty_assertions::assert_eq;
213 use std::error::Error;
214 use tokio::sync::RwLock;
215 use tracing_test::traced_test;
216
217 static ISSUE_RELATION_LOCK: RwLock<()> = RwLock::const_new(());
220
221 #[traced_test]
222 #[test]
223 fn test_list_issue_relations_no_pagination() -> Result<(), Box<dyn Error>> {
224 let _r_issue_relation = ISSUE_RELATION_LOCK.read();
225 dotenvy::dotenv()?;
226 let redmine = crate::api::Redmine::from_env(
227 reqwest::blocking::Client::builder()
228 .use_rustls_tls()
229 .build()?,
230 )?;
231 let endpoint = ListIssueRelations::builder().issue_id(50017).build()?;
232 redmine.json_response_body::<_, RelationsWrapper<IssueRelation>>(&endpoint)?;
233 Ok(())
234 }
235
236 #[traced_test]
237 #[test]
238 fn test_get_issue_relation() -> Result<(), Box<dyn Error>> {
239 let _r_issue_relation = ISSUE_RELATION_LOCK.read();
240 dotenvy::dotenv()?;
241 let redmine = crate::api::Redmine::from_env(
242 reqwest::blocking::Client::builder()
243 .use_rustls_tls()
244 .build()?,
245 )?;
246 let endpoint = GetIssueRelation::builder().id(10).build()?;
247 redmine.json_response_body::<_, RelationWrapper<IssueRelation>>(&endpoint)?;
248 Ok(())
249 }
250
251 #[function_name::named]
252 #[traced_test]
253 #[test]
254 fn test_create_issue_relation() -> Result<(), Box<dyn Error>> {
255 let _w_issues = ISSUES_LOCK.write();
256 let _w_issue_relation = ISSUE_RELATION_LOCK.write();
257 let name = format!("unittest_{}", function_name!());
258 with_project(&name, |redmine, project_id, _name| {
259 let create_issue1_endpoint = CreateIssue::builder()
260 .project_id(project_id)
261 .subject("Test issue 1")
262 .build()?;
263 let IssueWrapper { issue: issue1 }: IssueWrapper<Issue> =
264 redmine.json_response_body::<_, _>(&create_issue1_endpoint)?;
265 let create_issue2_endpoint = CreateIssue::builder()
266 .project_id(project_id)
267 .subject("Test issue 2")
268 .build()?;
269 let IssueWrapper { issue: issue2 }: IssueWrapper<Issue> =
270 redmine.json_response_body::<_, _>(&create_issue2_endpoint)?;
271 let create_endpoint = super::CreateIssueRelation::builder()
272 .issue_id(issue1.id)
273 .issue_to_id(issue2.id)
274 .relation_type(IssueRelationType::Relates)
275 .build()?;
276 redmine.json_response_body::<_, RelationWrapper<IssueRelation>>(&create_endpoint)?;
277 Ok(())
278 })?;
279 Ok(())
280 }
281
282 #[function_name::named]
283 #[traced_test]
284 #[test]
285 fn test_delete_issue_relation() -> Result<(), Box<dyn Error>> {
286 let _w_issues = ISSUES_LOCK.write();
287 let _w_issue_relation = ISSUE_RELATION_LOCK.write();
288 let name = format!("unittest_{}", function_name!());
289 with_project(&name, |redmine, project_id, _name| {
290 let create_issue1_endpoint = CreateIssue::builder()
291 .project_id(project_id)
292 .subject("Test issue 1")
293 .build()?;
294 let IssueWrapper { issue: issue1 }: IssueWrapper<Issue> =
295 redmine.json_response_body::<_, _>(&create_issue1_endpoint)?;
296 let create_issue2_endpoint = CreateIssue::builder()
297 .project_id(project_id)
298 .subject("Test issue 2")
299 .build()?;
300 let IssueWrapper { issue: issue2 }: IssueWrapper<Issue> =
301 redmine.json_response_body::<_, _>(&create_issue2_endpoint)?;
302 let create_endpoint = super::CreateIssueRelation::builder()
303 .issue_id(issue1.id)
304 .issue_to_id(issue2.id)
305 .relation_type(IssueRelationType::Relates)
306 .build()?;
307 let RelationWrapper { relation }: RelationWrapper<IssueRelation> =
308 redmine.json_response_body::<_, _>(&create_endpoint)?;
309 let id = relation.id;
310 let delete_endpoint = super::DeleteIssueRelation::builder().id(id).build()?;
311 redmine.ignore_response_body::<_>(&delete_endpoint)?;
312 Ok(())
313 })?;
314 Ok(())
315 }
316
317 #[traced_test]
322 #[test]
323 fn test_completeness_issue_relation_type() -> Result<(), Box<dyn Error>> {
324 let _r_issue_relation = ISSUE_RELATION_LOCK.read();
325 dotenvy::dotenv()?;
326 let redmine = crate::api::Redmine::from_env(
327 reqwest::blocking::Client::builder()
328 .use_rustls_tls()
329 .build()?,
330 )?;
331 let endpoint = ListIssueRelations::builder().issue_id(50017).build()?;
332 let RelationsWrapper { relations: values } =
333 redmine.json_response_body::<_, RelationsWrapper<serde_json::Value>>(&endpoint)?;
334 for value in values {
335 let o: IssueRelation = serde_json::from_value(value.clone())?;
336 let reserialized = serde_json::to_value(o)?;
337 assert_eq!(value, reserialized);
338 }
339 Ok(())
340 }
341}