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 let endpoint = ListIssueCategories::builder()
274 .project_id_or_name("336")
275 .build()?;
276 redmine.json_response_body::<_, IssueCategoriesWrapper<IssueCategory>>(&endpoint)?;
277 Ok(())
278 }
279
280 #[traced_test]
281 #[test]
282 fn test_get_issue_category() -> Result<(), Box<dyn Error>> {
283 let _r_issue_category = ISSUE_CATEGORY_LOCK.read();
284 dotenvy::dotenv()?;
285 let redmine = crate::api::Redmine::from_env()?;
286 let endpoint = GetIssueCategory::builder().id(10).build()?;
287 redmine.json_response_body::<_, IssueCategoryWrapper<IssueCategory>>(&endpoint)?;
288 Ok(())
289 }
290
291 #[function_name::named]
292 #[traced_test]
293 #[test]
294 fn test_create_issue_category() -> Result<(), Box<dyn Error>> {
295 let _w_issue_category = ISSUE_CATEGORY_LOCK.write();
296 let name = format!("unittest_{}", function_name!());
297 with_project(&name, |redmine, _id, name| {
298 let create_endpoint = super::CreateIssueCategory::builder()
299 .project_id_or_name(name)
300 .name("Unittest Issue Category")
301 .build()?;
302 redmine.ignore_response_body::<_>(&create_endpoint)?;
303 Ok(())
304 })?;
305 Ok(())
306 }
307
308 #[function_name::named]
309 #[traced_test]
310 #[test]
311 fn test_update_issue_category() -> Result<(), Box<dyn Error>> {
312 let _w_issue_category = ISSUE_CATEGORY_LOCK.write();
313 let name = format!("unittest_{}", function_name!());
314 with_project(&name, |redmine, _id, name| {
315 let create_endpoint = super::CreateIssueCategory::builder()
316 .project_id_or_name(name)
317 .name("Unittest Issue Category")
318 .build()?;
319 let IssueCategoryWrapper { issue_category }: IssueCategoryWrapper<IssueCategory> =
320 redmine.json_response_body::<_, _>(&create_endpoint)?;
321 let id = issue_category.id;
322 let update_endpoint = super::UpdateIssueCategory::builder()
323 .id(id)
324 .name("Renamed Unit-Test name")
325 .build()?;
326 redmine.ignore_response_body::<_>(&update_endpoint)?;
327 Ok(())
328 })?;
329 Ok(())
330 }
331
332 #[function_name::named]
333 #[traced_test]
334 #[test]
335 fn test_delete_issue_category() -> Result<(), Box<dyn Error>> {
336 let _w_issue_category = ISSUE_CATEGORY_LOCK.write();
337 let name = format!("unittest_{}", function_name!());
338 with_project(&name, |redmine, _id, name| {
339 let create_endpoint = super::CreateIssueCategory::builder()
340 .project_id_or_name(name)
341 .name("Unittest Issue Category")
342 .build()?;
343 let IssueCategoryWrapper { issue_category }: IssueCategoryWrapper<IssueCategory> =
344 redmine.json_response_body::<_, _>(&create_endpoint)?;
345 let id = issue_category.id;
346 let delete_endpoint = super::DeleteIssueCategory::builder().id(id).build()?;
347 redmine.ignore_response_body::<_>(&delete_endpoint)?;
348 Ok(())
349 })?;
350 Ok(())
351 }
352
353 #[traced_test]
358 #[test]
359 fn test_completeness_issue_category_type() -> Result<(), Box<dyn Error>> {
360 let _r_issue_category = ISSUE_CATEGORY_LOCK.read();
361 dotenvy::dotenv()?;
362 let redmine = crate::api::Redmine::from_env()?;
363 let endpoint = ListIssueCategories::builder()
364 .project_id_or_name("336")
365 .build()?;
366 let IssueCategoriesWrapper {
367 issue_categories: values,
368 } = redmine
369 .json_response_body::<_, IssueCategoriesWrapper<serde_json::Value>>(&endpoint)?;
370 for value in values {
371 let o: IssueCategory = serde_json::from_value(value.clone())?;
372 let reserialized = serde_json::to_value(o)?;
373 assert_eq!(value, reserialized);
374 }
375 Ok(())
376 }
377}