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            Self::C => write!(f, "c"),
163            Self::Cpp => write!(f, "cpp"),
164            Self::Csharp => write!(f, "csharp"),
165            Self::Css => write!(f, "css"),
166            Self::Diff => write!(f, "diff"),
167            Self::Go => write!(f, "go"),
168            Self::Groovy => write!(f, "groovy"),
169            Self::Html => write!(f, "html"),
170            Self::Java => write!(f, "java"),
171            Self::Javascript => write!(f, "javascript"),
172            Self::Objc => write!(f, "objc"),
173            Self::Perl => write!(f, "perl"),
174            Self::Php => write!(f, "php"),
175            Self::Python => write!(f, "python"),
176            Self::R => write!(f, "r"),
177            Self::Ruby => write!(f, "ruby"),
178            Self::Sass => write!(f, "sass"),
179            Self::Scala => write!(f, "scala"),
180            Self::Shell => write!(f, "shell"),
181            Self::Sql => write!(f, "sql"),
182            Self::Swift => write!(f, "swift"),
183            Self::Xml => write!(f, "xml"),
184            Self::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            Self::IssueCreated => write!(f, "issue_created"),
203            Self::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))]
211#[expect(
212    clippy::empty_structs_with_brackets,
213    reason = "derive_builder requires named-field syntax"
214)]
215pub struct GetMyAccount {}
216
217impl ReturnsJsonResponse for GetMyAccount {}
218impl NoPagination for GetMyAccount {}
219
220impl GetMyAccount {
221    /// Create a builder for the endpoint.
222    #[must_use]
223    pub fn builder() -> GetMyAccountBuilder {
224        GetMyAccountBuilder::default()
225    }
226}
227
228impl Endpoint for GetMyAccount {
229    fn method(&self) -> Method {
230        Method::GET
231    }
232
233    fn endpoint(&self) -> Cow<'static, str> {
234        "my/account.json".into()
235    }
236}
237
238/// The endpoint to update the current user's my account settings/data
239#[serde_with::skip_serializing_none]
240#[derive(Debug, Clone, Builder, serde::Serialize)]
241#[builder(setter(strip_option))]
242pub struct UpdateMyAccount<'a> {
243    /// user's firstname
244    #[builder(setter(into), default)]
245    firstname: Option<Cow<'a, str>>,
246    /// user's lastname
247    #[builder(setter(into), default)]
248    lastname: Option<Cow<'a, str>>,
249    /// primary email of the user
250    #[builder(setter(into), default)]
251    mail: Option<Cow<'a, str>>,
252    /// mail notification option
253    #[builder(default)]
254    mail_notification: Option<MailNotificationOption>,
255    /// project ids for which the user has explicitly turned mail notifications on
256    #[builder(default)]
257    notified_project_ids: Option<Vec<u64>>,
258    /// user's language
259    #[builder(setter(into), default)]
260    language: Option<Cow<'a, str>>,
261    /// hide mail address
262    #[builder(default)]
263    hide_mail: Option<bool>,
264    /// user's time zone
265    #[builder(setter(into), default)]
266    time_zone: Option<Cow<'a, str>>,
267    /// comments sorting order ('asc' or 'desc')
268    #[builder(default)]
269    comments_sorting: Option<CommentsSorting>,
270    /// warn on leaving unsaved changes
271    #[builder(default)]
272    warn_on_leaving_unsaved: Option<bool>,
273    /// no self notified
274    #[builder(default)]
275    no_self_notified: Option<bool>,
276    /// notify about high priority issues
277    #[builder(default)]
278    notify_about_high_priority_issues: Option<bool>,
279    /// textarea font ('monospace' or 'proportional')
280    #[builder(default)]
281    textarea_font: Option<TextareaFont>,
282    /// recently used projects
283    #[builder(default)]
284    recently_used_projects: Option<u64>,
285    /// history default tab
286    #[builder(setter(into), default)]
287    history_default_tab: Option<Cow<'a, str>>,
288    /// default issue query
289    #[builder(setter(into), default)]
290    default_issue_query: Option<Cow<'a, str>>,
291    /// default project query
292    #[builder(setter(into), default)]
293    default_project_query: Option<Cow<'a, str>>,
294    /// toolbar language options (comma-separated list of languages)
295    #[builder(default)]
296    toolbar_language_options: Option<Vec<ToolbarLanguage>>,
297    /// auto watch on (comma-separated list of actions)
298    #[builder(default)]
299    auto_watch_on: Option<Vec<AutoWatchAction>>,
300}
301
302impl<'a> UpdateMyAccount<'a> {
303    /// Create a builder for the endpoint.
304    #[must_use]
305    pub fn builder() -> UpdateMyAccountBuilder<'a> {
306        UpdateMyAccountBuilder::default()
307    }
308}
309
310impl Endpoint for UpdateMyAccount<'_> {
311    fn method(&self) -> Method {
312        Method::PUT
313    }
314
315    fn endpoint(&self) -> Cow<'static, str> {
316        "my/account.json".into()
317    }
318
319    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
320        use serde_json::json;
321        let mut user_params = serde_json::Map::new();
322        if let Some(ref firstname) = self.firstname {
323            user_params.insert("firstname".to_string(), json!(firstname));
324        }
325        if let Some(ref lastname) = self.lastname {
326            user_params.insert("lastname".to_string(), json!(lastname));
327        }
328        if let Some(ref mail) = self.mail {
329            user_params.insert("mail".to_string(), json!(mail));
330        }
331        if let Some(ref mail_notification) = self.mail_notification {
332            user_params.insert("mail_notification".to_string(), json!(mail_notification));
333        }
334        if let Some(ref notified_project_ids) = self.notified_project_ids {
335            user_params.insert(
336                "notified_project_ids".to_string(),
337                json!(notified_project_ids),
338            );
339        }
340        if let Some(ref language) = self.language {
341            user_params.insert("language".to_string(), json!(language));
342        }
343
344        let mut pref_params = serde_json::Map::new();
345        if let Some(hide_mail) = self.hide_mail {
346            pref_params.insert("hide_mail".to_string(), json!(hide_mail));
347        }
348        if let Some(ref time_zone) = self.time_zone {
349            pref_params.insert("time_zone".to_string(), json!(time_zone));
350        }
351        if let Some(ref comments_sorting) = self.comments_sorting {
352            pref_params.insert("comments_sorting".to_string(), json!(comments_sorting));
353        }
354        if let Some(warn_on_leaving_unsaved) = self.warn_on_leaving_unsaved {
355            pref_params.insert(
356                "warn_on_leaving_unsaved".to_string(),
357                json!(warn_on_leaving_unsaved),
358            );
359        }
360        if let Some(no_self_notified) = self.no_self_notified {
361            pref_params.insert("no_self_notified".to_string(), json!(no_self_notified));
362        }
363        if let Some(notify_about_high_priority_issues) = self.notify_about_high_priority_issues {
364            pref_params.insert(
365                "notify_about_high_priority_issues".to_string(),
366                json!(notify_about_high_priority_issues),
367            );
368        }
369        if let Some(ref textarea_font) = self.textarea_font {
370            pref_params.insert("textarea_font".to_string(), json!(textarea_font));
371        }
372        if let Some(recently_used_projects) = self.recently_used_projects {
373            pref_params.insert(
374                "recently_used_projects".to_string(),
375                json!(recently_used_projects),
376            );
377        }
378        if let Some(ref history_default_tab) = self.history_default_tab {
379            pref_params.insert(
380                "history_default_tab".to_string(),
381                json!(history_default_tab),
382            );
383        }
384        if let Some(ref default_issue_query) = self.default_issue_query {
385            pref_params.insert(
386                "default_issue_query".to_string(),
387                json!(default_issue_query),
388            );
389        }
390        if let Some(ref default_project_query) = self.default_project_query {
391            pref_params.insert(
392                "default_project_query".to_string(),
393                json!(default_project_query),
394            );
395        }
396        if let Some(ref toolbar_language_options) = self.toolbar_language_options {
397            pref_params.insert(
398                "toolbar_language_options".to_string(),
399                json!(
400                    toolbar_language_options
401                        .iter()
402                        .map(|e| e.to_string())
403                        .collect::<Vec<String>>()
404                        .join(",")
405                ),
406            );
407        }
408        if let Some(ref auto_watch_on) = self.auto_watch_on {
409            pref_params.insert(
410                "auto_watch_on".to_string(),
411                json!(
412                    auto_watch_on
413                        .iter()
414                        .map(|e| e.to_string())
415                        .collect::<Vec<String>>()
416                        .join(",")
417                ),
418            );
419        }
420
421        let mut root_map = serde_json::Map::new();
422        if !user_params.is_empty() {
423            root_map.insert("user".to_string(), serde_json::Value::Object(user_params));
424        }
425        if !pref_params.is_empty() {
426            root_map.insert("pref".to_string(), serde_json::Value::Object(pref_params));
427        }
428
429        if root_map.is_empty() {
430            Ok(None)
431        } else {
432            Ok(Some((
433                "application/json",
434                serde_json::to_vec(&serde_json::Value::Object(root_map))?,
435            )))
436        }
437    }
438}
439
440#[cfg(test)]
441mod test {
442    use super::*;
443    use crate::api::users::UserWrapper;
444    use pretty_assertions::assert_eq;
445    use std::error::Error;
446    use tracing_test::traced_test;
447
448    #[traced_test]
449    #[test]
450    fn test_get_my_account() -> Result<(), Box<dyn Error>> {
451        dotenvy::dotenv()?;
452        let redmine = crate::api::Redmine::from_env(
453            reqwest::blocking::Client::builder()
454                .tls_backend_rustls()
455                .build()?,
456        )?;
457        let endpoint = GetMyAccount::builder().build()?;
458        redmine.json_response_body::<_, UserWrapper<MyAccount>>(&endpoint)?;
459        Ok(())
460    }
461
462    #[traced_test]
463    #[test]
464    fn test_update_my_account() -> Result<(), Box<dyn Error>> {
465        dotenvy::dotenv()?;
466        let redmine = crate::api::Redmine::from_env(
467            reqwest::blocking::Client::builder()
468                .tls_backend_rustls()
469                .build()?,
470        )?;
471        let get_endpoint = GetMyAccount::builder().build()?;
472        let original_account: UserWrapper<MyAccount> = redmine.json_response_body(&get_endpoint)?;
473        let update_endpoint = UpdateMyAccount::builder()
474            .firstname("NewFirstName")
475            .build()?;
476        redmine.ignore_response_body(&update_endpoint)?;
477        let updated_account: UserWrapper<MyAccount> = redmine.json_response_body(&get_endpoint)?;
478        assert_eq!(updated_account.user.firstname, "NewFirstName");
479        let restore_endpoint = UpdateMyAccount::builder()
480            .firstname(original_account.user.firstname.as_str())
481            .build()?;
482        redmine.ignore_response_body(&restore_endpoint)?;
483        let restored_account: UserWrapper<MyAccount> = redmine.json_response_body(&get_endpoint)?;
484        assert_eq!(
485            restored_account.user.firstname,
486            original_account.user.firstname
487        );
488        Ok(())
489    }
490
491    /// this tests if any of the results contain a field we are not deserializing
492    ///
493    /// this will only catch fields we missed if they are part of the response but
494    /// it is better than nothing
495    #[traced_test]
496    #[test]
497    fn test_completeness_my_account_type() -> Result<(), Box<dyn Error>> {
498        dotenvy::dotenv()?;
499        let redmine = crate::api::Redmine::from_env(
500            reqwest::blocking::Client::builder()
501                .tls_backend_rustls()
502                .build()?,
503        )?;
504        let endpoint = GetMyAccount::builder().build()?;
505        let UserWrapper { user: value } =
506            redmine.json_response_body::<_, UserWrapper<serde_json::Value>>(&endpoint)?;
507        let o: MyAccount = serde_json::from_value(value.clone())?;
508        let reserialized = serde_json::to_value(o)?;
509        assert_eq!(value, reserialized);
510        Ok(())
511    }
512}