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