Skip to main content

redmine_api/api/
my_account.rs

1//! My Account Rest API Endpoint definitions
2//!
3//! [Redmine Documentation](https://www.redmine.org/projects/redmine/wiki/Rest_MyAccount)
4//!
5//! - [x] my account endpoint
6
7use derive_builder::Builder;
8use reqwest::Method;
9use std::borrow::Cow;
10
11use crate::api::custom_fields::CustomFieldEssentialsWithValue;
12use crate::api::{Endpoint, NoPagination, ReturnsJsonResponse};
13
14/// a type for my account to use as an API return type
15///
16/// alternatively you can use your own type limited to the fields you need
17#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
18pub struct MyAccount {
19    /// numeric id
20    pub id: u64,
21    /// login name
22    pub login: String,
23    /// is this user an admin
24    pub admin: bool,
25    /// user's firstname
26    pub firstname: String,
27    /// user's lastname
28    pub lastname: String,
29    /// primary email of the user
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub mail: Option<String>,
32    /// The time when this user was created
33    #[serde(
34        serialize_with = "crate::api::serialize_rfc3339",
35        deserialize_with = "crate::api::deserialize_rfc3339"
36    )]
37    pub created_on: time::OffsetDateTime,
38    /// the time when this user last logged in
39    #[serde(
40        serialize_with = "crate::api::serialize_optional_rfc3339",
41        deserialize_with = "crate::api::deserialize_optional_rfc3339"
42    )]
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub last_login_on: Option<time::OffsetDateTime>,
45    /// the user's API key
46    pub api_key: String,
47    /// two-factor authentication scheme
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub twofa_scheme: Option<String>,
50    /// authentication source id
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub auth_source_id: Option<u64>,
53    /// whether the user must change password
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub must_change_passwd: Option<bool>,
56    /// the time when the password was last changed
57    #[serde(
58        default,
59        serialize_with = "crate::api::serialize_optional_rfc3339",
60        deserialize_with = "crate::api::deserialize_optional_rfc3339"
61    )]
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub passwd_changed_on: Option<time::OffsetDateTime>,
64    /// custom fields with values
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub custom_fields: Option<Vec<CustomFieldEssentialsWithValue>>,
67}
68
69/// Mail notification options for a user.
70#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
71#[serde(rename_all = "snake_case")]
72pub enum MailNotificationOption {
73    /// All events.
74    All,
75    /// Only for selected projects.
76    Selected,
77    /// Only for my events.
78    OnlyMyEvents,
79    /// Only for assigned issues.
80    OnlyAssigned,
81    /// Only for issues I own.
82    OnlyOwner,
83    /// No notifications.
84    None,
85}
86
87/// Comments sorting order.
88#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
89#[serde(rename_all = "snake_case")]
90pub enum CommentsSorting {
91    /// Ascending order.
92    Asc,
93    /// Descending order.
94    Desc,
95}
96
97/// Textarea font options.
98#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
99#[serde(rename_all = "snake_case")]
100pub enum TextareaFont {
101    /// Monospace font.
102    Monospace,
103    /// Proportional font.
104    Proportional,
105}
106
107/// Toolbar language options.
108#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
109#[serde(rename_all = "snake_case")]
110pub enum ToolbarLanguage {
111    /// C programming language.
112    C,
113    /// C++ programming language.
114    Cpp,
115    /// C# programming language.
116    Csharp,
117    /// CSS stylesheet language.
118    Css,
119    /// Diff format.
120    Diff,
121    /// Go programming language.
122    Go,
123    /// Groovy programming language.
124    Groovy,
125    /// HTML markup language.
126    Html,
127    /// Java programming language.
128    Java,
129    /// Javascript programming language.
130    Javascript,
131    /// Objective-C programming language.
132    Objc,
133    /// Perl programming language.
134    Perl,
135    /// PHP programming language.
136    Php,
137    /// Python programming language.
138    Python,
139    /// R programming language.
140    R,
141    /// Ruby programming language.
142    Ruby,
143    /// Sass stylesheet language.
144    Sass,
145    /// Scala programming language.
146    Scala,
147    /// Shell scripting language.
148    Shell,
149    /// SQL query language.
150    Sql,
151    /// Swift programming language.
152    Swift,
153    /// XML markup language.
154    Xml,
155    /// YAML data serialization language.
156    Yaml,
157}
158
159impl std::fmt::Display for ToolbarLanguage {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        match self {
162            ToolbarLanguage::C => write!(f, "c"),
163            ToolbarLanguage::Cpp => write!(f, "cpp"),
164            ToolbarLanguage::Csharp => write!(f, "csharp"),
165            ToolbarLanguage::Css => write!(f, "css"),
166            ToolbarLanguage::Diff => write!(f, "diff"),
167            ToolbarLanguage::Go => write!(f, "go"),
168            ToolbarLanguage::Groovy => write!(f, "groovy"),
169            ToolbarLanguage::Html => write!(f, "html"),
170            ToolbarLanguage::Java => write!(f, "java"),
171            ToolbarLanguage::Javascript => write!(f, "javascript"),
172            ToolbarLanguage::Objc => write!(f, "objc"),
173            ToolbarLanguage::Perl => write!(f, "perl"),
174            ToolbarLanguage::Php => write!(f, "php"),
175            ToolbarLanguage::Python => write!(f, "python"),
176            ToolbarLanguage::R => write!(f, "r"),
177            ToolbarLanguage::Ruby => write!(f, "ruby"),
178            ToolbarLanguage::Sass => write!(f, "sass"),
179            ToolbarLanguage::Scala => write!(f, "scala"),
180            ToolbarLanguage::Shell => write!(f, "shell"),
181            ToolbarLanguage::Sql => write!(f, "sql"),
182            ToolbarLanguage::Swift => write!(f, "swift"),
183            ToolbarLanguage::Xml => write!(f, "xml"),
184            ToolbarLanguage::Yaml => write!(f, "yaml"),
185        }
186    }
187}
188
189/// Auto watch on actions.
190#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
191#[serde(rename_all = "snake_case")]
192pub enum AutoWatchAction {
193    /// Watch issues when created.
194    IssueCreated,
195    /// Watch issues when contributed to.
196    IssueContributedTo,
197}
198
199impl std::fmt::Display for AutoWatchAction {
200    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201        match self {
202            AutoWatchAction::IssueCreated => write!(f, "issue_created"),
203            AutoWatchAction::IssueContributedTo => write!(f, "issue_contributed_to"),
204        }
205    }
206}
207
208/// The endpoint to retrieve the current user's my account settings/data
209#[derive(Debug, Clone, Builder)]
210#[builder(setter(strip_option))]
211pub struct GetMyAccount {}
212
213impl ReturnsJsonResponse for GetMyAccount {}
214impl NoPagination for GetMyAccount {}
215
216impl GetMyAccount {
217    /// Create a builder for the endpoint.
218    #[must_use]
219    pub fn builder() -> GetMyAccountBuilder {
220        GetMyAccountBuilder::default()
221    }
222}
223
224impl Endpoint for GetMyAccount {
225    fn method(&self) -> Method {
226        Method::GET
227    }
228
229    fn endpoint(&self) -> Cow<'static, str> {
230        "my/account.json".into()
231    }
232}
233
234/// The endpoint to update the current user's my account settings/data
235#[serde_with::skip_serializing_none]
236#[derive(Debug, Clone, Builder, serde::Serialize)]
237#[builder(setter(strip_option))]
238pub struct UpdateMyAccount<'a> {
239    /// user's firstname
240    #[builder(setter(into), default)]
241    firstname: Option<Cow<'a, str>>,
242    /// user's lastname
243    #[builder(setter(into), default)]
244    lastname: Option<Cow<'a, str>>,
245    /// primary email of the user
246    #[builder(setter(into), default)]
247    mail: Option<Cow<'a, str>>,
248    /// mail notification option
249    #[builder(default)]
250    mail_notification: Option<MailNotificationOption>,
251    /// project ids for which the user has explicitly turned mail notifications on
252    #[builder(default)]
253    notified_project_ids: Option<Vec<u64>>,
254    /// user's language
255    #[builder(setter(into), default)]
256    language: Option<Cow<'a, str>>,
257    /// hide mail address
258    #[builder(default)]
259    hide_mail: Option<bool>,
260    /// user's time zone
261    #[builder(setter(into), default)]
262    time_zone: Option<Cow<'a, str>>,
263    /// comments sorting order ('asc' or 'desc')
264    #[builder(default)]
265    comments_sorting: Option<CommentsSorting>,
266    /// warn on leaving unsaved changes
267    #[builder(default)]
268    warn_on_leaving_unsaved: Option<bool>,
269    /// no self notified
270    #[builder(default)]
271    no_self_notified: Option<bool>,
272    /// notify about high priority issues
273    #[builder(default)]
274    notify_about_high_priority_issues: Option<bool>,
275    /// textarea font ('monospace' or 'proportional')
276    #[builder(default)]
277    textarea_font: Option<TextareaFont>,
278    /// recently used projects
279    #[builder(default)]
280    recently_used_projects: Option<u64>,
281    /// history default tab
282    #[builder(setter(into), default)]
283    history_default_tab: Option<Cow<'a, str>>,
284    /// default issue query
285    #[builder(setter(into), default)]
286    default_issue_query: Option<Cow<'a, str>>,
287    /// default project query
288    #[builder(setter(into), default)]
289    default_project_query: Option<Cow<'a, str>>,
290    /// toolbar language options (comma-separated list of languages)
291    #[builder(default)]
292    toolbar_language_options: Option<Vec<ToolbarLanguage>>,
293    /// auto watch on (comma-separated list of actions)
294    #[builder(default)]
295    auto_watch_on: Option<Vec<AutoWatchAction>>,
296}
297
298impl<'a> UpdateMyAccount<'a> {
299    /// Create a builder for the endpoint.
300    #[must_use]
301    pub fn builder() -> UpdateMyAccountBuilder<'a> {
302        UpdateMyAccountBuilder::default()
303    }
304}
305
306impl Endpoint for UpdateMyAccount<'_> {
307    fn method(&self) -> Method {
308        Method::PUT
309    }
310
311    fn endpoint(&self) -> Cow<'static, str> {
312        "my/account.json".into()
313    }
314
315    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
316        use serde_json::json;
317        let mut user_params = serde_json::Map::new();
318        if let Some(ref firstname) = self.firstname {
319            user_params.insert("firstname".to_string(), json!(firstname));
320        }
321        if let Some(ref lastname) = self.lastname {
322            user_params.insert("lastname".to_string(), json!(lastname));
323        }
324        if let Some(ref mail) = self.mail {
325            user_params.insert("mail".to_string(), json!(mail));
326        }
327        if let Some(ref mail_notification) = self.mail_notification {
328            user_params.insert("mail_notification".to_string(), json!(mail_notification));
329        }
330        if let Some(ref notified_project_ids) = self.notified_project_ids {
331            user_params.insert(
332                "notified_project_ids".to_string(),
333                json!(notified_project_ids),
334            );
335        }
336        if let Some(ref language) = self.language {
337            user_params.insert("language".to_string(), json!(language));
338        }
339
340        let mut pref_params = serde_json::Map::new();
341        if let Some(hide_mail) = self.hide_mail {
342            pref_params.insert("hide_mail".to_string(), json!(hide_mail));
343        }
344        if let Some(ref time_zone) = self.time_zone {
345            pref_params.insert("time_zone".to_string(), json!(time_zone));
346        }
347        if let Some(ref comments_sorting) = self.comments_sorting {
348            pref_params.insert("comments_sorting".to_string(), json!(comments_sorting));
349        }
350        if let Some(warn_on_leaving_unsaved) = self.warn_on_leaving_unsaved {
351            pref_params.insert(
352                "warn_on_leaving_unsaved".to_string(),
353                json!(warn_on_leaving_unsaved),
354            );
355        }
356        if let Some(no_self_notified) = self.no_self_notified {
357            pref_params.insert("no_self_notified".to_string(), json!(no_self_notified));
358        }
359        if let Some(notify_about_high_priority_issues) = self.notify_about_high_priority_issues {
360            pref_params.insert(
361                "notify_about_high_priority_issues".to_string(),
362                json!(notify_about_high_priority_issues),
363            );
364        }
365        if let Some(ref textarea_font) = self.textarea_font {
366            pref_params.insert("textarea_font".to_string(), json!(textarea_font));
367        }
368        if let Some(recently_used_projects) = self.recently_used_projects {
369            pref_params.insert(
370                "recently_used_projects".to_string(),
371                json!(recently_used_projects),
372            );
373        }
374        if let Some(ref history_default_tab) = self.history_default_tab {
375            pref_params.insert(
376                "history_default_tab".to_string(),
377                json!(history_default_tab),
378            );
379        }
380        if let Some(ref default_issue_query) = self.default_issue_query {
381            pref_params.insert(
382                "default_issue_query".to_string(),
383                json!(default_issue_query),
384            );
385        }
386        if let Some(ref default_project_query) = self.default_project_query {
387            pref_params.insert(
388                "default_project_query".to_string(),
389                json!(default_project_query),
390            );
391        }
392        if let Some(ref toolbar_language_options) = self.toolbar_language_options {
393            pref_params.insert(
394                "toolbar_language_options".to_string(),
395                json!(
396                    toolbar_language_options
397                        .iter()
398                        .map(|e| e.to_string())
399                        .collect::<Vec<String>>()
400                        .join(",")
401                ),
402            );
403        }
404        if let Some(ref auto_watch_on) = self.auto_watch_on {
405            pref_params.insert(
406                "auto_watch_on".to_string(),
407                json!(
408                    auto_watch_on
409                        .iter()
410                        .map(|e| e.to_string())
411                        .collect::<Vec<String>>()
412                        .join(",")
413                ),
414            );
415        }
416
417        let mut root_map = serde_json::Map::new();
418        if !user_params.is_empty() {
419            root_map.insert("user".to_string(), serde_json::Value::Object(user_params));
420        }
421        if !pref_params.is_empty() {
422            root_map.insert("pref".to_string(), serde_json::Value::Object(pref_params));
423        }
424
425        if root_map.is_empty() {
426            Ok(None)
427        } else {
428            Ok(Some((
429                "application/json",
430                serde_json::to_vec(&serde_json::Value::Object(root_map))?,
431            )))
432        }
433    }
434}
435
436#[cfg(test)]
437mod test {
438    use super::*;
439    use crate::api::users::UserWrapper;
440    use pretty_assertions::assert_eq;
441    use std::error::Error;
442    use tracing_test::traced_test;
443
444    #[traced_test]
445    #[test]
446    fn test_get_my_account() -> Result<(), Box<dyn Error>> {
447        dotenvy::dotenv()?;
448        let redmine = crate::api::Redmine::from_env(
449            reqwest::blocking::Client::builder()
450                .tls_backend_rustls()
451                .build()?,
452        )?;
453        let endpoint = GetMyAccount::builder().build()?;
454        redmine.json_response_body::<_, UserWrapper<MyAccount>>(&endpoint)?;
455        Ok(())
456    }
457
458    #[traced_test]
459    #[test]
460    fn test_update_my_account() -> Result<(), Box<dyn Error>> {
461        dotenvy::dotenv()?;
462        let redmine = crate::api::Redmine::from_env(
463            reqwest::blocking::Client::builder()
464                .tls_backend_rustls()
465                .build()?,
466        )?;
467        let get_endpoint = GetMyAccount::builder().build()?;
468        let original_account: UserWrapper<MyAccount> = redmine.json_response_body(&get_endpoint)?;
469        let update_endpoint = UpdateMyAccount::builder()
470            .firstname("NewFirstName")
471            .build()?;
472        redmine.ignore_response_body(&update_endpoint)?;
473        let updated_account: UserWrapper<MyAccount> = redmine.json_response_body(&get_endpoint)?;
474        assert_eq!(updated_account.user.firstname, "NewFirstName");
475        let restore_endpoint = UpdateMyAccount::builder()
476            .firstname(original_account.user.firstname.as_str())
477            .build()?;
478        redmine.ignore_response_body(&restore_endpoint)?;
479        let restored_account: UserWrapper<MyAccount> = redmine.json_response_body(&get_endpoint)?;
480        assert_eq!(
481            restored_account.user.firstname,
482            original_account.user.firstname
483        );
484        Ok(())
485    }
486
487    /// this tests if any of the results contain a field we are not deserializing
488    ///
489    /// this will only catch fields we missed if they are part of the response but
490    /// it is better than nothing
491    #[traced_test]
492    #[test]
493    fn test_completeness_my_account_type() -> Result<(), Box<dyn Error>> {
494        dotenvy::dotenv()?;
495        let redmine = crate::api::Redmine::from_env(
496            reqwest::blocking::Client::builder()
497                .tls_backend_rustls()
498                .build()?,
499        )?;
500        let endpoint = GetMyAccount::builder().build()?;
501        let UserWrapper { user: value } =
502            redmine.json_response_body::<_, UserWrapper<serde_json::Value>>(&endpoint)?;
503        let o: MyAccount = serde_json::from_value(value.clone())?;
504        let reserialized = serde_json::to_value(o)?;
505        assert_eq!(value, reserialized);
506        Ok(())
507    }
508}