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