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::project_memberships::test::PROJECT_MEMBERSHIP_LOCK;
343 use crate::api::test_helpers::with_group;
344 use crate::api::users::test::USER_LOCK;
345 use pretty_assertions::assert_eq;
346 use std::error::Error;
347 use tokio::sync::RwLock;
348 use tracing_test::traced_test;
349
350 pub static GROUP_LOCK: RwLock<()> = RwLock::const_new(());
353
354 #[traced_test]
355 #[test]
356 fn test_list_groups_no_pagination() -> Result<(), Box<dyn Error>> {
357 let _r_groups = GROUP_LOCK.blocking_read();
358 dotenvy::dotenv()?;
359 let redmine = crate::api::Redmine::from_env(
360 reqwest::blocking::Client::builder()
361 .use_rustls_tls()
362 .build()?,
363 )?;
364 let endpoint = ListGroups::builder().build()?;
365 redmine.json_response_body::<_, GroupsWrapper<Group>>(&endpoint)?;
366 Ok(())
367 }
368
369 #[traced_test]
370 #[test]
371 fn test_get_group() -> Result<(), Box<dyn Error>> {
372 let _r_groups = GROUP_LOCK.blocking_read();
373 dotenvy::dotenv()?;
374 let redmine = crate::api::Redmine::from_env(
375 reqwest::blocking::Client::builder()
376 .use_rustls_tls()
377 .build()?,
378 )?;
379 let endpoint = GetGroup::builder().id(338).build()?;
380 redmine.json_response_body::<_, GroupWrapper<Group>>(&endpoint)?;
381 Ok(())
382 }
383
384 #[function_name::named]
385 #[traced_test]
386 #[test]
387 fn test_create_group() -> Result<(), Box<dyn Error>> {
388 let name = format!("unittest_{}", function_name!());
389 with_group(&name, |_, _, _| Ok(()))?;
390 Ok(())
391 }
392
393 #[function_name::named]
394 #[traced_test]
395 #[test]
396 fn test_update_group() -> Result<(), Box<dyn Error>> {
397 let name = format!("unittest_{}", function_name!());
398 with_group(&name, |redmine, id, _name| {
399 let update_endpoint = super::UpdateGroup::builder()
400 .id(id)
401 .name("unittest_rename_test")
402 .build()?;
403 redmine.ignore_response_body::<_>(&update_endpoint)?;
404 Ok(())
405 })?;
406 Ok(())
407 }
408
409 #[traced_test]
414 #[test]
415 fn test_completeness_group_type() -> Result<(), Box<dyn Error>> {
416 let _r_groups = GROUP_LOCK.blocking_read();
417 dotenvy::dotenv()?;
418 let redmine = crate::api::Redmine::from_env(
419 reqwest::blocking::Client::builder()
420 .use_rustls_tls()
421 .build()?,
422 )?;
423 let endpoint = ListGroups::builder().build()?;
424 let GroupsWrapper { groups: values } =
425 redmine.json_response_body::<_, GroupsWrapper<serde_json::Value>>(&endpoint)?;
426 for value in values {
427 let o: Group = serde_json::from_value(value.clone())?;
428 let reserialized = serde_json::to_value(o)?;
429 assert_eq!(value, reserialized);
430 }
431 Ok(())
432 }
433
434 #[traced_test]
442 #[test]
443 fn test_completeness_group_type_all_group_details() -> Result<(), Box<dyn Error>> {
444 let _r_user = USER_LOCK.blocking_read();
445 let _r_groups = GROUP_LOCK.blocking_read();
446 let _r_project_memberships = PROJECT_MEMBERSHIP_LOCK.blocking_read();
447 dotenvy::dotenv()?;
448 let redmine = crate::api::Redmine::from_env(
449 reqwest::blocking::Client::builder()
450 .use_rustls_tls()
451 .build()?,
452 )?;
453 let endpoint = ListGroups::builder().build()?;
454 let GroupsWrapper { groups } =
455 redmine.json_response_body::<_, GroupsWrapper<Group>>(&endpoint)?;
456 for group in groups {
457 let get_endpoint = GetGroup::builder()
458 .id(group.id)
459 .include(vec![GroupInclude::Users, GroupInclude::Memberships])
460 .build()?;
461 let GroupWrapper { group: value } =
462 redmine.json_response_body::<_, GroupWrapper<serde_json::Value>>(&get_endpoint)?;
463 let o: Group = serde_json::from_value(value.clone())?;
464 let reserialized = serde_json::to_value(o)?;
465 assert_eq!(value, reserialized);
466 }
467 Ok(())
468 }
469}