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 let endpoint = ListIssueRelations::builder().issue_id(50017).build()?;
228 redmine.json_response_body::<_, RelationsWrapper<IssueRelation>>(&endpoint)?;
229 Ok(())
230 }
231
232 #[traced_test]
233 #[test]
234 fn test_get_issue_relation() -> Result<(), Box<dyn Error>> {
235 let _r_issue_relation = ISSUE_RELATION_LOCK.read();
236 dotenvy::dotenv()?;
237 let redmine = crate::api::Redmine::from_env()?;
238 let endpoint = GetIssueRelation::builder().id(10).build()?;
239 redmine.json_response_body::<_, RelationWrapper<IssueRelation>>(&endpoint)?;
240 Ok(())
241 }
242
243 #[function_name::named]
244 #[traced_test]
245 #[test]
246 fn test_create_issue_relation() -> Result<(), Box<dyn Error>> {
247 let _w_issues = ISSUES_LOCK.write();
248 let _w_issue_relation = ISSUE_RELATION_LOCK.write();
249 let name = format!("unittest_{}", function_name!());
250 with_project(&name, |redmine, project_id, _name| {
251 let create_issue1_endpoint = CreateIssue::builder()
252 .project_id(project_id)
253 .subject("Test issue 1")
254 .build()?;
255 let IssueWrapper { issue: issue1 }: IssueWrapper<Issue> =
256 redmine.json_response_body::<_, _>(&create_issue1_endpoint)?;
257 let create_issue2_endpoint = CreateIssue::builder()
258 .project_id(project_id)
259 .subject("Test issue 2")
260 .build()?;
261 let IssueWrapper { issue: issue2 }: IssueWrapper<Issue> =
262 redmine.json_response_body::<_, _>(&create_issue2_endpoint)?;
263 let create_endpoint = super::CreateIssueRelation::builder()
264 .issue_id(issue1.id)
265 .issue_to_id(issue2.id)
266 .relation_type(IssueRelationType::Relates)
267 .build()?;
268 redmine.json_response_body::<_, RelationWrapper<IssueRelation>>(&create_endpoint)?;
269 Ok(())
270 })?;
271 Ok(())
272 }
273
274 #[function_name::named]
275 #[traced_test]
276 #[test]
277 fn test_delete_issue_relation() -> Result<(), Box<dyn Error>> {
278 let _w_issues = ISSUES_LOCK.write();
279 let _w_issue_relation = ISSUE_RELATION_LOCK.write();
280 let name = format!("unittest_{}", function_name!());
281 with_project(&name, |redmine, project_id, _name| {
282 let create_issue1_endpoint = CreateIssue::builder()
283 .project_id(project_id)
284 .subject("Test issue 1")
285 .build()?;
286 let IssueWrapper { issue: issue1 }: IssueWrapper<Issue> =
287 redmine.json_response_body::<_, _>(&create_issue1_endpoint)?;
288 let create_issue2_endpoint = CreateIssue::builder()
289 .project_id(project_id)
290 .subject("Test issue 2")
291 .build()?;
292 let IssueWrapper { issue: issue2 }: IssueWrapper<Issue> =
293 redmine.json_response_body::<_, _>(&create_issue2_endpoint)?;
294 let create_endpoint = super::CreateIssueRelation::builder()
295 .issue_id(issue1.id)
296 .issue_to_id(issue2.id)
297 .relation_type(IssueRelationType::Relates)
298 .build()?;
299 let RelationWrapper { relation }: RelationWrapper<IssueRelation> =
300 redmine.json_response_body::<_, _>(&create_endpoint)?;
301 let id = relation.id;
302 let delete_endpoint = super::DeleteIssueRelation::builder().id(id).build()?;
303 redmine.ignore_response_body::<_>(&delete_endpoint)?;
304 Ok(())
305 })?;
306 Ok(())
307 }
308
309 #[traced_test]
314 #[test]
315 fn test_completeness_issue_relation_type() -> Result<(), Box<dyn Error>> {
316 let _r_issue_relation = ISSUE_RELATION_LOCK.read();
317 dotenvy::dotenv()?;
318 let redmine = crate::api::Redmine::from_env()?;
319 let endpoint = ListIssueRelations::builder().issue_id(50017).build()?;
320 let RelationsWrapper { relations: values } =
321 redmine.json_response_body::<_, RelationsWrapper<serde_json::Value>>(&endpoint)?;
322 for value in values {
323 let o: IssueRelation = serde_json::from_value(value.clone())?;
324 let reserialized = serde_json::to_value(o)?;
325 assert_eq!(value, reserialized);
326 }
327 Ok(())
328 }
329}