1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
//! Roles Rest API Endpoint definitions
//!
//! [Redmine Documentation](https://www.redmine.org/projects/redmine/wiki/Rest_Roles)
//!
//! - [x] all roles endpoint
//! - [x] specific role endpoint

use derive_builder::Builder;
use http::Method;
use std::borrow::Cow;

use crate::api::{Endpoint, ReturnsJsonResponse};

/// a minimal type for Redmine roles used in lists of roles included in
/// other Redmine objects (e.g. custom fields) and also in the global ListRoles
/// endpoint (unlike most other Redmine API objects)
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct RoleEssentials {
    /// numeric id
    id: u64,
    /// display name
    name: String,
    /// true if this role is inherited from a parent project, used e.g. in project memberships
    #[serde(default, skip_serializing_if = "Option::is_none")]
    inherited: Option<bool>,
}

/// determines which issues are visible to users/group with a role
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum IssuesVisibility {
    /// a user/group with the role can see all issues (in visible projects)
    #[serde(rename = "all")]
    All,
    /// a user/group with the role can see all non-private issues (in visible projects)
    #[serde(rename = "default")]
    AllNonPrivate,
    /// a user/group with the role can see only issues created by or assigned to them
    #[serde(rename = "own")]
    Own,
}

/// determines which time entries are visible to users/group with a role
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum TimeEntriesVisibility {
    /// a user/group with the role can see all time entries (in visible projects)
    #[serde(rename = "all")]
    All,
    /// a user/group with the role can see only time entries created by them
    #[serde(rename = "own")]
    Own,
}

/// determines which users are visible to users/group with a role
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum UsersVisibility {
    /// a user/group with the role can see all active users
    #[serde(rename = "all")]
    All,
    /// a user/group with the role can only see users which are members of the project
    #[serde(rename = "members_of_visible_projects")]
    MembersOfVisibleProjects,
}

/// a type for roles to use as an API return type
///
/// alternatively you can use your own type limited to the fields you need
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Role {
    /// numeric id
    pub id: u64,
    /// display name
    pub name: String,
    /// if this is true users/groups with this role can be assignees for issues
    pub assignable: bool,
    /// the issues that can be seen by users/groups with this role
    pub issues_visibility: IssuesVisibility,
    /// the time entries that can be seen by users/groups with this role
    pub time_entries_visibility: TimeEntriesVisibility,
    /// the users that can be seen by users/groups with this role
    pub users_visibility: UsersVisibility,
    /// list of permissions, this can contain core Redmine permissions
    /// and those provided by plugins
    pub permissions: Vec<String>,
}

/// The endpoint for all roles
///
/// unlike most other Redmine objects this only returns a RoleEssentials like
/// minimal object
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
pub struct ListRoles {}

impl ReturnsJsonResponse for ListRoles {}

impl ListRoles {
    /// Create a builder for the endpoint.
    #[must_use]
    pub fn builder() -> ListRolesBuilder {
        ListRolesBuilder::default()
    }
}

impl<'a> Endpoint for ListRoles {
    fn method(&self) -> Method {
        Method::GET
    }

    fn endpoint(&self) -> Cow<'static, str> {
        "roles.json".into()
    }
}

/// The endpoint for a specific role
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
pub struct GetRole {
    /// the id of the role to retrieve
    id: u64,
}

impl ReturnsJsonResponse for GetRole {}

impl GetRole {
    /// Create a builder for the endpoint.
    #[must_use]
    pub fn builder() -> GetRoleBuilder {
        GetRoleBuilder::default()
    }
}

impl<'a> Endpoint for GetRole {
    fn method(&self) -> Method {
        Method::GET
    }

    fn endpoint(&self) -> Cow<'static, str> {
        format!("roles/{}.json", self.id).into()
    }
}

/// helper struct for outer layers with a roles field holding the inner data
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct RolesWrapper<T> {
    /// to parse JSON with roles key
    pub roles: Vec<T>,
}

/// A lot of APIs in Redmine wrap their data in an extra layer, this is a
/// helper struct for outer layers with a role field holding the inner data
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct RoleWrapper<T> {
    /// to parse JSON with role key
    pub role: T,
}

#[cfg(test)]
mod test {
    use super::*;
    use pretty_assertions::assert_eq;
    use std::error::Error;
    use tracing_test::traced_test;

    #[traced_test]
    #[test]
    fn test_list_roles_no_pagination() -> Result<(), Box<dyn Error>> {
        dotenv::dotenv()?;
        let redmine = crate::api::Redmine::from_env()?;
        let endpoint = ListRoles::builder().build()?;
        redmine.json_response_body::<_, RolesWrapper<RoleEssentials>>(&endpoint)?;
        Ok(())
    }

    #[test]
    fn test_get_role() -> Result<(), Box<dyn Error>> {
        dotenv::dotenv()?;
        let redmine = crate::api::Redmine::from_env()?;
        let endpoint = GetRole::builder().id(8).build()?;
        redmine.json_response_body::<_, RoleWrapper<Role>>(&endpoint)?;
        Ok(())
    }

    /// this tests if any of the results contain a field we are not deserializing
    ///
    /// this will only catch fields we missed if they are part of the response but
    /// it is better than nothing
    #[traced_test]
    #[test]
    fn test_completeness_role_type() -> Result<(), Box<dyn Error>> {
        dotenv::dotenv()?;
        let redmine = crate::api::Redmine::from_env()?;
        let list_endpoint = ListRoles::builder().build()?;
        let RolesWrapper { roles } =
            redmine.json_response_body::<_, RolesWrapper<RoleEssentials>>(&list_endpoint)?;
        for role in roles {
            let endpoint = GetRole::builder().id(role.id).build()?;
            let RoleWrapper { role: value } =
                redmine.json_response_body::<_, RoleWrapper<serde_json::Value>>(&endpoint)?;
            let o: Role = serde_json::from_value(value.clone())?;
            let reserialized = serde_json::to_value(o)?;
            assert_eq!(value, reserialized);
        }
        Ok(())
    }
}