1use derive_builder::Builder;
14use reqwest::Method;
15use std::borrow::Cow;
16
17use crate::api::custom_fields::CustomField;
18use crate::api::custom_fields::CustomFieldEssentialsWithValue;
19use crate::api::project_memberships::GroupProjectMembership;
20use crate::api::users::UserEssentials;
21use crate::api::{Endpoint, NoPagination, QueryParams, ReturnsJsonResponse};
22use serde::Serialize;
23
24#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
27pub struct GroupEssentials {
28 pub id: u64,
30 pub name: String,
32}
33
34impl From<Group> for GroupEssentials {
35 fn from(v: Group) -> Self {
36 GroupEssentials {
37 id: v.id,
38 name: v.name,
39 }
40 }
41}
42
43impl From<&Group> for GroupEssentials {
44 fn from(v: &Group) -> Self {
45 GroupEssentials {
46 id: v.id,
47 name: v.name.to_owned(),
48 }
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
56pub struct Group {
57 pub id: u64,
59 pub name: String,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub users: Option<Vec<UserEssentials>>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub memberships: Option<Vec<GroupProjectMembership>>,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub custom_fields: Option<Vec<CustomFieldEssentialsWithValue>>,
70}
71
72#[derive(Debug, Clone, Builder)]
74#[builder(setter(strip_option))]
75pub struct ListGroups {
76 #[builder(default)]
78 builtin: Option<bool>,
79}
80
81impl ReturnsJsonResponse for ListGroups {}
82impl NoPagination for ListGroups {}
83
84impl ListGroups {
85 #[must_use]
87 pub fn builder() -> ListGroupsBuilder {
88 ListGroupsBuilder::default()
89 }
90}
91
92impl Endpoint for ListGroups {
93 fn method(&self) -> Method {
94 Method::GET
95 }
96
97 fn endpoint(&self) -> Cow<'static, str> {
98 "groups.json".into()
99 }
100
101 fn parameters(&self) -> QueryParams<'_> {
102 let mut params = QueryParams::default();
103 params.push_opt("builtin", self.builtin);
104 params
105 }
106}
107
108#[derive(Debug, Clone)]
110pub enum GroupInclude {
111 Users,
113 Memberships,
115}
116
117impl std::fmt::Display for GroupInclude {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 match self {
120 Self::Users => {
121 write!(f, "users")
122 }
123 Self::Memberships => {
124 write!(f, "memberships")
125 }
126 }
127 }
128}
129
130#[derive(Debug, Clone, Builder)]
132#[builder(setter(strip_option))]
133pub struct GetGroup {
134 id: u64,
136 #[builder(default)]
138 include: Option<Vec<GroupInclude>>,
139}
140
141impl ReturnsJsonResponse for GetGroup {}
142impl NoPagination for GetGroup {}
143
144impl GetGroup {
145 #[must_use]
147 pub fn builder() -> GetGroupBuilder {
148 GetGroupBuilder::default()
149 }
150}
151
152impl Endpoint for GetGroup {
153 fn method(&self) -> Method {
154 Method::GET
155 }
156
157 fn endpoint(&self) -> Cow<'static, str> {
158 format!("groups/{}.json", &self.id).into()
159 }
160
161 fn parameters(&self) -> QueryParams<'_> {
162 let mut params = QueryParams::default();
163 params.push_opt("include", self.include.as_ref());
164 params
165 }
166}
167
168#[serde_with::skip_serializing_none]
170#[derive(Debug, Clone, Builder, Serialize)]
171#[builder(setter(strip_option))]
172pub struct CreateGroup<'a> {
173 #[builder(setter(into))]
175 name: Cow<'a, str>,
176 #[builder(default)]
178 twofa_required: Option<bool>,
179 #[builder(default)]
181 user_ids: Option<Vec<u64>>,
182 #[builder(default)]
184 custom_fields: Option<Vec<CustomField<'a>>>,
185}
186
187impl ReturnsJsonResponse for CreateGroup<'_> {}
188impl NoPagination for CreateGroup<'_> {}
189
190impl<'a> CreateGroup<'a> {
191 #[must_use]
193 pub fn builder() -> CreateGroupBuilder<'a> {
194 CreateGroupBuilder::default()
195 }
196}
197
198impl Endpoint for CreateGroup<'_> {
199 fn method(&self) -> Method {
200 Method::POST
201 }
202
203 fn endpoint(&self) -> Cow<'static, str> {
204 "groups.json".into()
205 }
206
207 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
208 Ok(Some((
209 "application/json",
210 serde_json::to_vec(&GroupWrapper::<CreateGroup> {
211 group: (*self).to_owned(),
212 })?,
213 )))
214 }
215}
216
217#[serde_with::skip_serializing_none]
219#[derive(Debug, Clone, Builder, Serialize)]
220#[builder(setter(strip_option))]
221pub struct UpdateGroup<'a> {
222 #[serde(skip_serializing)]
224 id: u64,
225 #[builder(setter(into))]
227 name: Cow<'a, str>,
228 #[builder(default)]
230 twofa_required: Option<bool>,
231 #[builder(default)]
233 user_ids: Option<Vec<u64>>,
234 #[builder(default)]
236 custom_fields: Option<Vec<CustomField<'a>>>,
237}
238
239impl<'a> UpdateGroup<'a> {
240 #[must_use]
242 pub fn builder() -> UpdateGroupBuilder<'a> {
243 UpdateGroupBuilder::default()
244 }
245}
246
247impl Endpoint for UpdateGroup<'_> {
248 fn method(&self) -> Method {
249 Method::PUT
250 }
251
252 fn endpoint(&self) -> Cow<'static, str> {
253 format!("groups/{}.json", self.id).into()
254 }
255
256 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
257 Ok(Some((
258 "application/json",
259 serde_json::to_vec(&GroupWrapper::<UpdateGroup> {
260 group: (*self).to_owned(),
261 })?,
262 )))
263 }
264}
265
266#[derive(Debug, Clone, Builder)]
268#[builder(setter(strip_option))]
269pub struct DeleteGroup {
270 id: u64,
272}
273
274impl DeleteGroup {
275 #[must_use]
277 pub fn builder() -> DeleteGroupBuilder {
278 DeleteGroupBuilder::default()
279 }
280}
281
282impl Endpoint for DeleteGroup {
283 fn method(&self) -> Method {
284 Method::DELETE
285 }
286
287 fn endpoint(&self) -> Cow<'static, str> {
288 format!("groups/{}.json", &self.id).into()
289 }
290}
291
292#[derive(Debug, Clone, Builder, Serialize)]
294#[builder(setter(strip_option))]
295pub struct AddUserToGroup {
296 #[serde(skip_serializing)]
298 group_id: u64,
299 user_id: u64,
301}
302
303impl AddUserToGroup {
304 #[must_use]
306 pub fn builder() -> AddUserToGroupBuilder {
307 AddUserToGroupBuilder::default()
308 }
309}
310
311impl Endpoint for AddUserToGroup {
312 fn method(&self) -> Method {
313 Method::POST
314 }
315
316 fn endpoint(&self) -> Cow<'static, str> {
317 format!("groups/{}/users.json", &self.group_id).into()
318 }
319
320 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
321 Ok(Some(("application/json", serde_json::to_vec(self)?)))
322 }
323}
324
325#[derive(Debug, Clone, Builder)]
327#[builder(setter(strip_option))]
328pub struct RemoveUserFromGroup {
329 group_id: u64,
331 user_id: u64,
333}
334
335impl RemoveUserFromGroup {
336 #[must_use]
338 pub fn builder() -> RemoveUserFromGroupBuilder {
339 RemoveUserFromGroupBuilder::default()
340 }
341}
342
343impl Endpoint for RemoveUserFromGroup {
344 fn method(&self) -> Method {
345 Method::DELETE
346 }
347
348 fn endpoint(&self) -> Cow<'static, str> {
349 format!("groups/{}/users/{}.json", &self.group_id, &self.user_id).into()
350 }
351}
352
353#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
355pub struct GroupsWrapper<T> {
356 pub groups: Vec<T>,
358}
359
360#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
363pub struct GroupWrapper<T> {
364 pub group: T,
366}
367
368#[cfg(test)]
369pub(crate) mod test {
370 use super::*;
371 use crate::api::project_memberships::test::PROJECT_MEMBERSHIP_LOCK;
372 use crate::api::test_helpers::with_group;
373 use crate::api::users::test::USER_LOCK;
374 use pretty_assertions::assert_eq;
375 use std::error::Error;
376 use tokio::sync::RwLock;
377 use tracing_test::traced_test;
378
379 pub static GROUP_LOCK: RwLock<()> = RwLock::const_new(());
382
383 #[traced_test]
384 #[test]
385 fn test_list_groups_no_pagination() -> Result<(), Box<dyn Error>> {
386 let _r_groups = GROUP_LOCK.blocking_read();
387 dotenvy::dotenv()?;
388 let redmine = crate::api::Redmine::from_env(
389 reqwest::blocking::Client::builder()
390 .use_rustls_tls()
391 .build()?,
392 )?;
393 let endpoint = ListGroups::builder().build()?;
394 redmine.json_response_body::<_, GroupsWrapper<Group>>(&endpoint)?;
395 Ok(())
396 }
397
398 #[traced_test]
399 #[test]
400 fn test_get_group() -> Result<(), Box<dyn Error>> {
401 let _r_groups = GROUP_LOCK.blocking_read();
402 dotenvy::dotenv()?;
403 let redmine = crate::api::Redmine::from_env(
404 reqwest::blocking::Client::builder()
405 .use_rustls_tls()
406 .build()?,
407 )?;
408 let endpoint = GetGroup::builder().id(338).build()?;
409 redmine.json_response_body::<_, GroupWrapper<Group>>(&endpoint)?;
410 Ok(())
411 }
412
413 #[function_name::named]
414 #[traced_test]
415 #[test]
416 fn test_create_group() -> Result<(), Box<dyn Error>> {
417 let name = format!("unittest_{}", function_name!());
418 with_group(&name, |_, _, _| Ok(()))?;
419 Ok(())
420 }
421
422 #[function_name::named]
423 #[traced_test]
424 #[test]
425 fn test_update_group() -> Result<(), Box<dyn Error>> {
426 let name = format!("unittest_{}", function_name!());
427 with_group(&name, |redmine, id, _name| {
428 let update_endpoint = super::UpdateGroup::builder()
429 .id(id)
430 .name("unittest_rename_test")
431 .build()?;
432 redmine.ignore_response_body::<_>(&update_endpoint)?;
433 Ok(())
434 })?;
435 Ok(())
436 }
437
438 #[traced_test]
443 #[test]
444 fn test_completeness_group_type() -> Result<(), Box<dyn Error>> {
445 let _r_groups = GROUP_LOCK.blocking_read();
446 dotenvy::dotenv()?;
447 let redmine = crate::api::Redmine::from_env(
448 reqwest::blocking::Client::builder()
449 .use_rustls_tls()
450 .build()?,
451 )?;
452 let endpoint = ListGroups::builder().build()?;
453 let GroupsWrapper { groups: values } =
454 redmine.json_response_body::<_, GroupsWrapper<serde_json::Value>>(&endpoint)?;
455 for value in values {
456 let o: Group = serde_json::from_value(value.clone())?;
457 let reserialized = serde_json::to_value(o)?;
458 assert_eq!(value, reserialized);
459 }
460 Ok(())
461 }
462
463 #[traced_test]
471 #[test]
472 fn test_completeness_group_type_all_group_details() -> Result<(), Box<dyn Error>> {
473 let _r_user = USER_LOCK.blocking_read();
474 let _r_groups = GROUP_LOCK.blocking_read();
475 let _r_project_memberships = PROJECT_MEMBERSHIP_LOCK.blocking_read();
476 dotenvy::dotenv()?;
477 let redmine = crate::api::Redmine::from_env(
478 reqwest::blocking::Client::builder()
479 .use_rustls_tls()
480 .build()?,
481 )?;
482 let endpoint = ListGroups::builder().build()?;
483 let GroupsWrapper { groups } =
484 redmine.json_response_body::<_, GroupsWrapper<Group>>(&endpoint)?;
485 for group in groups {
486 let get_endpoint = GetGroup::builder()
487 .id(group.id)
488 .include(vec![GroupInclude::Users, GroupInclude::Memberships])
489 .build()?;
490 let GroupWrapper { group: value } =
491 redmine.json_response_body::<_, GroupWrapper<serde_json::Value>>(&get_endpoint)?;
492 let o: Group = serde_json::from_value(value.clone())?;
493 let reserialized = serde_json::to_value(o)?;
494 assert_eq!(value, reserialized);
495 }
496 Ok(())
497 }
498}