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