redmine_api/api/
roles.rs

1//! Roles Rest API Endpoint definitions
2//!
3//! [Redmine Documentation](https://www.redmine.org/projects/redmine/wiki/Rest_Roles)
4//!
5//! - [x] all roles endpoint
6//! - [x] specific role endpoint
7
8use derive_builder::Builder;
9use reqwest::Method;
10use std::borrow::Cow;
11
12use crate::api::{Endpoint, NoPagination, ReturnsJsonResponse};
13use serde_repr::{Deserialize_repr, Serialize_repr};
14
15/// a minimal type for Redmine roles used in lists of roles included in
16/// other Redmine objects (e.g. custom fields) and also in the global ListRoles
17/// endpoint (unlike most other Redmine API objects)
18#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19pub struct RoleEssentials {
20    /// numeric id
21    pub id: u64,
22    /// display name
23    pub name: String,
24    /// true if this role is inherited from a parent project, used e.g. in project memberships
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub inherited: Option<bool>,
27}
28
29/// determines which issues are visible to users/group with a role
30#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
31pub enum IssuesVisibility {
32    /// a user/group with the role can see all issues (in visible projects)
33    #[serde(rename = "all")]
34    All,
35    /// a user/group with the role can see all non-private issues (in visible projects)
36    #[serde(rename = "default")]
37    AllNonPrivate,
38    /// a user/group with the role can see only issues created by or assigned to them
39    #[serde(rename = "own")]
40    Own,
41}
42
43/// determines which time entries are visible to users/group with a role
44#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
45pub enum TimeEntriesVisibility {
46    /// a user/group with the role can see all time entries (in visible projects)
47    #[serde(rename = "all")]
48    All,
49    /// a user/group with the role can see only time entries created by them
50    #[serde(rename = "own")]
51    Own,
52}
53
54/// determines which users are visible to users/group with a role
55#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
56pub enum UsersVisibility {
57    /// a user/group with the role can see all active users
58    #[serde(rename = "all")]
59    All,
60    /// a user/group with the role can only see users which are members of the project
61    #[serde(rename = "members_of_visible_projects")]
62    MembersOfVisibleProjects,
63}
64
65/// The type of a built-in role
66#[derive(
67    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize_repr, Deserialize_repr,
68)]
69#[repr(u8)]
70pub enum BuiltinRole {
71    /// custom role
72    Custom = 0,
73    /// non-member role
74    NonMember = 1,
75    /// anonymous role
76    Anonymous = 2,
77}
78
79/// a type for roles to use as an API return type
80///
81/// alternatively you can use your own type limited to the fields you need
82#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
83pub struct Role {
84    /// numeric id
85    pub id: u64,
86    /// display name
87    pub name: String,
88    /// if this is true users/groups with this role can be assignees for issues
89    pub assignable: bool,
90    /// the issues that can be seen by users/groups with this role
91    pub issues_visibility: IssuesVisibility,
92    /// the time entries that can be seen by users/groups with this role
93    pub time_entries_visibility: TimeEntriesVisibility,
94    /// the users that can be seen by users/groups with this role
95    pub users_visibility: UsersVisibility,
96    /// list of permissions, this can contain core Redmine permissions
97    /// and those provided by plugins
98    pub permissions: Vec<String>,
99    /// the position of the role in the list
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub position: Option<u64>,
102    /// the type of built-in role
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub builtin: Option<BuiltinRole>,
105    /// settings for the role
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub settings: Option<serde_json::Value>,
108    /// the default time entry activity for this role
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub default_time_entry_activity_id: Option<u64>,
111}
112
113/// The endpoint for all roles
114///
115/// unlike most other Redmine objects this only returns a RoleEssentials like
116/// minimal object
117#[derive(Debug, Clone, Builder)]
118#[builder(setter(strip_option))]
119pub struct ListRoles {
120    /// filter for givable roles
121    #[builder(default)]
122    givable: Option<bool>,
123}
124
125impl ReturnsJsonResponse for ListRoles {}
126impl NoPagination for ListRoles {}
127
128impl ListRoles {
129    /// Create a builder for the endpoint.
130    #[must_use]
131    pub fn builder() -> ListRolesBuilder {
132        ListRolesBuilder::default()
133    }
134}
135
136impl Endpoint for ListRoles {
137    fn method(&self) -> Method {
138        Method::GET
139    }
140
141    fn endpoint(&self) -> Cow<'static, str> {
142        "roles.json".into()
143    }
144
145    fn parameters(&self) -> crate::api::QueryParams<'_> {
146        let mut params = crate::api::QueryParams::default();
147        params.push_opt("givable", self.givable);
148        params
149    }
150}
151
152/// The endpoint for a specific role
153#[derive(Debug, Clone, Builder)]
154#[builder(setter(strip_option))]
155pub struct GetRole {
156    /// the id of the role to retrieve
157    id: u64,
158}
159
160impl ReturnsJsonResponse for GetRole {}
161impl NoPagination for GetRole {}
162
163impl GetRole {
164    /// Create a builder for the endpoint.
165    #[must_use]
166    pub fn builder() -> GetRoleBuilder {
167        GetRoleBuilder::default()
168    }
169}
170
171impl Endpoint for GetRole {
172    fn method(&self) -> Method {
173        Method::GET
174    }
175
176    fn endpoint(&self) -> Cow<'static, str> {
177        format!("roles/{}.json", self.id).into()
178    }
179}
180
181/// helper struct for outer layers with a roles field holding the inner data
182#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
183pub struct RolesWrapper<T> {
184    /// to parse JSON with roles key
185    pub roles: Vec<T>,
186}
187
188/// A lot of APIs in Redmine wrap their data in an extra layer, this is a
189/// helper struct for outer layers with a role field holding the inner data
190#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
191pub struct RoleWrapper<T> {
192    /// to parse JSON with role key
193    pub role: T,
194}
195
196#[cfg(test)]
197mod test {
198    use super::*;
199    use pretty_assertions::assert_eq;
200    use std::error::Error;
201    use tracing_test::traced_test;
202
203    #[traced_test]
204    #[test]
205    fn test_list_roles_no_pagination() -> Result<(), Box<dyn Error>> {
206        dotenvy::dotenv()?;
207        let redmine = crate::api::Redmine::from_env(
208            reqwest::blocking::Client::builder()
209                .use_rustls_tls()
210                .build()?,
211        )?;
212        let endpoint = ListRoles::builder().build()?;
213        redmine.json_response_body::<_, RolesWrapper<RoleEssentials>>(&endpoint)?;
214        Ok(())
215    }
216
217    #[traced_test]
218    #[test]
219    fn test_list_roles_givable_filter() -> Result<(), Box<dyn Error>> {
220        dotenvy::dotenv()?;
221        let redmine = crate::api::Redmine::from_env(
222            reqwest::blocking::Client::builder()
223                .use_rustls_tls()
224                .build()?,
225        )?;
226        let endpoint = ListRoles::builder().givable(true).build()?;
227        redmine.json_response_body::<_, RolesWrapper<RoleEssentials>>(&endpoint)?;
228        Ok(())
229    }
230
231    #[test]
232    fn test_get_role() -> Result<(), Box<dyn Error>> {
233        dotenvy::dotenv()?;
234        let redmine = crate::api::Redmine::from_env(
235            reqwest::blocking::Client::builder()
236                .use_rustls_tls()
237                .build()?,
238        )?;
239        let endpoint = GetRole::builder().id(8).build()?;
240        redmine.json_response_body::<_, RoleWrapper<Role>>(&endpoint)?;
241        Ok(())
242    }
243
244    /// this tests if any of the results contain a field we are not deserializing
245    ///
246    /// this will only catch fields we missed if they are part of the response but
247    /// it is better than nothing
248    #[traced_test]
249    #[test]
250    fn test_completeness_role_type() -> Result<(), Box<dyn Error>> {
251        dotenvy::dotenv()?;
252        let redmine = crate::api::Redmine::from_env(
253            reqwest::blocking::Client::builder()
254                .use_rustls_tls()
255                .build()?,
256        )?;
257        let list_endpoint = ListRoles::builder().build()?;
258        let RolesWrapper { roles } =
259            redmine.json_response_body::<_, RolesWrapper<RoleEssentials>>(&list_endpoint)?;
260        for role in roles {
261            let endpoint = GetRole::builder().id(role.id).build()?;
262            let RoleWrapper { role: value } =
263                redmine.json_response_body::<_, RoleWrapper<serde_json::Value>>(&endpoint)?;
264            let o: Role = serde_json::from_value(value.clone())?;
265            let reserialized = serde_json::to_value(o)?;
266            assert_eq!(value, reserialized);
267        }
268        Ok(())
269    }
270}