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, 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 {}
73
74impl ListGroups {
75 #[must_use]
77 pub fn builder() -> ListGroupsBuilder {
78 ListGroupsBuilder::default()
79 }
80}
81
82impl Endpoint for ListGroups {
83 fn method(&self) -> Method {
84 Method::GET
85 }
86
87 fn endpoint(&self) -> Cow<'static, str> {
88 "groups.json".into()
89 }
90}
91
92#[derive(Debug, Clone)]
94pub enum GroupInclude {
95 Users,
97 Memberships,
99}
100
101impl std::fmt::Display for GroupInclude {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 match self {
104 Self::Users => {
105 write!(f, "users")
106 }
107 Self::Memberships => {
108 write!(f, "memberships")
109 }
110 }
111 }
112}
113
114#[derive(Debug, Clone, Builder)]
116#[builder(setter(strip_option))]
117pub struct GetGroup {
118 id: u64,
120 #[builder(default)]
122 include: Option<Vec<GroupInclude>>,
123}
124
125impl ReturnsJsonResponse for GetGroup {}
126
127impl GetGroup {
128 #[must_use]
130 pub fn builder() -> GetGroupBuilder {
131 GetGroupBuilder::default()
132 }
133}
134
135impl Endpoint for GetGroup {
136 fn method(&self) -> Method {
137 Method::GET
138 }
139
140 fn endpoint(&self) -> Cow<'static, str> {
141 format!("groups/{}.json", &self.id).into()
142 }
143
144 fn parameters(&self) -> QueryParams {
145 let mut params = QueryParams::default();
146 params.push_opt("include", self.include.as_ref());
147 params
148 }
149}
150
151#[derive(Debug, Clone, Builder, Serialize)]
153#[builder(setter(strip_option))]
154pub struct CreateGroup<'a> {
155 #[builder(setter(into))]
157 name: Cow<'a, str>,
158 #[builder(default)]
160 user_ids: Option<Vec<u64>>,
161}
162
163impl ReturnsJsonResponse for CreateGroup<'_> {}
164
165impl<'a> CreateGroup<'a> {
166 #[must_use]
168 pub fn builder() -> CreateGroupBuilder<'a> {
169 CreateGroupBuilder::default()
170 }
171}
172
173impl Endpoint for CreateGroup<'_> {
174 fn method(&self) -> Method {
175 Method::POST
176 }
177
178 fn endpoint(&self) -> Cow<'static, str> {
179 "groups.json".into()
180 }
181
182 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
183 Ok(Some((
184 "application/json",
185 serde_json::to_vec(&GroupWrapper::<CreateGroup> {
186 group: (*self).to_owned(),
187 })?,
188 )))
189 }
190}
191
192#[derive(Debug, Clone, Builder, Serialize)]
194#[builder(setter(strip_option))]
195pub struct UpdateGroup<'a> {
196 #[serde(skip_serializing)]
198 id: u64,
199 #[builder(setter(into))]
201 name: Cow<'a, str>,
202 #[builder(default)]
204 user_ids: Option<Vec<u64>>,
205}
206
207impl<'a> UpdateGroup<'a> {
208 #[must_use]
210 pub fn builder() -> UpdateGroupBuilder<'a> {
211 UpdateGroupBuilder::default()
212 }
213}
214
215impl Endpoint for UpdateGroup<'_> {
216 fn method(&self) -> Method {
217 Method::PUT
218 }
219
220 fn endpoint(&self) -> Cow<'static, str> {
221 format!("groups/{}.json", self.id).into()
222 }
223
224 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
225 Ok(Some((
226 "application/json",
227 serde_json::to_vec(&GroupWrapper::<UpdateGroup> {
228 group: (*self).to_owned(),
229 })?,
230 )))
231 }
232}
233
234#[derive(Debug, Clone, Builder)]
236#[builder(setter(strip_option))]
237pub struct DeleteGroup {
238 id: u64,
240}
241
242impl DeleteGroup {
243 #[must_use]
245 pub fn builder() -> DeleteGroupBuilder {
246 DeleteGroupBuilder::default()
247 }
248}
249
250impl Endpoint for DeleteGroup {
251 fn method(&self) -> Method {
252 Method::DELETE
253 }
254
255 fn endpoint(&self) -> Cow<'static, str> {
256 format!("groups/{}.json", &self.id).into()
257 }
258}
259
260#[derive(Debug, Clone, Builder, Serialize)]
262#[builder(setter(strip_option))]
263pub struct AddUserToGroup {
264 #[serde(skip_serializing)]
266 group_id: u64,
267 user_id: u64,
269}
270
271impl AddUserToGroup {
272 #[must_use]
274 pub fn builder() -> AddUserToGroupBuilder {
275 AddUserToGroupBuilder::default()
276 }
277}
278
279impl Endpoint for AddUserToGroup {
280 fn method(&self) -> Method {
281 Method::POST
282 }
283
284 fn endpoint(&self) -> Cow<'static, str> {
285 format!("groups/{}/users.json", &self.group_id).into()
286 }
287
288 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
289 Ok(Some(("application/json", serde_json::to_vec(self)?)))
290 }
291}
292
293#[derive(Debug, Clone, Builder)]
295#[builder(setter(strip_option))]
296pub struct RemoveUserFromGroup {
297 group_id: u64,
299 user_id: u64,
301}
302
303impl RemoveUserFromGroup {
304 #[must_use]
306 pub fn builder() -> RemoveUserFromGroupBuilder {
307 RemoveUserFromGroupBuilder::default()
308 }
309}
310
311impl Endpoint for RemoveUserFromGroup {
312 fn method(&self) -> Method {
313 Method::DELETE
314 }
315
316 fn endpoint(&self) -> Cow<'static, str> {
317 format!("groups/{}/users/{}.json", &self.group_id, &self.user_id).into()
318 }
319}
320
321#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
323pub struct GroupsWrapper<T> {
324 pub groups: Vec<T>,
326}
327
328#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
331pub struct GroupWrapper<T> {
332 pub group: T,
334}
335
336#[cfg(test)]
337pub(crate) mod test {
338 use super::*;
339 use crate::api::test_helpers::with_group;
340 use pretty_assertions::assert_eq;
341 use std::error::Error;
342 use tokio::sync::RwLock;
343 use tracing_test::traced_test;
344
345 pub static GROUP_LOCK: RwLock<()> = RwLock::const_new(());
348
349 #[traced_test]
350 #[test]
351 fn test_list_groups_no_pagination() -> Result<(), Box<dyn Error>> {
352 let _r_groups = GROUP_LOCK.read();
353 dotenvy::dotenv()?;
354 let redmine = crate::api::Redmine::from_env()?;
355 let endpoint = ListGroups::builder().build()?;
356 redmine.json_response_body::<_, GroupsWrapper<Group>>(&endpoint)?;
357 Ok(())
358 }
359
360 #[traced_test]
361 #[test]
362 fn test_get_group() -> Result<(), Box<dyn Error>> {
363 let _r_groups = GROUP_LOCK.read();
364 dotenvy::dotenv()?;
365 let redmine = crate::api::Redmine::from_env()?;
366 let endpoint = GetGroup::builder().id(338).build()?;
367 redmine.json_response_body::<_, GroupWrapper<Group>>(&endpoint)?;
368 Ok(())
369 }
370
371 #[function_name::named]
372 #[traced_test]
373 #[test]
374 fn test_create_group() -> Result<(), Box<dyn Error>> {
375 let name = format!("unittest_{}", function_name!());
376 with_group(&name, |_, _, _| Ok(()))?;
377 Ok(())
378 }
379
380 #[function_name::named]
381 #[traced_test]
382 #[test]
383 fn test_update_project() -> Result<(), Box<dyn Error>> {
384 let name = format!("unittest_{}", function_name!());
385 with_group(&name, |redmine, id, _name| {
386 let update_endpoint = super::UpdateGroup::builder()
387 .id(id)
388 .name("unittest_rename_test")
389 .build()?;
390 redmine.ignore_response_body::<_>(&update_endpoint)?;
391 Ok(())
392 })?;
393 Ok(())
394 }
395
396 #[traced_test]
401 #[test]
402 fn test_completeness_group_type() -> Result<(), Box<dyn Error>> {
403 let _r_groups = GROUP_LOCK.read();
404 dotenvy::dotenv()?;
405 let redmine = crate::api::Redmine::from_env()?;
406 let endpoint = ListGroups::builder().build()?;
407 let GroupsWrapper { groups: values } =
408 redmine.json_response_body::<_, GroupsWrapper<serde_json::Value>>(&endpoint)?;
409 for value in values {
410 let o: Group = serde_json::from_value(value.clone())?;
411 let reserialized = serde_json::to_value(o)?;
412 assert_eq!(value, reserialized);
413 }
414 Ok(())
415 }
416
417 #[traced_test]
425 #[test]
426 fn test_completeness_group_type_all_group_details() -> Result<(), Box<dyn Error>> {
427 let _r_groups = GROUP_LOCK.read();
428 dotenvy::dotenv()?;
429 let redmine = crate::api::Redmine::from_env()?;
430 let endpoint = ListGroups::builder().build()?;
431 let GroupsWrapper { groups } =
432 redmine.json_response_body::<_, GroupsWrapper<Group>>(&endpoint)?;
433 for group in groups {
434 let get_endpoint = GetGroup::builder()
435 .id(group.id)
436 .include(vec![GroupInclude::Users, GroupInclude::Memberships])
437 .build()?;
438 let GroupWrapper { group: value } =
439 redmine.json_response_body::<_, GroupWrapper<serde_json::Value>>(&get_endpoint)?;
440 let o: Group = serde_json::from_value(value.clone())?;
441 let reserialized = serde_json::to_value(o)?;
442 assert_eq!(value, reserialized);
443 }
444 Ok(())
445 }
446}