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 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#[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 AutoWatchAction::IssueCreated => write!(f, "issue_created"),
203 AutoWatchAction::IssueContributedTo => write!(f, "issue_contributed_to"),
204 }
205 }
206}
207
208#[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 #[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#[serde_with::skip_serializing_none]
236#[derive(Debug, Clone, Builder, serde::Serialize)]
237#[builder(setter(strip_option))]
238pub struct UpdateMyAccount<'a> {
239 #[builder(setter(into), default)]
241 firstname: Option<Cow<'a, str>>,
242 #[builder(setter(into), default)]
244 lastname: Option<Cow<'a, str>>,
245 #[builder(setter(into), default)]
247 mail: Option<Cow<'a, str>>,
248 #[builder(default)]
250 mail_notification: Option<MailNotificationOption>,
251 #[builder(default)]
253 notified_project_ids: Option<Vec<u64>>,
254 #[builder(setter(into), default)]
256 language: Option<Cow<'a, str>>,
257 #[builder(default)]
259 hide_mail: Option<bool>,
260 #[builder(setter(into), default)]
262 time_zone: Option<Cow<'a, str>>,
263 #[builder(default)]
265 comments_sorting: Option<CommentsSorting>,
266 #[builder(default)]
268 warn_on_leaving_unsaved: Option<bool>,
269 #[builder(default)]
271 no_self_notified: Option<bool>,
272 #[builder(default)]
274 notify_about_high_priority_issues: Option<bool>,
275 #[builder(default)]
277 textarea_font: Option<TextareaFont>,
278 #[builder(default)]
280 recently_used_projects: Option<u64>,
281 #[builder(setter(into), default)]
283 history_default_tab: Option<Cow<'a, str>>,
284 #[builder(setter(into), default)]
286 default_issue_query: Option<Cow<'a, str>>,
287 #[builder(setter(into), default)]
289 default_project_query: Option<Cow<'a, str>>,
290 #[builder(default)]
292 toolbar_language_options: Option<Vec<ToolbarLanguage>>,
293 #[builder(default)]
295 auto_watch_on: Option<Vec<AutoWatchAction>>,
296}
297
298impl<'a> UpdateMyAccount<'a> {
299 #[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 #[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}