1use 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#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
18pub struct MyAccount {
19 pub id: u64,
21 pub login: String,
23 pub admin: bool,
25 pub firstname: String,
27 pub lastname: String,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub mail: Option<String>,
32 #[serde(
34 serialize_with = "crate::api::serialize_rfc3339",
35 deserialize_with = "crate::api::deserialize_rfc3339"
36 )]
37 pub created_on: time::OffsetDateTime,
38 #[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 pub api_key: String,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub twofa_scheme: Option<String>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub auth_source_id: Option<u64>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub must_change_passwd: Option<bool>,
56 #[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 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub custom_fields: Option<Vec<CustomFieldEssentialsWithValue>>,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
71#[serde(rename_all = "snake_case")]
72pub enum MailNotificationOption {
73 All,
75 Selected,
77 OnlyMyEvents,
79 OnlyAssigned,
81 OnlyOwner,
83 None,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
89#[serde(rename_all = "snake_case")]
90pub enum CommentsSorting {
91 Asc,
93 Desc,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
99#[serde(rename_all = "snake_case")]
100pub enum TextareaFont {
101 Monospace,
103 Proportional,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
109#[serde(rename_all = "snake_case")]
110pub enum ToolbarLanguage {
111 C,
113 Cpp,
115 Csharp,
117 Css,
119 Diff,
121 Go,
123 Groovy,
125 Html,
127 Java,
129 Javascript,
131 Objc,
133 Perl,
135 Php,
137 Python,
139 R,
141 Ruby,
143 Sass,
145 Scala,
147 Shell,
149 Sql,
151 Swift,
153 Xml,
155 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#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
191#[serde(rename_all = "snake_case")]
192pub enum AutoWatchAction {
193 IssueCreated,
195 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#[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 #[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#[serde_with::skip_serializing_none]
240#[derive(Debug, Clone, Builder, serde::Serialize)]
241#[builder(setter(strip_option))]
242pub struct UpdateMyAccount<'a> {
243 #[builder(setter(into), default)]
245 firstname: Option<Cow<'a, str>>,
246 #[builder(setter(into), default)]
248 lastname: Option<Cow<'a, str>>,
249 #[builder(setter(into), default)]
251 mail: Option<Cow<'a, str>>,
252 #[builder(default)]
254 mail_notification: Option<MailNotificationOption>,
255 #[builder(default)]
257 notified_project_ids: Option<Vec<u64>>,
258 #[builder(setter(into), default)]
260 language: Option<Cow<'a, str>>,
261 #[builder(default)]
263 hide_mail: Option<bool>,
264 #[builder(setter(into), default)]
266 time_zone: Option<Cow<'a, str>>,
267 #[builder(default)]
269 comments_sorting: Option<CommentsSorting>,
270 #[builder(default)]
272 warn_on_leaving_unsaved: Option<bool>,
273 #[builder(default)]
275 no_self_notified: Option<bool>,
276 #[builder(default)]
278 notify_about_high_priority_issues: Option<bool>,
279 #[builder(default)]
281 textarea_font: Option<TextareaFont>,
282 #[builder(default)]
284 recently_used_projects: Option<u64>,
285 #[builder(setter(into), default)]
287 history_default_tab: Option<Cow<'a, str>>,
288 #[builder(setter(into), default)]
290 default_issue_query: Option<Cow<'a, str>>,
291 #[builder(setter(into), default)]
293 default_project_query: Option<Cow<'a, str>>,
294 #[builder(default)]
296 toolbar_language_options: Option<Vec<ToolbarLanguage>>,
297 #[builder(default)]
299 auto_watch_on: Option<Vec<AutoWatchAction>>,
300}
301
302impl<'a> UpdateMyAccount<'a> {
303 #[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 #[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}