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, 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<'_> {}
74
75impl<'a> ListIssueCategories<'a> {
76 #[must_use]
78 pub fn builder() -> ListIssueCategoriesBuilder<'a> {
79 ListIssueCategoriesBuilder::default()
80 }
81}
82
83impl Endpoint for ListIssueCategories<'_> {
84 fn method(&self) -> Method {
85 Method::GET
86 }
87
88 fn endpoint(&self) -> Cow<'static, str> {
89 format!("projects/{}/issue_categories.json", self.project_id_or_name).into()
90 }
91}
92
93#[derive(Debug, Clone, Builder)]
95#[builder(setter(strip_option))]
96pub struct GetIssueCategory {
97 id: u64,
99}
100
101impl ReturnsJsonResponse for GetIssueCategory {}
102
103impl GetIssueCategory {
104 #[must_use]
106 pub fn builder() -> GetIssueCategoryBuilder {
107 GetIssueCategoryBuilder::default()
108 }
109}
110
111impl Endpoint for GetIssueCategory {
112 fn method(&self) -> Method {
113 Method::GET
114 }
115
116 fn endpoint(&self) -> Cow<'static, str> {
117 format!("issue_categories/{}.json", &self.id).into()
118 }
119}
120
121#[serde_with::skip_serializing_none]
123#[derive(Debug, Clone, Builder, Serialize)]
124#[builder(setter(strip_option))]
125pub struct CreateIssueCategory<'a> {
126 #[serde(skip_serializing)]
128 #[builder(setter(into))]
129 project_id_or_name: Cow<'a, str>,
130 #[builder(setter(into))]
132 name: Cow<'a, str>,
133 #[builder(default)]
135 assigned_to_id: Option<u64>,
136}
137
138impl ReturnsJsonResponse for CreateIssueCategory<'_> {}
139
140impl<'a> CreateIssueCategory<'a> {
141 #[must_use]
143 pub fn builder() -> CreateIssueCategoryBuilder<'a> {
144 CreateIssueCategoryBuilder::default()
145 }
146}
147
148impl Endpoint for CreateIssueCategory<'_> {
149 fn method(&self) -> Method {
150 Method::POST
151 }
152
153 fn endpoint(&self) -> Cow<'static, str> {
154 format!("projects/{}/issue_categories.json", self.project_id_or_name).into()
155 }
156
157 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
158 Ok(Some((
159 "application/json",
160 serde_json::to_vec(&IssueCategoryWrapper::<CreateIssueCategory> {
161 issue_category: (*self).to_owned(),
162 })?,
163 )))
164 }
165}
166
167#[serde_with::skip_serializing_none]
169#[derive(Debug, Clone, Builder, Serialize)]
170#[builder(setter(strip_option))]
171pub struct UpdateIssueCategory<'a> {
172 #[serde(skip_serializing)]
174 id: u64,
175 #[builder(setter(into), default)]
177 name: Option<Cow<'a, str>>,
178 #[builder(default)]
180 assigned_to_id: Option<u64>,
181}
182
183impl<'a> UpdateIssueCategory<'a> {
184 #[must_use]
186 pub fn builder() -> UpdateIssueCategoryBuilder<'a> {
187 UpdateIssueCategoryBuilder::default()
188 }
189}
190
191impl Endpoint for UpdateIssueCategory<'_> {
192 fn method(&self) -> Method {
193 Method::PUT
194 }
195
196 fn endpoint(&self) -> Cow<'static, str> {
197 format!("issue_categories/{}.json", self.id).into()
198 }
199
200 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
201 Ok(Some((
202 "application/json",
203 serde_json::to_vec(&IssueCategoryWrapper::<UpdateIssueCategory> {
204 issue_category: (*self).to_owned(),
205 })?,
206 )))
207 }
208}
209
210#[derive(Debug, Clone, Builder)]
212#[builder(setter(strip_option))]
213pub struct DeleteIssueCategory {
214 id: u64,
216}
217
218impl DeleteIssueCategory {
219 #[must_use]
221 pub fn builder() -> DeleteIssueCategoryBuilder {
222 DeleteIssueCategoryBuilder::default()
223 }
224}
225
226impl Endpoint for DeleteIssueCategory {
227 fn method(&self) -> Method {
228 Method::DELETE
229 }
230
231 fn endpoint(&self) -> Cow<'static, str> {
232 format!("issue_categories/{}.json", &self.id).into()
233 }
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
238pub struct IssueCategoriesWrapper<T> {
239 pub issue_categories: Vec<T>,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
246pub struct IssueCategoryWrapper<T> {
247 pub issue_category: T,
249}
250
251#[cfg(test)]
252mod test {
253 use super::*;
254 use crate::api::test_helpers::with_project;
255 use pretty_assertions::assert_eq;
256 use std::error::Error;
257 use tokio::sync::RwLock;
258 use tracing_test::traced_test;
259
260 static ISSUE_CATEGORY_LOCK: RwLock<()> = RwLock::const_new(());
263
264 #[traced_test]
265 #[test]
266 fn test_list_issue_categories_no_pagination() -> Result<(), Box<dyn Error>> {
267 let _r_issue_category = ISSUE_CATEGORY_LOCK.read();
268 dotenvy::dotenv()?;
269 let redmine = crate::api::Redmine::from_env()?;
270 let endpoint = ListIssueCategories::builder()
271 .project_id_or_name("336")
272 .build()?;
273 redmine.json_response_body::<_, IssueCategoriesWrapper<IssueCategory>>(&endpoint)?;
274 Ok(())
275 }
276
277 #[traced_test]
278 #[test]
279 fn test_get_issue_category() -> Result<(), Box<dyn Error>> {
280 let _r_issue_category = ISSUE_CATEGORY_LOCK.read();
281 dotenvy::dotenv()?;
282 let redmine = crate::api::Redmine::from_env()?;
283 let endpoint = GetIssueCategory::builder().id(10).build()?;
284 redmine.json_response_body::<_, IssueCategoryWrapper<IssueCategory>>(&endpoint)?;
285 Ok(())
286 }
287
288 #[function_name::named]
289 #[traced_test]
290 #[test]
291 fn test_create_issue_category() -> Result<(), Box<dyn Error>> {
292 let _w_issue_category = ISSUE_CATEGORY_LOCK.write();
293 let name = format!("unittest_{}", function_name!());
294 with_project(&name, |redmine, _id, name| {
295 let create_endpoint = super::CreateIssueCategory::builder()
296 .project_id_or_name(name)
297 .name("Unittest Issue Category")
298 .build()?;
299 redmine.ignore_response_body::<_>(&create_endpoint)?;
300 Ok(())
301 })?;
302 Ok(())
303 }
304
305 #[function_name::named]
306 #[traced_test]
307 #[test]
308 fn test_update_issue_category() -> Result<(), Box<dyn Error>> {
309 let _w_issue_category = ISSUE_CATEGORY_LOCK.write();
310 let name = format!("unittest_{}", function_name!());
311 with_project(&name, |redmine, _id, name| {
312 let create_endpoint = super::CreateIssueCategory::builder()
313 .project_id_or_name(name)
314 .name("Unittest Issue Category")
315 .build()?;
316 let IssueCategoryWrapper { issue_category }: IssueCategoryWrapper<IssueCategory> =
317 redmine.json_response_body::<_, _>(&create_endpoint)?;
318 let id = issue_category.id;
319 let update_endpoint = super::UpdateIssueCategory::builder()
320 .id(id)
321 .name("Renamed Unit-Test name")
322 .build()?;
323 redmine.ignore_response_body::<_>(&update_endpoint)?;
324 Ok(())
325 })?;
326 Ok(())
327 }
328
329 #[function_name::named]
330 #[traced_test]
331 #[test]
332 fn test_delete_issue_category() -> Result<(), Box<dyn Error>> {
333 let _w_issue_category = ISSUE_CATEGORY_LOCK.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 delete_endpoint = super::DeleteIssueCategory::builder().id(id).build()?;
344 redmine.ignore_response_body::<_>(&delete_endpoint)?;
345 Ok(())
346 })?;
347 Ok(())
348 }
349
350 #[traced_test]
355 #[test]
356 fn test_completeness_issue_category_type() -> Result<(), Box<dyn Error>> {
357 let _r_issue_category = ISSUE_CATEGORY_LOCK.read();
358 dotenvy::dotenv()?;
359 let redmine = crate::api::Redmine::from_env()?;
360 let endpoint = ListIssueCategories::builder()
361 .project_id_or_name("336")
362 .build()?;
363 let IssueCategoriesWrapper {
364 issue_categories: values,
365 } = redmine
366 .json_response_body::<_, IssueCategoriesWrapper<serde_json::Value>>(&endpoint)?;
367 for value in values {
368 let o: IssueCategory = serde_json::from_value(value.clone())?;
369 let reserialized = serde_json::to_value(o)?;
370 assert_eq!(value, reserialized);
371 }
372 Ok(())
373 }
374}