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, 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}
220
221impl DeleteIssueCategory {
222 #[must_use]
224 pub fn builder() -> DeleteIssueCategoryBuilder {
225 DeleteIssueCategoryBuilder::default()
226 }
227}
228
229impl Endpoint for DeleteIssueCategory {
230 fn method(&self) -> Method {
231 Method::DELETE
232 }
233
234 fn endpoint(&self) -> Cow<'static, str> {
235 format!("issue_categories/{}.json", &self.id).into()
236 }
237}
238
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
241pub struct IssueCategoriesWrapper<T> {
242 pub issue_categories: Vec<T>,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
249pub struct IssueCategoryWrapper<T> {
250 pub issue_category: T,
252}
253
254#[cfg(test)]
255mod test {
256 use super::*;
257 use crate::api::test_helpers::with_project;
258 use pretty_assertions::assert_eq;
259 use std::error::Error;
260 use tokio::sync::RwLock;
261 use tracing_test::traced_test;
262
263 static ISSUE_CATEGORY_LOCK: RwLock<()> = RwLock::const_new(());
266
267 #[traced_test]
268 #[test]
269 fn test_list_issue_categories_no_pagination() -> Result<(), Box<dyn Error>> {
270 let _r_issue_category = ISSUE_CATEGORY_LOCK.read();
271 dotenvy::dotenv()?;
272 let redmine = crate::api::Redmine::from_env(
273 reqwest::blocking::Client::builder()
274 .use_rustls_tls()
275 .build()?,
276 )?;
277 let endpoint = ListIssueCategories::builder()
278 .project_id_or_name("336")
279 .build()?;
280 redmine.json_response_body::<_, IssueCategoriesWrapper<IssueCategory>>(&endpoint)?;
281 Ok(())
282 }
283
284 #[traced_test]
285 #[test]
286 fn test_get_issue_category() -> Result<(), Box<dyn Error>> {
287 let _r_issue_category = ISSUE_CATEGORY_LOCK.read();
288 dotenvy::dotenv()?;
289 let redmine = crate::api::Redmine::from_env(
290 reqwest::blocking::Client::builder()
291 .use_rustls_tls()
292 .build()?,
293 )?;
294 let endpoint = GetIssueCategory::builder().id(10).build()?;
295 redmine.json_response_body::<_, IssueCategoryWrapper<IssueCategory>>(&endpoint)?;
296 Ok(())
297 }
298
299 #[function_name::named]
300 #[traced_test]
301 #[test]
302 fn test_create_issue_category() -> Result<(), Box<dyn Error>> {
303 let _w_issue_category = ISSUE_CATEGORY_LOCK.write();
304 let name = format!("unittest_{}", function_name!());
305 with_project(&name, |redmine, _id, name| {
306 let create_endpoint = super::CreateIssueCategory::builder()
307 .project_id_or_name(name)
308 .name("Unittest Issue Category")
309 .build()?;
310 redmine.ignore_response_body::<_>(&create_endpoint)?;
311 Ok(())
312 })?;
313 Ok(())
314 }
315
316 #[function_name::named]
317 #[traced_test]
318 #[test]
319 fn test_update_issue_category() -> Result<(), Box<dyn Error>> {
320 let _w_issue_category = ISSUE_CATEGORY_LOCK.write();
321 let name = format!("unittest_{}", function_name!());
322 with_project(&name, |redmine, _id, name| {
323 let create_endpoint = super::CreateIssueCategory::builder()
324 .project_id_or_name(name)
325 .name("Unittest Issue Category")
326 .build()?;
327 let IssueCategoryWrapper { issue_category }: IssueCategoryWrapper<IssueCategory> =
328 redmine.json_response_body::<_, _>(&create_endpoint)?;
329 let id = issue_category.id;
330 let update_endpoint = super::UpdateIssueCategory::builder()
331 .id(id)
332 .name("Renamed Unit-Test name")
333 .build()?;
334 redmine.ignore_response_body::<_>(&update_endpoint)?;
335 Ok(())
336 })?;
337 Ok(())
338 }
339
340 #[function_name::named]
341 #[traced_test]
342 #[test]
343 fn test_delete_issue_category() -> Result<(), Box<dyn Error>> {
344 let _w_issue_category = ISSUE_CATEGORY_LOCK.write();
345 let name = format!("unittest_{}", function_name!());
346 with_project(&name, |redmine, _id, name| {
347 let create_endpoint = super::CreateIssueCategory::builder()
348 .project_id_or_name(name)
349 .name("Unittest Issue Category")
350 .build()?;
351 let IssueCategoryWrapper { issue_category }: IssueCategoryWrapper<IssueCategory> =
352 redmine.json_response_body::<_, _>(&create_endpoint)?;
353 let id = issue_category.id;
354 let delete_endpoint = super::DeleteIssueCategory::builder().id(id).build()?;
355 redmine.ignore_response_body::<_>(&delete_endpoint)?;
356 Ok(())
357 })?;
358 Ok(())
359 }
360
361 #[traced_test]
366 #[test]
367 fn test_completeness_issue_category_type() -> Result<(), Box<dyn Error>> {
368 let _r_issue_category = ISSUE_CATEGORY_LOCK.read();
369 dotenvy::dotenv()?;
370 let redmine = crate::api::Redmine::from_env(
371 reqwest::blocking::Client::builder()
372 .use_rustls_tls()
373 .build()?,
374 )?;
375 let endpoint = ListIssueCategories::builder()
376 .project_id_or_name("336")
377 .build()?;
378 let IssueCategoriesWrapper {
379 issue_categories: values,
380 } = redmine
381 .json_response_body::<_, IssueCategoriesWrapper<serde_json::Value>>(&endpoint)?;
382 for value in values {
383 let o: IssueCategory = serde_json::from_value(value.clone())?;
384 let reserialized = serde_json::to_value(o)?;
385 assert_eq!(value, reserialized);
386 }
387 Ok(())
388 }
389}