1use derive_builder::Builder;
12use reqwest::Method;
13use std::borrow::Cow;
14
15use crate::api::issues::AssigneeEssentials;
16use crate::api::projects::ProjectEssentials;
17use crate::api::{Endpoint, NoPagination, QueryParams, ReturnsJsonResponse};
18use serde::Serialize;
19
20#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
23pub struct IssueCategoryEssentials {
24 pub id: u64,
26 pub name: String,
28}
29
30impl From<IssueCategory> for IssueCategoryEssentials {
31 fn from(v: IssueCategory) -> Self {
32 IssueCategoryEssentials {
33 id: v.id,
34 name: v.name,
35 }
36 }
37}
38
39impl From<&IssueCategory> for IssueCategoryEssentials {
40 fn from(v: &IssueCategory) -> Self {
41 IssueCategoryEssentials {
42 id: v.id,
43 name: v.name.to_owned(),
44 }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
52pub struct IssueCategory {
53 pub id: u64,
55 pub name: String,
57 pub project: ProjectEssentials,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub assigned_to: Option<AssigneeEssentials>,
62}
63
64#[derive(Debug, Clone, Builder)]
66#[builder(setter(strip_option))]
67pub struct ListIssueCategories<'a> {
68 #[builder(setter(into))]
70 project_id_or_name: Cow<'a, str>,
71}
72
73impl ReturnsJsonResponse for ListIssueCategories<'_> {}
74impl NoPagination for ListIssueCategories<'_> {}
75
76impl<'a> ListIssueCategories<'a> {
77 #[must_use]
79 pub fn builder() -> ListIssueCategoriesBuilder<'a> {
80 ListIssueCategoriesBuilder::default()
81 }
82}
83
84impl Endpoint for ListIssueCategories<'_> {
85 fn method(&self) -> Method {
86 Method::GET
87 }
88
89 fn endpoint(&self) -> Cow<'static, str> {
90 format!("projects/{}/issue_categories.json", self.project_id_or_name).into()
91 }
92}
93
94#[derive(Debug, Clone, Builder)]
96#[builder(setter(strip_option))]
97pub struct GetIssueCategory {
98 id: u64,
100}
101
102impl ReturnsJsonResponse for GetIssueCategory {}
103impl NoPagination for GetIssueCategory {}
104
105impl GetIssueCategory {
106 #[must_use]
108 pub fn builder() -> GetIssueCategoryBuilder {
109 GetIssueCategoryBuilder::default()
110 }
111}
112
113impl Endpoint for GetIssueCategory {
114 fn method(&self) -> Method {
115 Method::GET
116 }
117
118 fn endpoint(&self) -> Cow<'static, str> {
119 format!("issue_categories/{}.json", &self.id).into()
120 }
121}
122
123#[serde_with::skip_serializing_none]
125#[derive(Debug, Clone, Builder, Serialize)]
126#[builder(setter(strip_option))]
127pub struct CreateIssueCategory<'a> {
128 #[serde(skip_serializing)]
130 #[builder(setter(into))]
131 project_id_or_name: Cow<'a, str>,
132 #[builder(setter(into))]
134 name: Cow<'a, str>,
135 #[builder(default)]
137 assigned_to_id: Option<u64>,
138}
139
140impl ReturnsJsonResponse for CreateIssueCategory<'_> {}
141impl NoPagination for CreateIssueCategory<'_> {}
142
143impl<'a> CreateIssueCategory<'a> {
144 #[must_use]
146 pub fn builder() -> CreateIssueCategoryBuilder<'a> {
147 CreateIssueCategoryBuilder::default()
148 }
149}
150
151impl Endpoint for CreateIssueCategory<'_> {
152 fn method(&self) -> Method {
153 Method::POST
154 }
155
156 fn endpoint(&self) -> Cow<'static, str> {
157 format!("projects/{}/issue_categories.json", self.project_id_or_name).into()
158 }
159
160 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
161 Ok(Some((
162 "application/json",
163 serde_json::to_vec(&IssueCategoryWrapper::<CreateIssueCategory> {
164 issue_category: (*self).to_owned(),
165 })?,
166 )))
167 }
168}
169
170#[serde_with::skip_serializing_none]
172#[derive(Debug, Clone, Builder, Serialize)]
173#[builder(setter(strip_option))]
174pub struct UpdateIssueCategory<'a> {
175 #[serde(skip_serializing)]
177 id: u64,
178 #[builder(setter(into), default)]
180 name: Option<Cow<'a, str>>,
181 #[builder(default)]
183 assigned_to_id: Option<u64>,
184}
185
186impl<'a> UpdateIssueCategory<'a> {
187 #[must_use]
189 pub fn builder() -> UpdateIssueCategoryBuilder<'a> {
190 UpdateIssueCategoryBuilder::default()
191 }
192}
193
194impl Endpoint for UpdateIssueCategory<'_> {
195 fn method(&self) -> Method {
196 Method::PUT
197 }
198
199 fn endpoint(&self) -> Cow<'static, str> {
200 format!("issue_categories/{}.json", self.id).into()
201 }
202
203 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
204 Ok(Some((
205 "application/json",
206 serde_json::to_vec(&IssueCategoryWrapper::<UpdateIssueCategory> {
207 issue_category: (*self).to_owned(),
208 })?,
209 )))
210 }
211}
212
213#[derive(Debug, Clone, Builder)]
215#[builder(setter(strip_option))]
216pub struct DeleteIssueCategory {
217 id: u64,
219 #[builder(default)]
221 reassign_to_id: Option<u64>,
222 #[builder(default)]
224 todo: Option<String>,
225}
226
227impl DeleteIssueCategory {
228 #[must_use]
230 pub fn builder() -> DeleteIssueCategoryBuilder {
231 DeleteIssueCategoryBuilder::default()
232 }
233}
234
235impl Endpoint for DeleteIssueCategory {
236 fn method(&self) -> Method {
237 Method::DELETE
238 }
239
240 fn endpoint(&self) -> Cow<'static, str> {
241 format!("issue_categories/{}.json", &self.id).into()
242 }
243
244 fn parameters(&self) -> QueryParams<'_> {
245 let mut params = QueryParams::default();
246 params.push_opt("reassign_to_id", self.reassign_to_id);
247 params.push_opt("todo", self.todo.as_ref());
248 params
249 }
250}
251
252#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
254pub struct IssueCategoriesWrapper<T> {
255 pub issue_categories: Vec<T>,
257}
258
259#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
262pub struct IssueCategoryWrapper<T> {
263 pub issue_category: T,
265}
266
267#[cfg(test)]
268mod test {
269 use super::*;
270 use crate::api::test_helpers::with_project;
271 use pretty_assertions::assert_eq;
272 use std::error::Error;
273 use tokio::sync::RwLock;
274 use tracing_test::traced_test;
275
276 static ISSUE_CATEGORY_LOCK: RwLock<()> = RwLock::const_new(());
279
280 #[traced_test]
281 #[test]
282 fn test_list_issue_categories_no_pagination() -> Result<(), Box<dyn Error>> {
283 let _r_issue_category = ISSUE_CATEGORY_LOCK.blocking_read();
284 dotenvy::dotenv()?;
285 let redmine = crate::api::Redmine::from_env(
286 reqwest::blocking::Client::builder()
287 .use_rustls_tls()
288 .build()?,
289 )?;
290 let endpoint = ListIssueCategories::builder()
291 .project_id_or_name("336")
292 .build()?;
293 redmine.json_response_body::<_, IssueCategoriesWrapper<IssueCategory>>(&endpoint)?;
294 Ok(())
295 }
296
297 #[traced_test]
298 #[test]
299 fn test_get_issue_category() -> Result<(), Box<dyn Error>> {
300 let _r_issue_category = ISSUE_CATEGORY_LOCK.blocking_read();
301 dotenvy::dotenv()?;
302 let redmine = crate::api::Redmine::from_env(
303 reqwest::blocking::Client::builder()
304 .use_rustls_tls()
305 .build()?,
306 )?;
307 let endpoint = GetIssueCategory::builder().id(10).build()?;
308 redmine.json_response_body::<_, IssueCategoryWrapper<IssueCategory>>(&endpoint)?;
309 Ok(())
310 }
311
312 #[function_name::named]
313 #[traced_test]
314 #[test]
315 fn test_create_issue_category() -> Result<(), Box<dyn Error>> {
316 let _w_issue_category = ISSUE_CATEGORY_LOCK.blocking_write();
317 let name = format!("unittest_{}", function_name!());
318 with_project(&name, |redmine, _id, name| {
319 let create_endpoint = super::CreateIssueCategory::builder()
320 .project_id_or_name(name)
321 .name("Unittest Issue Category")
322 .build()?;
323 redmine.ignore_response_body::<_>(&create_endpoint)?;
324 Ok(())
325 })?;
326 Ok(())
327 }
328
329 #[function_name::named]
330 #[traced_test]
331 #[test]
332 fn test_update_issue_category() -> Result<(), Box<dyn Error>> {
333 let _w_issue_category = ISSUE_CATEGORY_LOCK.blocking_write();
334 let name = format!("unittest_{}", function_name!());
335 with_project(&name, |redmine, _id, name| {
336 let create_endpoint = super::CreateIssueCategory::builder()
337 .project_id_or_name(name)
338 .name("Unittest Issue Category")
339 .build()?;
340 let IssueCategoryWrapper { issue_category }: IssueCategoryWrapper<IssueCategory> =
341 redmine.json_response_body::<_, _>(&create_endpoint)?;
342 let id = issue_category.id;
343 let update_endpoint = super::UpdateIssueCategory::builder()
344 .id(id)
345 .name("Renamed Unit-Test name")
346 .build()?;
347 redmine.ignore_response_body::<_>(&update_endpoint)?;
348 Ok(())
349 })?;
350 Ok(())
351 }
352
353 #[function_name::named]
354 #[traced_test]
355 #[test]
356 fn test_delete_issue_category() -> Result<(), Box<dyn Error>> {
357 let _w_issue_category = ISSUE_CATEGORY_LOCK.blocking_write();
358 let name = format!("unittest_{}", function_name!());
359 with_project(&name, |redmine, _id, name| {
360 let create_endpoint = super::CreateIssueCategory::builder()
361 .project_id_or_name(name)
362 .name("Unittest Issue Category")
363 .build()?;
364 let IssueCategoryWrapper { issue_category }: IssueCategoryWrapper<IssueCategory> =
365 redmine.json_response_body::<_, _>(&create_endpoint)?;
366 let id = issue_category.id;
367 let delete_endpoint = super::DeleteIssueCategory::builder().id(id).build()?;
368 redmine.ignore_response_body::<_>(&delete_endpoint)?;
369 Ok(())
370 })?;
371 Ok(())
372 }
373
374 #[traced_test]
379 #[test]
380 fn test_completeness_issue_category_type() -> Result<(), Box<dyn Error>> {
381 let _r_issue_category = ISSUE_CATEGORY_LOCK.blocking_read();
382 dotenvy::dotenv()?;
383 let redmine = crate::api::Redmine::from_env(
384 reqwest::blocking::Client::builder()
385 .use_rustls_tls()
386 .build()?,
387 )?;
388 let endpoint = ListIssueCategories::builder()
389 .project_id_or_name("336")
390 .build()?;
391 let IssueCategoriesWrapper {
392 issue_categories: values,
393 } = redmine
394 .json_response_body::<_, IssueCategoriesWrapper<serde_json::Value>>(&endpoint)?;
395 for value in values {
396 let o: IssueCategory = serde_json::from_value(value.clone())?;
397 let reserialized = serde_json::to_value(o)?;
398 assert_eq!(value, reserialized);
399 }
400 Ok(())
401 }
402}