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, NoPagination, 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 {}
108impl NoPagination for GetProjectMembership {}
109
110impl GetProjectMembership {
111 #[must_use]
113 pub fn builder() -> GetProjectMembershipBuilder {
114 GetProjectMembershipBuilder::default()
115 }
116}
117
118impl Endpoint for GetProjectMembership {
119 fn method(&self) -> Method {
120 Method::GET
121 }
122
123 fn endpoint(&self) -> Cow<'static, str> {
124 format!("memberships/{}.json", &self.id).into()
125 }
126}
127
128#[serde_with::skip_serializing_none]
130#[derive(Debug, Clone, Builder, Serialize)]
131#[builder(setter(strip_option))]
132pub struct CreateProjectMembership<'a> {
133 #[builder(setter(into))]
135 #[serde(skip_serializing)]
136 project_id_or_name: Cow<'a, str>,
137 user_id: u64,
139 role_ids: Vec<u64>,
141}
142
143impl ReturnsJsonResponse for CreateProjectMembership<'_> {}
144impl NoPagination for CreateProjectMembership<'_> {}
145
146impl<'a> CreateProjectMembership<'a> {
147 #[must_use]
149 pub fn builder() -> CreateProjectMembershipBuilder<'a> {
150 CreateProjectMembershipBuilder::default()
151 }
152}
153
154impl Endpoint for CreateProjectMembership<'_> {
155 fn method(&self) -> Method {
156 Method::POST
157 }
158
159 fn endpoint(&self) -> Cow<'static, str> {
160 format!("projects/{}/memberships.json", self.project_id_or_name).into()
161 }
162
163 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
164 Ok(Some((
165 "application/json",
166 serde_json::to_vec(&MembershipWrapper::<CreateProjectMembership> {
167 membership: (*self).to_owned(),
168 })?,
169 )))
170 }
171}
172
173#[serde_with::skip_serializing_none]
175#[derive(Debug, Clone, Builder, Serialize)]
176#[builder(setter(strip_option))]
177pub struct UpdateProjectMembership {
178 #[serde(skip_serializing)]
180 id: u64,
181 role_ids: Vec<u64>,
183}
184
185impl UpdateProjectMembership {
186 #[must_use]
188 pub fn builder() -> UpdateProjectMembershipBuilder {
189 UpdateProjectMembershipBuilder::default()
190 }
191}
192
193impl Endpoint for UpdateProjectMembership {
194 fn method(&self) -> Method {
195 Method::PUT
196 }
197
198 fn endpoint(&self) -> Cow<'static, str> {
199 format!("memberships/{}.json", self.id).into()
200 }
201
202 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
203 Ok(Some((
204 "application/json",
205 serde_json::to_vec(&MembershipWrapper::<UpdateProjectMembership> {
206 membership: (*self).to_owned(),
207 })?,
208 )))
209 }
210}
211
212#[derive(Debug, Clone, Builder)]
214#[builder(setter(strip_option))]
215pub struct DeleteProjectMembership {
216 id: u64,
218}
219
220impl DeleteProjectMembership {
221 #[must_use]
223 pub fn builder() -> DeleteProjectMembershipBuilder {
224 DeleteProjectMembershipBuilder::default()
225 }
226}
227
228impl Endpoint for DeleteProjectMembership {
229 fn method(&self) -> Method {
230 Method::DELETE
231 }
232
233 fn endpoint(&self) -> Cow<'static, str> {
234 format!("memberships/{}.json", &self.id).into()
235 }
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
240pub struct MembershipsWrapper<T> {
241 pub memberships: Vec<T>,
243}
244
245#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
248pub struct MembershipWrapper<T> {
249 pub membership: T,
251}
252
253#[cfg(test)]
254mod test {
255 use super::*;
256 use crate::api::test_helpers::with_project;
257 use pretty_assertions::assert_eq;
258 use std::error::Error;
259 use tokio::sync::RwLock;
260 use tracing_test::traced_test;
261
262 static PROJECT_MEMBERSHIP_LOCK: RwLock<()> = RwLock::const_new(());
265
266 #[traced_test]
267 #[test]
268 fn test_list_project_memberships_first_page() -> Result<(), Box<dyn Error>> {
269 let _r_project_memberships = PROJECT_MEMBERSHIP_LOCK.read();
270 dotenvy::dotenv()?;
271 let redmine = crate::api::Redmine::from_env(
272 reqwest::blocking::Client::builder()
273 .use_rustls_tls()
274 .build()?,
275 )?;
276 let endpoint = ListProjectMemberships::builder()
277 .project_id_or_name("sandbox")
278 .build()?;
279 redmine.json_response_body_page::<_, ProjectMembership>(&endpoint, 0, 25)?;
280 Ok(())
281 }
282
283 #[traced_test]
284 #[test]
285 fn test_list_project_memberships_all_pages() -> Result<(), Box<dyn Error>> {
286 let _r_project_memberships = PROJECT_MEMBERSHIP_LOCK.read();
287 dotenvy::dotenv()?;
288 let redmine = crate::api::Redmine::from_env(
289 reqwest::blocking::Client::builder()
290 .use_rustls_tls()
291 .build()?,
292 )?;
293 let endpoint = ListProjectMemberships::builder()
294 .project_id_or_name("sandbox")
295 .build()?;
296 redmine.json_response_body_all_pages::<_, ProjectMembership>(&endpoint)?;
297 Ok(())
298 }
299
300 #[traced_test]
301 #[test]
302 fn test_get_project_membership() -> Result<(), Box<dyn Error>> {
303 let _r_project_memberships = PROJECT_MEMBERSHIP_LOCK.read();
304 dotenvy::dotenv()?;
305 let redmine = crate::api::Redmine::from_env(
306 reqwest::blocking::Client::builder()
307 .use_rustls_tls()
308 .build()?,
309 )?;
310 let endpoint = GetProjectMembership::builder().id(238).build()?;
311 redmine.json_response_body::<_, MembershipWrapper<ProjectMembership>>(&endpoint)?;
312 Ok(())
313 }
314
315 #[function_name::named]
316 #[traced_test]
317 #[test]
318 fn test_create_project_membership() -> Result<(), Box<dyn Error>> {
319 let _w_project_memberships = PROJECT_MEMBERSHIP_LOCK.write();
320 let name = format!("unittest_{}", function_name!());
321 with_project(&name, |redmine, project_id, _| {
322 let create_endpoint = super::CreateProjectMembership::builder()
323 .project_id_or_name(project_id.to_string())
324 .user_id(1)
325 .role_ids(vec![8])
326 .build()?;
327 redmine
328 .json_response_body::<_, MembershipWrapper<ProjectMembership>>(&create_endpoint)?;
329 Ok(())
330 })?;
331 Ok(())
332 }
333
334 #[function_name::named]
335 #[traced_test]
336 #[test]
337 fn test_update_project_membership() -> Result<(), Box<dyn Error>> {
338 let _w_project_memberships = PROJECT_MEMBERSHIP_LOCK.write();
339 let name = format!("unittest_{}", function_name!());
340 with_project(&name, |redmine, project_id, _name| {
341 let create_endpoint = super::CreateProjectMembership::builder()
342 .project_id_or_name(project_id.to_string())
343 .user_id(1)
344 .role_ids(vec![8])
345 .build()?;
346 let MembershipWrapper { membership } = redmine
347 .json_response_body::<_, MembershipWrapper<ProjectMembership>>(&create_endpoint)?;
348 let update_endpoint = super::UpdateProjectMembership::builder()
349 .id(membership.id)
350 .role_ids(vec![9])
351 .build()?;
352 redmine.ignore_response_body::<_>(&update_endpoint)?;
353 Ok(())
354 })?;
355 Ok(())
356 }
357
358 #[traced_test]
363 #[test]
364 fn test_completeness_project_membership_type() -> Result<(), Box<dyn Error>> {
365 let _r_project_memberships = PROJECT_MEMBERSHIP_LOCK.read();
366 dotenvy::dotenv()?;
367 let redmine = crate::api::Redmine::from_env(
368 reqwest::blocking::Client::builder()
369 .use_rustls_tls()
370 .build()?,
371 )?;
372 let endpoint = ListProjectMemberships::builder()
373 .project_id_or_name("sandbox")
374 .build()?;
375 let values: Vec<serde_json::Value> = redmine.json_response_body_all_pages(&endpoint)?;
376 for value in values {
377 let o: ProjectMembership = serde_json::from_value(value.clone())?;
378 let reserialized = serde_json::to_value(o)?;
379 assert_eq!(value, reserialized);
380 }
381 Ok(())
382 }
383}