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, 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 {}
95
96impl ListRoles {
97    /// Create a builder for the endpoint.
98    #[must_use]
99    pub fn builder() -> ListRolesBuilder {
100        ListRolesBuilder::default()
101    }
102}
103
104impl Endpoint for ListRoles {
105    fn method(&self) -> Method {
106        Method::GET
107    }
108
109    fn endpoint(&self) -> Cow<'static, str> {
110        "roles.json".into()
111    }
112}
113
114/// The endpoint for a specific role
115#[derive(Debug, Clone, Builder)]
116#[builder(setter(strip_option))]
117pub struct GetRole {
118    /// the id of the role to retrieve
119    id: u64,
120}
121
122impl ReturnsJsonResponse for GetRole {}
123
124impl GetRole {
125    /// Create a builder for the endpoint.
126    #[must_use]
127    pub fn builder() -> GetRoleBuilder {
128        GetRoleBuilder::default()
129    }
130}
131
132impl Endpoint for GetRole {
133    fn method(&self) -> Method {
134        Method::GET
135    }
136
137    fn endpoint(&self) -> Cow<'static, str> {
138        format!("roles/{}.json", self.id).into()
139    }
140}
141
142/// helper struct for outer layers with a roles field holding the inner data
143#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
144pub struct RolesWrapper<T> {
145    /// to parse JSON with roles key
146    pub roles: Vec<T>,
147}
148
149/// A lot of APIs in Redmine wrap their data in an extra layer, this is a
150/// helper struct for outer layers with a role field holding the inner data
151#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
152pub struct RoleWrapper<T> {
153    /// to parse JSON with role key
154    pub role: T,
155}
156
157#[cfg(test)]
158mod test {
159    use super::*;
160    use pretty_assertions::assert_eq;
161    use std::error::Error;
162    use tracing_test::traced_test;
163
164    #[traced_test]
165    #[test]
166    fn test_list_roles_no_pagination() -> Result<(), Box<dyn Error>> {
167        dotenvy::dotenv()?;
168        let redmine = crate::api::Redmine::from_env()?;
169        let endpoint = ListRoles::builder().build()?;
170        redmine.json_response_body::<_, RolesWrapper<RoleEssentials>>(&endpoint)?;
171        Ok(())
172    }
173
174    #[test]
175    fn test_get_role() -> Result<(), Box<dyn Error>> {
176        dotenvy::dotenv()?;
177        let redmine = crate::api::Redmine::from_env()?;
178        let endpoint = GetRole::builder().id(8).build()?;
179        redmine.json_response_body::<_, RoleWrapper<Role>>(&endpoint)?;
180        Ok(())
181    }
182
183    /// this tests if any of the results contain a field we are not deserializing
184    ///
185    /// this will only catch fields we missed if they are part of the response but
186    /// it is better than nothing
187    #[traced_test]
188    #[test]
189    fn test_completeness_role_type() -> Result<(), Box<dyn Error>> {
190        dotenvy::dotenv()?;
191        let redmine = crate::api::Redmine::from_env()?;
192        let list_endpoint = ListRoles::builder().build()?;
193        let RolesWrapper { roles } =
194            redmine.json_response_body::<_, RolesWrapper<RoleEssentials>>(&list_endpoint)?;
195        for role in roles {
196            let endpoint = GetRole::builder().id(role.id).build()?;
197            let RoleWrapper { role: value } =
198                redmine.json_response_body::<_, RoleWrapper<serde_json::Value>>(&endpoint)?;
199            let o: Role = serde_json::from_value(value.clone())?;
200            let reserialized = serde_json::to_value(o)?;
201            assert_eq!(value, reserialized);
202        }
203        Ok(())
204    }
205}