1use derive_builder::Builder;
14use reqwest::Method;
15use std::borrow::Cow;
16
17use crate::api::project_memberships::GroupProjectMembership;
18use crate::api::users::UserEssentials;
19use crate::api::{Endpoint, NoPagination, QueryParams, ReturnsJsonResponse};
20use serde::Serialize;
21
22#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
25pub struct GroupEssentials {
26 pub id: u64,
28 pub name: String,
30}
31
32impl From<Group> for GroupEssentials {
33 fn from(v: Group) -> Self {
34 GroupEssentials {
35 id: v.id,
36 name: v.name,
37 }
38 }
39}
40
41impl From<&Group> for GroupEssentials {
42 fn from(v: &Group) -> Self {
43 GroupEssentials {
44 id: v.id,
45 name: v.name.to_owned(),
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
54pub struct Group {
55 pub id: u64,
57 pub name: String,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub users: Option<Vec<UserEssentials>>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub memberships: Option<Vec<GroupProjectMembership>>,
65}
66
67#[derive(Debug, Clone, Builder)]
69#[builder(setter(strip_option))]
70pub struct ListGroups {}
71
72impl ReturnsJsonResponse for ListGroups {}
73impl NoPagination for ListGroups {}
74
75impl ListGroups {
76 #[must_use]
78 pub fn builder() -> ListGroupsBuilder {
79 ListGroupsBuilder::default()
80 }
81}
82
83impl Endpoint for ListGroups {
84 fn method(&self) -> Method {
85 Method::GET
86 }
87
88 fn endpoint(&self) -> Cow<'static, str> {
89 "groups.json".into()
90 }
91}
92
93#[derive(Debug, Clone)]
95pub enum GroupInclude {
96 Users,
98 Memberships,
100}
101
102impl std::fmt::Display for GroupInclude {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 match self {
105 Self::Users => {
106 write!(f, "users")
107 }
108 Self::Memberships => {
109 write!(f, "memberships")
110 }
111 }
112 }
113}
114
115#[derive(Debug, Clone, Builder)]
117#[builder(setter(strip_option))]
118pub struct GetGroup {
119 id: u64,
121 #[builder(default)]
123 include: Option<Vec<GroupInclude>>,
124}
125
126impl ReturnsJsonResponse for GetGroup {}
127impl NoPagination for GetGroup {}
128
129impl GetGroup {
130 #[must_use]
132 pub fn builder() -> GetGroupBuilder {
133 GetGroupBuilder::default()
134 }
135}
136
137impl Endpoint for GetGroup {
138 fn method(&self) -> Method {
139 Method::GET
140 }
141
142 fn endpoint(&self) -> Cow<'static, str> {
143 format!("groups/{}.json", &self.id).into()
144 }
145
146 fn parameters(&self) -> QueryParams {
147 let mut params = QueryParams::default();
148 params.push_opt("include", self.include.as_ref());
149 params
150 }
151}
152
153#[derive(Debug, Clone, Builder, Serialize)]
155#[builder(setter(strip_option))]
156pub struct CreateGroup<'a> {
157 #[builder(setter(into))]
159 name: Cow<'a, str>,
160 #[builder(default)]
162 user_ids: Option<Vec<u64>>,
163}
164
165impl ReturnsJsonResponse for CreateGroup<'_> {}
166impl NoPagination for CreateGroup<'_> {}
167
168impl<'a> CreateGroup<'a> {
169 #[must_use]
171 pub fn builder() -> CreateGroupBuilder<'a> {
172 CreateGroupBuilder::default()
173 }
174}
175
176impl Endpoint for CreateGroup<'_> {
177 fn method(&self) -> Method {
178 Method::POST
179 }
180
181 fn endpoint(&self) -> Cow<'static, str> {
182 "groups.json".into()
183 }
184
185 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
186 Ok(Some((
187 "application/json",
188 serde_json::to_vec(&GroupWrapper::<CreateGroup> {
189 group: (*self).to_owned(),
190 })?,
191 )))
192 }
193}
194
195#[derive(Debug, Clone, Builder, Serialize)]
197#[builder(setter(strip_option))]
198pub struct UpdateGroup<'a> {
199 #[serde(skip_serializing)]
201 id: u64,
202 #[builder(setter(into))]
204 name: Cow<'a, str>,
205 #[builder(default)]
207 user_ids: Option<Vec<u64>>,
208}
209
210impl<'a> UpdateGroup<'a> {
211 #[must_use]
213 pub fn builder() -> UpdateGroupBuilder<'a> {
214 UpdateGroupBuilder::default()
215 }
216}
217
218impl Endpoint for UpdateGroup<'_> {
219 fn method(&self) -> Method {
220 Method::PUT
221 }
222
223 fn endpoint(&self) -> Cow<'static, str> {
224 format!("groups/{}.json", self.id).into()
225 }
226
227 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
228 Ok(Some((
229 "application/json",
230 serde_json::to_vec(&GroupWrapper::<UpdateGroup> {
231 group: (*self).to_owned(),
232 })?,
233 )))
234 }
235}
236
237#[derive(Debug, Clone, Builder)]
239#[builder(setter(strip_option))]
240pub struct DeleteGroup {
241 id: u64,
243}
244
245impl DeleteGroup {
246 #[must_use]
248 pub fn builder() -> DeleteGroupBuilder {
249 DeleteGroupBuilder::default()
250 }
251}
252
253impl Endpoint for DeleteGroup {
254 fn method(&self) -> Method {
255 Method::DELETE
256 }
257
258 fn endpoint(&self) -> Cow<'static, str> {
259 format!("groups/{}.json", &self.id).into()
260 }
261}
262
263#[derive(Debug, Clone, Builder, Serialize)]
265#[builder(setter(strip_option))]
266pub struct AddUserToGroup {
267 #[serde(skip_serializing)]
269 group_id: u64,
270 user_id: u64,
272}
273
274impl AddUserToGroup {
275 #[must_use]
277 pub fn builder() -> AddUserToGroupBuilder {
278 AddUserToGroupBuilder::default()
279 }
280}
281
282impl Endpoint for AddUserToGroup {
283 fn method(&self) -> Method {
284 Method::POST
285 }
286
287 fn endpoint(&self) -> Cow<'static, str> {
288 format!("groups/{}/users.json", &self.group_id).into()
289 }
290
291 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
292 Ok(Some(("application/json", serde_json::to_vec(self)?)))
293 }
294}
295
296#[derive(Debug, Clone, Builder)]
298#[builder(setter(strip_option))]
299pub struct RemoveUserFromGroup {
300 group_id: u64,
302 user_id: u64,
304}
305
306impl RemoveUserFromGroup {
307 #[must_use]
309 pub fn builder() -> RemoveUserFromGroupBuilder {
310 RemoveUserFromGroupBuilder::default()
311 }
312}
313
314impl Endpoint for RemoveUserFromGroup {
315 fn method(&self) -> Method {
316 Method::DELETE
317 }
318
319 fn endpoint(&self) -> Cow<'static, str> {
320 format!("groups/{}/users/{}.json", &self.group_id, &self.user_id).into()
321 }
322}
323
324#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
326pub struct GroupsWrapper<T> {
327 pub groups: Vec<T>,
329}
330
331#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
334pub struct GroupWrapper<T> {
335 pub group: T,
337}
338
339#[cfg(test)]
340pub(crate) mod test {
341 use super::*;
342 use crate::api::test_helpers::with_group;
343 use pretty_assertions::assert_eq;
344 use std::error::Error;
345 use tokio::sync::RwLock;
346 use tracing_test::traced_test;
347
348 pub static GROUP_LOCK: RwLock<()> = RwLock::const_new(());
351
352 #[traced_test]
353 #[test]
354 fn test_list_groups_no_pagination() -> Result<(), Box<dyn Error>> {
355 let _r_groups = GROUP_LOCK.read();
356 dotenvy::dotenv()?;
357 let redmine = crate::api::Redmine::from_env()?;
358 let endpoint = ListGroups::builder().build()?;
359 redmine.json_response_body::<_, GroupsWrapper<Group>>(&endpoint)?;
360 Ok(())
361 }
362
363 #[traced_test]
364 #[test]
365 fn test_get_group() -> Result<(), Box<dyn Error>> {
366 let _r_groups = GROUP_LOCK.read();
367 dotenvy::dotenv()?;
368 let redmine = crate::api::Redmine::from_env()?;
369 let endpoint = GetGroup::builder().id(338).build()?;
370 redmine.json_response_body::<_, GroupWrapper<Group>>(&endpoint)?;
371 Ok(())
372 }
373
374 #[function_name::named]
375 #[traced_test]
376 #[test]
377 fn test_create_group() -> Result<(), Box<dyn Error>> {
378 let name = format!("unittest_{}", function_name!());
379 with_group(&name, |_, _, _| Ok(()))?;
380 Ok(())
381 }
382
383 #[function_name::named]
384 #[traced_test]
385 #[test]
386 fn test_update_project() -> Result<(), Box<dyn Error>> {
387 let name = format!("unittest_{}", function_name!());
388 with_group(&name, |redmine, id, _name| {
389 let update_endpoint = super::UpdateGroup::builder()
390 .id(id)
391 .name("unittest_rename_test")
392 .build()?;
393 redmine.ignore_response_body::<_>(&update_endpoint)?;
394 Ok(())
395 })?;
396 Ok(())
397 }
398
399 #[traced_test]
404 #[test]
405 fn test_completeness_group_type() -> Result<(), Box<dyn Error>> {
406 let _r_groups = GROUP_LOCK.read();
407 dotenvy::dotenv()?;
408 let redmine = crate::api::Redmine::from_env()?;
409 let endpoint = ListGroups::builder().build()?;
410 let GroupsWrapper { groups: values } =
411 redmine.json_response_body::<_, GroupsWrapper<serde_json::Value>>(&endpoint)?;
412 for value in values {
413 let o: Group = serde_json::from_value(value.clone())?;
414 let reserialized = serde_json::to_value(o)?;
415 assert_eq!(value, reserialized);
416 }
417 Ok(())
418 }
419
420 #[traced_test]
428 #[test]
429 fn test_completeness_group_type_all_group_details() -> Result<(), Box<dyn Error>> {
430 let _r_groups = GROUP_LOCK.read();
431 dotenvy::dotenv()?;
432 let redmine = crate::api::Redmine::from_env()?;
433 let endpoint = ListGroups::builder().build()?;
434 let GroupsWrapper { groups } =
435 redmine.json_response_body::<_, GroupsWrapper<Group>>(&endpoint)?;
436 for group in groups {
437 let get_endpoint = GetGroup::builder()
438 .id(group.id)
439 .include(vec![GroupInclude::Users, GroupInclude::Memberships])
440 .build()?;
441 let GroupWrapper { group: value } =
442 redmine.json_response_body::<_, GroupWrapper<serde_json::Value>>(&get_endpoint)?;
443 let o: Group = serde_json::from_value(value.clone())?;
444 let reserialized = serde_json::to_value(o)?;
445 assert_eq!(value, reserialized);
446 }
447 Ok(())
448 }
449}