1use derive_builder::Builder;
12use reqwest::Method;
13use std::borrow::Cow;
14
15use crate::api::groups::GroupEssentials;
16use crate::api::projects::ProjectEssentials;
17use crate::api::roles::RoleEssentials;
18use crate::api::users::UserEssentials;
19use crate::api::{Endpoint, Pageable, ReturnsJsonResponse};
20use serde::Serialize;
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
25pub struct UserProjectMembership {
26 pub id: u64,
28 pub project: ProjectEssentials,
30 pub roles: Vec<RoleEssentials>,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
37pub struct GroupProjectMembership {
38 pub id: u64,
40 pub project: ProjectEssentials,
42 pub roles: Vec<RoleEssentials>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
50pub struct ProjectMembership {
51 pub id: u64,
53 pub project: ProjectEssentials,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub user: Option<UserEssentials>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub group: Option<GroupEssentials>,
61 pub roles: Vec<RoleEssentials>,
63}
64
65#[derive(Debug, Clone, Builder)]
67#[builder(setter(strip_option))]
68pub struct ListProjectMemberships<'a> {
69 #[builder(setter(into))]
71 project_id_or_name: Cow<'a, str>,
72}
73
74impl ReturnsJsonResponse for ListProjectMemberships<'_> {}
75impl Pageable for ListProjectMemberships<'_> {
76 fn response_wrapper_key(&self) -> String {
77 "memberships".to_string()
78 }
79}
80
81impl<'a> ListProjectMemberships<'a> {
82 #[must_use]
84 pub fn builder() -> ListProjectMembershipsBuilder<'a> {
85 ListProjectMembershipsBuilder::default()
86 }
87}
88
89impl Endpoint for ListProjectMemberships<'_> {
90 fn method(&self) -> Method {
91 Method::GET
92 }
93
94 fn endpoint(&self) -> Cow<'static, str> {
95 format!("projects/{}/memberships.json", self.project_id_or_name).into()
96 }
97}
98
99#[derive(Debug, Clone, Builder)]
101#[builder(setter(strip_option))]
102pub struct GetProjectMembership {
103 id: u64,
105}
106
107impl ReturnsJsonResponse for GetProjectMembership {}
108
109impl GetProjectMembership {
110 #[must_use]
112 pub fn builder() -> GetProjectMembershipBuilder {
113 GetProjectMembershipBuilder::default()
114 }
115}
116
117impl Endpoint for GetProjectMembership {
118 fn method(&self) -> Method {
119 Method::GET
120 }
121
122 fn endpoint(&self) -> Cow<'static, str> {
123 format!("memberships/{}.json", &self.id).into()
124 }
125}
126
127#[serde_with::skip_serializing_none]
129#[derive(Debug, Clone, Builder, Serialize)]
130#[builder(setter(strip_option))]
131pub struct CreateProjectMembership<'a> {
132 #[builder(setter(into))]
134 #[serde(skip_serializing)]
135 project_id_or_name: Cow<'a, str>,
136 user_id: u64,
138 role_ids: Vec<u64>,
140}
141
142impl ReturnsJsonResponse for CreateProjectMembership<'_> {}
143
144impl<'a> CreateProjectMembership<'a> {
145 #[must_use]
147 pub fn builder() -> CreateProjectMembershipBuilder<'a> {
148 CreateProjectMembershipBuilder::default()
149 }
150}
151
152impl Endpoint for CreateProjectMembership<'_> {
153 fn method(&self) -> Method {
154 Method::POST
155 }
156
157 fn endpoint(&self) -> Cow<'static, str> {
158 format!("projects/{}/memberships.json", self.project_id_or_name).into()
159 }
160
161 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
162 Ok(Some((
163 "application/json",
164 serde_json::to_vec(&MembershipWrapper::<CreateProjectMembership> {
165 membership: (*self).to_owned(),
166 })?,
167 )))
168 }
169}
170
171#[serde_with::skip_serializing_none]
173#[derive(Debug, Clone, Builder, Serialize)]
174#[builder(setter(strip_option))]
175pub struct UpdateProjectMembership {
176 #[serde(skip_serializing)]
178 id: u64,
179 role_ids: Vec<u64>,
181}
182
183impl UpdateProjectMembership {
184 #[must_use]
186 pub fn builder() -> UpdateProjectMembershipBuilder {
187 UpdateProjectMembershipBuilder::default()
188 }
189}
190
191impl Endpoint for UpdateProjectMembership {
192 fn method(&self) -> Method {
193 Method::PUT
194 }
195
196 fn endpoint(&self) -> Cow<'static, str> {
197 format!("memberships/{}.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(&MembershipWrapper::<UpdateProjectMembership> {
204 membership: (*self).to_owned(),
205 })?,
206 )))
207 }
208}
209
210#[derive(Debug, Clone, Builder)]
212#[builder(setter(strip_option))]
213pub struct DeleteProjectMembership {
214 id: u64,
216}
217
218impl DeleteProjectMembership {
219 #[must_use]
221 pub fn builder() -> DeleteProjectMembershipBuilder {
222 DeleteProjectMembershipBuilder::default()
223 }
224}
225
226impl Endpoint for DeleteProjectMembership {
227 fn method(&self) -> Method {
228 Method::DELETE
229 }
230
231 fn endpoint(&self) -> Cow<'static, str> {
232 format!("memberships/{}.json", &self.id).into()
233 }
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
238pub struct MembershipsWrapper<T> {
239 pub memberships: Vec<T>,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
246pub struct MembershipWrapper<T> {
247 pub membership: 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 PROJECT_MEMBERSHIP_LOCK: RwLock<()> = RwLock::const_new(());
263
264 #[traced_test]
265 #[test]
266 fn test_list_project_memberships_no_pagination() -> Result<(), Box<dyn Error>> {
267 let _r_project_memberships = PROJECT_MEMBERSHIP_LOCK.read();
268 dotenvy::dotenv()?;
269 let redmine = crate::api::Redmine::from_env()?;
270 let endpoint = ListProjectMemberships::builder()
271 .project_id_or_name("sandbox")
272 .build()?;
273 redmine.json_response_body::<_, MembershipsWrapper<ProjectMembership>>(&endpoint)?;
274 Ok(())
275 }
276
277 #[traced_test]
278 #[test]
279 fn test_list_project_memberships_first_page() -> Result<(), Box<dyn Error>> {
280 let _r_project_memberships = PROJECT_MEMBERSHIP_LOCK.read();
281 dotenvy::dotenv()?;
282 let redmine = crate::api::Redmine::from_env()?;
283 let endpoint = ListProjectMemberships::builder()
284 .project_id_or_name("sandbox")
285 .build()?;
286 redmine.json_response_body_page::<_, ProjectMembership>(&endpoint, 0, 25)?;
287 Ok(())
288 }
289
290 #[traced_test]
291 #[test]
292 fn test_list_project_memberships_all_pages() -> Result<(), Box<dyn Error>> {
293 let _r_project_memberships = PROJECT_MEMBERSHIP_LOCK.read();
294 dotenvy::dotenv()?;
295 let redmine = crate::api::Redmine::from_env()?;
296 let endpoint = ListProjectMemberships::builder()
297 .project_id_or_name("sandbox")
298 .build()?;
299 redmine.json_response_body_all_pages::<_, ProjectMembership>(&endpoint)?;
300 Ok(())
301 }
302
303 #[traced_test]
304 #[test]
305 fn test_get_project_membership() -> Result<(), Box<dyn Error>> {
306 let _r_project_memberships = PROJECT_MEMBERSHIP_LOCK.read();
307 dotenvy::dotenv()?;
308 let redmine = crate::api::Redmine::from_env()?;
309 let endpoint = GetProjectMembership::builder().id(238).build()?;
310 redmine.json_response_body::<_, MembershipWrapper<ProjectMembership>>(&endpoint)?;
311 Ok(())
312 }
313
314 #[function_name::named]
315 #[traced_test]
316 #[test]
317 fn test_create_project_membership() -> Result<(), Box<dyn Error>> {
318 let _w_project_memberships = PROJECT_MEMBERSHIP_LOCK.write();
319 let name = format!("unittest_{}", function_name!());
320 with_project(&name, |redmine, project_id, _| {
321 let create_endpoint = super::CreateProjectMembership::builder()
322 .project_id_or_name(project_id.to_string())
323 .user_id(1)
324 .role_ids(vec![8])
325 .build()?;
326 redmine
327 .json_response_body::<_, MembershipWrapper<ProjectMembership>>(&create_endpoint)?;
328 Ok(())
329 })?;
330 Ok(())
331 }
332
333 #[function_name::named]
334 #[traced_test]
335 #[test]
336 fn test_update_project_membership() -> Result<(), Box<dyn Error>> {
337 let _w_project_memberships = PROJECT_MEMBERSHIP_LOCK.write();
338 let name = format!("unittest_{}", function_name!());
339 with_project(&name, |redmine, project_id, _name| {
340 let create_endpoint = super::CreateProjectMembership::builder()
341 .project_id_or_name(project_id.to_string())
342 .user_id(1)
343 .role_ids(vec![8])
344 .build()?;
345 let MembershipWrapper { membership } = redmine
346 .json_response_body::<_, MembershipWrapper<ProjectMembership>>(&create_endpoint)?;
347 let update_endpoint = super::UpdateProjectMembership::builder()
348 .id(membership.id)
349 .role_ids(vec![9])
350 .build()?;
351 redmine.ignore_response_body::<_>(&update_endpoint)?;
352 Ok(())
353 })?;
354 Ok(())
355 }
356
357 #[traced_test]
362 #[test]
363 fn test_completeness_project_membership_type() -> Result<(), Box<dyn Error>> {
364 let _r_project_memberships = PROJECT_MEMBERSHIP_LOCK.read();
365 dotenvy::dotenv()?;
366 let redmine = crate::api::Redmine::from_env()?;
367 let endpoint = ListProjectMemberships::builder()
368 .project_id_or_name("sandbox")
369 .build()?;
370 let MembershipsWrapper {
371 memberships: values,
372 } = redmine.json_response_body::<_, MembershipsWrapper<serde_json::Value>>(&endpoint)?;
373 for value in values {
374 let o: ProjectMembership = serde_json::from_value(value.clone())?;
375 let reserialized = serde_json::to_value(o)?;
376 assert_eq!(value, reserialized);
377 }
378 Ok(())
379 }
380}