1use derive_builder::Builder;
20use reqwest::Method;
21use serde::Serialize;
22use std::borrow::Cow;
23
24use crate::api::attachments::Attachment;
25use crate::api::users::UserEssentials;
26use crate::api::{Endpoint, NoPagination, QueryParams, ReturnsJsonResponse};
27
28#[derive(Debug, Clone)]
30pub enum WikiPageInclude {
31 Attachments,
33}
34
35impl std::fmt::Display for WikiPageInclude {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 Self::Attachments => {
39 write!(f, "attachments")
40 }
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
47pub struct WikiPageParent {
48 pub title: String,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
56pub struct WikiPageEssentials {
57 pub title: String,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub parent: Option<WikiPageParent>,
62 pub version: u64,
64 #[serde(
66 serialize_with = "crate::api::serialize_rfc3339",
67 deserialize_with = "crate::api::deserialize_rfc3339"
68 )]
69 pub created_on: time::OffsetDateTime,
70 #[serde(
72 serialize_with = "crate::api::serialize_rfc3339",
73 deserialize_with = "crate::api::deserialize_rfc3339"
74 )]
75 pub updated_on: time::OffsetDateTime,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub attachments: Option<Vec<Attachment>>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub protected: Option<bool>,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
88pub struct WikiPage {
89 pub title: String,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub parent: Option<WikiPageParent>,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub author: Option<UserEssentials>,
97 pub text: String,
99 pub version: u64,
101 pub comments: String,
103 #[serde(
105 serialize_with = "crate::api::serialize_rfc3339",
106 deserialize_with = "crate::api::deserialize_rfc3339"
107 )]
108 pub created_on: time::OffsetDateTime,
109 #[serde(
111 serialize_with = "crate::api::serialize_rfc3339",
112 deserialize_with = "crate::api::deserialize_rfc3339"
113 )]
114 pub updated_on: time::OffsetDateTime,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub attachments: Option<Vec<Attachment>>,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub protected: Option<bool>,
121}
122
123#[derive(Debug, Clone, Builder)]
125#[builder(setter(strip_option))]
126pub struct ListProjectWikiPages<'a> {
127 #[builder(setter(into))]
129 project_id_or_name: Cow<'a, str>,
130 #[builder(default)]
132 include: Option<Vec<WikiPageInclude>>,
133}
134
135impl ReturnsJsonResponse for ListProjectWikiPages<'_> {}
136impl NoPagination for ListProjectWikiPages<'_> {}
137
138impl<'a> ListProjectWikiPages<'a> {
139 #[must_use]
141 pub fn builder() -> ListProjectWikiPagesBuilder<'a> {
142 ListProjectWikiPagesBuilder::default()
143 }
144}
145
146impl Endpoint for ListProjectWikiPages<'_> {
147 fn method(&self) -> Method {
148 Method::GET
149 }
150
151 fn endpoint(&self) -> Cow<'static, str> {
152 format!("projects/{}/wiki/index.json", self.project_id_or_name).into()
153 }
154
155 fn parameters(&self) -> QueryParams<'_> {
156 let mut params = QueryParams::default();
157 params.push_opt("include", self.include.as_ref());
158 params
159 }
160}
161
162#[derive(Debug, Clone, Builder)]
164#[builder(setter(strip_option))]
165pub struct GetProjectWikiPage<'a> {
166 #[builder(setter(into))]
168 project_id_or_name: Cow<'a, str>,
169 #[builder(setter(into))]
171 title: Cow<'a, str>,
172 #[builder(default)]
174 include: Option<Vec<WikiPageInclude>>,
175}
176
177impl ReturnsJsonResponse for GetProjectWikiPage<'_> {}
178impl NoPagination for GetProjectWikiPage<'_> {}
179
180impl<'a> GetProjectWikiPage<'a> {
181 #[must_use]
183 pub fn builder() -> GetProjectWikiPageBuilder<'a> {
184 GetProjectWikiPageBuilder::default()
185 }
186}
187
188impl Endpoint for GetProjectWikiPage<'_> {
189 fn method(&self) -> Method {
190 Method::GET
191 }
192
193 fn endpoint(&self) -> Cow<'static, str> {
194 format!(
195 "projects/{}/wiki/{}.json",
196 &self.project_id_or_name, &self.title
197 )
198 .into()
199 }
200
201 fn parameters(&self) -> QueryParams<'_> {
202 let mut params = QueryParams::default();
203 params.push_opt("include", self.include.as_ref());
204 params
205 }
206}
207
208#[derive(Debug, Clone, Builder)]
210#[builder(setter(strip_option))]
211pub struct GetProjectWikiPageVersion<'a> {
212 #[builder(setter(into))]
214 project_id_or_name: Cow<'a, str>,
215 #[builder(setter(into))]
217 title: Cow<'a, str>,
218 version: u64,
220 #[builder(default)]
222 include: Option<Vec<WikiPageInclude>>,
223}
224
225impl ReturnsJsonResponse for GetProjectWikiPageVersion<'_> {}
226impl NoPagination for GetProjectWikiPageVersion<'_> {}
227
228impl<'a> GetProjectWikiPageVersion<'a> {
229 #[must_use]
231 pub fn builder() -> GetProjectWikiPageVersionBuilder<'a> {
232 GetProjectWikiPageVersionBuilder::default()
233 }
234}
235
236impl Endpoint for GetProjectWikiPageVersion<'_> {
237 fn method(&self) -> Method {
238 Method::GET
239 }
240
241 fn endpoint(&self) -> Cow<'static, str> {
242 format!(
243 "projects/{}/wiki/{}/{}.json",
244 &self.project_id_or_name, &self.title, &self.version,
245 )
246 .into()
247 }
248
249 fn parameters(&self) -> QueryParams<'_> {
250 let mut params = QueryParams::default();
251 params.push_opt("include", self.include.as_ref());
252 params
253 }
254}
255
256#[derive(Debug, Clone, Builder, serde::Serialize, serde::Deserialize)]
258#[builder(setter(strip_option))]
259pub struct CreateOrUpdateProjectWikiPage<'a> {
260 #[serde(skip_serializing)]
262 #[builder(setter(into))]
263 project_id_or_name: Cow<'a, str>,
264 #[serde(skip_serializing)]
266 #[builder(setter(into))]
267 title: Cow<'a, str>,
268 #[serde(skip_serializing_if = "Option::is_none")]
270 #[builder(default)]
271 version: Option<u64>,
272 #[builder(setter(into))]
274 text: Cow<'a, str>,
275 #[builder(setter(into))]
277 comments: Cow<'a, str>,
278 #[serde(default, skip_serializing_if = "Option::is_none")]
280 #[builder(default)]
281 redirect_existing_links: Option<bool>,
282 #[serde(default, skip_serializing_if = "Option::is_none")]
284 #[builder(default)]
285 is_start_page: Option<bool>,
286}
287
288impl<'a> CreateOrUpdateProjectWikiPage<'a> {
289 #[must_use]
291 pub fn builder() -> CreateOrUpdateProjectWikiPageBuilder<'a> {
292 CreateOrUpdateProjectWikiPageBuilder::default()
293 }
294}
295
296impl Endpoint for CreateOrUpdateProjectWikiPage<'_> {
297 fn method(&self) -> Method {
298 Method::PUT
299 }
300
301 fn endpoint(&self) -> Cow<'static, str> {
302 format!(
303 "projects/{}/wiki/{}.json",
304 &self.project_id_or_name, &self.title
305 )
306 .into()
307 }
308
309 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
310 Ok(Some((
311 "application/json",
312 serde_json::to_vec(&WikiPageWrapper::<CreateOrUpdateProjectWikiPage> {
313 wiki_page: (*self).to_owned(),
314 })?,
315 )))
316 }
317}
318
319#[derive(Debug, Clone, Builder)]
321#[builder(setter(strip_option))]
322pub struct DeleteProjectWikiPage<'a> {
323 #[builder(setter(into))]
325 project_id_or_name: Cow<'a, str>,
326 #[builder(setter(into))]
328 title: Cow<'a, str>,
329 #[builder(default)]
331 todo: Option<Cow<'a, str>>,
332 #[builder(default)]
334 reassign_to_id: Option<u64>,
335}
336
337impl<'a> DeleteProjectWikiPage<'a> {
338 #[must_use]
340 pub fn builder() -> DeleteProjectWikiPageBuilder<'a> {
341 DeleteProjectWikiPageBuilder::default()
342 }
343}
344
345impl Endpoint for DeleteProjectWikiPage<'_> {
346 fn method(&self) -> Method {
347 Method::DELETE
348 }
349
350 fn endpoint(&self) -> Cow<'static, str> {
351 format!(
352 "projects/{}/wiki/{}.json",
353 &self.project_id_or_name, &self.title
354 )
355 .into()
356 }
357
358 fn parameters(&self) -> QueryParams<'_> {
359 let mut params = QueryParams::default();
360 params.push_opt("todo", self.todo.as_ref());
361 params.push_opt("reassign_to_id", self.reassign_to_id);
362 params
363 }
364}
365
366#[derive(Debug, Clone, Builder)]
368#[builder(setter(strip_option))]
369pub struct DeleteProjectWikiPageVersion<'a> {
370 #[builder(setter(into))]
372 project_id_or_name: Cow<'a, str>,
373 #[builder(setter(into))]
375 title: Cow<'a, str>,
376 version: u64,
378}
379
380impl<'a> DeleteProjectWikiPageVersion<'a> {
381 #[must_use]
383 pub fn builder() -> DeleteProjectWikiPageVersionBuilder<'a> {
384 DeleteProjectWikiPageVersionBuilder::default()
385 }
386}
387
388impl Endpoint for DeleteProjectWikiPageVersion<'_> {
389 fn method(&self) -> Method {
390 Method::DELETE
391 }
392
393 fn endpoint(&self) -> Cow<'static, str> {
394 format!(
395 "projects/{}/wiki/{}/{}/destroy_version.json",
396 &self.project_id_or_name, &self.title, &self.version
397 )
398 .into()
399 }
400}
401
402#[derive(Debug, Clone, Builder)]
404#[builder(setter(strip_option))]
405pub struct GetProjectWikiPageAnnotate<'a> {
406 #[builder(setter(into))]
408 project_id_or_name: Cow<'a, str>,
409 #[builder(setter(into))]
411 title: Cow<'a, str>,
412 version: u64,
414}
415
416impl<'a> GetProjectWikiPageAnnotate<'a> {
417 #[must_use]
419 pub fn builder() -> GetProjectWikiPageAnnotateBuilder<'a> {
420 GetProjectWikiPageAnnotateBuilder::default()
421 }
422}
423
424impl NoPagination for GetProjectWikiPageAnnotate<'_> {}
425
426impl Endpoint for GetProjectWikiPageAnnotate<'_> {
427 fn method(&self) -> Method {
428 Method::GET
429 }
430
431 fn endpoint(&self) -> Cow<'static, str> {
432 format!(
433 "projects/{}/wiki/{}/annotate.json",
434 &self.project_id_or_name, &self.title
435 )
436 .into()
437 }
438
439 fn parameters(&self) -> QueryParams<'_> {
440 let mut params = QueryParams::default();
441 params.push("v", self.version);
442 params
443 }
444}
445
446#[derive(Debug, Clone, Builder)]
448#[builder(setter(strip_option))]
449pub struct ExportProjectWikiPage<'a> {
450 #[builder(setter(into))]
452 project_id_or_name: Cow<'a, str>,
453 #[builder(setter(into))]
455 title: Cow<'a, str>,
456 #[builder(default)]
458 version: Option<u64>,
459}
460
461impl<'a> ExportProjectWikiPage<'a> {
462 #[must_use]
464 pub fn builder() -> ExportProjectWikiPageBuilder<'a> {
465 ExportProjectWikiPageBuilder::default()
466 }
467}
468
469impl NoPagination for ExportProjectWikiPage<'_> {}
470
471impl Endpoint for ExportProjectWikiPage<'_> {
472 fn method(&self) -> Method {
473 Method::GET
474 }
475
476 fn endpoint(&self) -> Cow<'static, str> {
477 format!(
478 "projects/{}/wiki/{}/export.json",
479 &self.project_id_or_name, &self.title
480 )
481 .into()
482 }
483
484 fn parameters(&self) -> QueryParams<'_> {
485 let mut params = QueryParams::default();
486 params.push_opt("v", self.version);
487 params
488 }
489}
490
491#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
493pub struct WikiPagesWrapper<T> {
494 pub wiki_pages: Vec<T>,
496}
497
498#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
501pub struct WikiPageWrapper<T> {
502 pub wiki_page: T,
504}
505
506#[cfg(test)]
507pub(crate) mod test {
508 use crate::api::projects::{ListProjects, Project, ProjectsInclude, test::PROJECT_LOCK};
509
510 use super::*;
511 use pretty_assertions::assert_eq;
512 use std::error::Error;
513 use tokio::sync::RwLock;
514 use tracing_test::traced_test;
515
516 pub static PROJECT_WIKI_PAGE_LOCK: RwLock<()> = RwLock::const_new(());
519
520 #[traced_test]
521 #[test]
522 fn test_list_project_wiki_pages() -> Result<(), Box<dyn Error>> {
523 let _r_project = PROJECT_LOCK.blocking_read();
524 let _r_project_wiki_pages = PROJECT_WIKI_PAGE_LOCK.blocking_read();
525 dotenvy::dotenv()?;
526 let redmine = crate::api::Redmine::from_env(
527 reqwest::blocking::Client::builder()
528 .tls_backend_rustls()
529 .build()?,
530 )?;
531 let endpoint = ListProjectWikiPages::builder()
532 .project_id_or_name("25")
533 .build()?;
534 redmine.json_response_body::<_, WikiPagesWrapper<WikiPageEssentials>>(&endpoint)?;
535 Ok(())
536 }
537
538 #[traced_test]
543 #[test]
544 fn test_completeness_wiki_page_essentials() -> Result<(), Box<dyn Error>> {
545 let _r_project = PROJECT_LOCK.blocking_read();
546 let _r_issues = PROJECT_WIKI_PAGE_LOCK.blocking_read();
547 dotenvy::dotenv()?;
548 let redmine = crate::api::Redmine::from_env(
549 reqwest::blocking::Client::builder()
550 .tls_backend_rustls()
551 .build()?,
552 )?;
553 let endpoint = ListProjects::builder()
554 .include(vec![ProjectsInclude::EnabledModules])
555 .build()?;
556 let projects = redmine.json_response_body_all_pages_iter::<_, Project>(&endpoint);
557 let mut checked_projects = 0;
558 for project in projects {
559 let project = project?;
560 if !project
561 .enabled_modules
562 .is_some_and(|em| em.iter().any(|m| m.name == "wiki"))
563 {
564 continue;
566 }
567 let endpoint = ListProjectWikiPages::builder()
568 .project_id_or_name(project.id.to_string())
569 .include(vec![WikiPageInclude::Attachments])
570 .build()?;
571 let Ok(WikiPagesWrapper { wiki_pages: values }) =
572 redmine.json_response_body::<_, WikiPagesWrapper<serde_json::Value>>(&endpoint)
573 else {
574 continue;
582 };
583 checked_projects += 1;
584 for value in values {
585 let o: WikiPageEssentials = serde_json::from_value(value.clone())?;
586 let reserialized = serde_json::to_value(o)?;
587 assert_eq!(value, reserialized);
588 }
589 }
590 assert!(checked_projects > 0);
591 Ok(())
592 }
593
594 #[traced_test]
595 #[test]
596 fn test_get_project_wiki_page() -> Result<(), Box<dyn Error>> {
597 let _r_project = PROJECT_LOCK.blocking_read();
598 let _r_project_wiki_pages = PROJECT_WIKI_PAGE_LOCK.blocking_read();
599 dotenvy::dotenv()?;
600 let redmine = crate::api::Redmine::from_env(
601 reqwest::blocking::Client::builder()
602 .tls_backend_rustls()
603 .build()?,
604 )?;
605 let endpoint = GetProjectWikiPage::builder()
606 .project_id_or_name("25")
607 .title("Administration")
608 .build()?;
609 redmine.json_response_body::<_, WikiPageWrapper<WikiPage>>(&endpoint)?;
610 Ok(())
611 }
612
613 #[traced_test]
618 #[test]
619 fn test_completeness_wiki_page() -> Result<(), Box<dyn Error>> {
620 let _r_project = PROJECT_LOCK.blocking_read();
621 let _r_issues = PROJECT_WIKI_PAGE_LOCK.blocking_read();
622 dotenvy::dotenv()?;
623 let redmine = crate::api::Redmine::from_env(
624 reqwest::blocking::Client::builder()
625 .tls_backend_rustls()
626 .build()?,
627 )?;
628 let endpoint = ListProjects::builder()
629 .include(vec![ProjectsInclude::EnabledModules])
630 .build()?;
631 let projects = redmine.json_response_body_all_pages_iter::<_, Project>(&endpoint);
632 let mut checked_pages = 0;
633 for project in projects {
634 let project = project?;
635 if !project
636 .enabled_modules
637 .is_some_and(|em| em.iter().any(|m| m.name == "wiki"))
638 {
639 continue;
641 }
642 let endpoint = ListProjectWikiPages::builder()
643 .project_id_or_name(project.id.to_string())
644 .include(vec![WikiPageInclude::Attachments])
645 .build()?;
646 let Ok(WikiPagesWrapper { wiki_pages }) =
647 redmine.json_response_body::<_, WikiPagesWrapper<WikiPageEssentials>>(&endpoint)
648 else {
649 continue;
657 };
658 checked_pages += 1;
659 for wiki_page in wiki_pages {
660 let endpoint = GetProjectWikiPage::builder()
661 .project_id_or_name(project.id.to_string())
662 .title(wiki_page.title)
663 .include(vec![WikiPageInclude::Attachments])
664 .build()?;
665 let WikiPageWrapper { wiki_page: value } = redmine
666 .json_response_body::<_, WikiPageWrapper<serde_json::Value>>(&endpoint)?;
667 let o: WikiPage = serde_json::from_value(value.clone())?;
668 let reserialized = serde_json::to_value(o)?;
669 assert_eq!(value, reserialized);
670 }
671 }
672 assert!(checked_pages > 0);
673 Ok(())
674 }
675
676 #[traced_test]
677 #[test]
678 fn test_get_project_wiki_page_version() -> Result<(), Box<dyn Error>> {
679 let _r_project = PROJECT_LOCK.blocking_read();
680 let _r_project_wiki_pages = PROJECT_WIKI_PAGE_LOCK.blocking_read();
681 dotenvy::dotenv()?;
682 let redmine = crate::api::Redmine::from_env(
683 reqwest::blocking::Client::builder()
684 .tls_backend_rustls()
685 .build()?,
686 )?;
687 let endpoint = GetProjectWikiPageVersion::builder()
688 .project_id_or_name("25")
689 .title("Administration")
690 .version(18)
691 .build()?;
692 redmine.json_response_body::<_, WikiPageWrapper<WikiPage>>(&endpoint)?;
693 Ok(())
694 }
695
696 #[traced_test]
697 #[test]
698 fn test_create_update_and_delete_project_wiki_page() -> Result<(), Box<dyn Error>> {
699 let _r_project = PROJECT_LOCK.blocking_read();
700 let _w_project_wiki_pages = PROJECT_WIKI_PAGE_LOCK.blocking_write();
701 dotenvy::dotenv()?;
702 let redmine = crate::api::Redmine::from_env(
703 reqwest::blocking::Client::builder()
704 .tls_backend_rustls()
705 .build()?,
706 )?;
707 let endpoint = GetProjectWikiPage::builder()
708 .project_id_or_name("25")
709 .title("CreateWikiPageTest")
710 .build()?;
711 if redmine.ignore_response_body(&endpoint).is_ok() {
712 let endpoint = DeleteProjectWikiPage::builder()
714 .project_id_or_name("25")
715 .title("CreateWikiPageTest")
716 .build()?;
717 redmine.ignore_response_body(&endpoint)?;
718 }
719 let endpoint = CreateOrUpdateProjectWikiPage::builder()
720 .project_id_or_name("25")
721 .title("CreateWikiPageTest")
722 .text("Test Content")
723 .comments("Create Page Test")
724 .build()?;
725 redmine.ignore_response_body(&endpoint)?;
726 let endpoint = CreateOrUpdateProjectWikiPage::builder()
727 .project_id_or_name("25")
728 .title("CreateWikiPageTest")
729 .text("Test Content Updates")
730 .version(1)
731 .comments("Update Page Test")
732 .build()?;
733 redmine.ignore_response_body(&endpoint)?;
734 let endpoint = DeleteProjectWikiPage::builder()
735 .project_id_or_name("25")
736 .title("CreateWikiPageTest")
737 .build()?;
738 redmine.ignore_response_body(&endpoint)?;
739 Ok(())
740 }
741
742 #[traced_test]
743 #[test]
744 fn test_wiki_page_lifecycle() -> Result<(), Box<dyn Error>> {
745 use crate::api::test_helpers::with_project;
746
747 with_project("test_wiki_page_lifecycle", |redmine, project_id, _| {
748 tracing::debug!("Creating wiki page TestWikiPage");
749 let endpoint = CreateOrUpdateProjectWikiPage::builder()
750 .project_id_or_name(project_id.to_string())
751 .title("TestWikiPage")
752 .text("Test Content")
753 .comments("Create Page Test")
754 .build()?;
755 redmine.ignore_response_body(&endpoint)?;
756
757 tracing::debug!("Verifying existence, content and version of wiki page TestWikiPage");
758 let get_endpoint = GetProjectWikiPage::builder()
759 .project_id_or_name(project_id.to_string())
760 .title("TestWikiPage")
761 .build()?;
762 let WikiPageWrapper { wiki_page } =
763 redmine.json_response_body::<_, WikiPageWrapper<WikiPage>>(&get_endpoint)?;
764 assert_eq!(wiki_page.text, "Test Content");
765 assert_eq!(wiki_page.version, 1);
766
767 tracing::debug!("Updating wiki page TestWikiPage");
768 let update_endpoint = CreateOrUpdateProjectWikiPage::builder()
769 .project_id_or_name(project_id.to_string())
770 .title("TestWikiPage")
771 .text("Test Content Updates")
772 .version(1)
773 .comments("Update Page Test")
774 .build()?;
775 redmine.ignore_response_body(&update_endpoint)?;
776
777 tracing::debug!(
778 "Verifying existence, content and version of updated wiki page TestWikiPage"
779 );
780 let get_endpoint = GetProjectWikiPage::builder()
781 .project_id_or_name(project_id.to_string())
782 .title("TestWikiPage")
783 .build()?;
784 let WikiPageWrapper { wiki_page } =
785 redmine.json_response_body::<_, WikiPageWrapper<WikiPage>>(&get_endpoint)?;
786 assert_eq!(wiki_page.text, "Test Content Updates");
787 assert_eq!(wiki_page.version, 2);
788
789 tracing::debug!("Verifying existence and content of wiki page TestWikiPage version 1");
790 let version_endpoint = GetProjectWikiPageVersion::builder()
791 .project_id_or_name(project_id.to_string())
792 .title("TestWikiPage")
793 .version(1)
794 .build()?;
795 let WikiPageWrapper { wiki_page } =
796 redmine.json_response_body::<_, WikiPageWrapper<WikiPage>>(&version_endpoint)?;
797 assert_eq!(wiki_page.text, "Test Content");
798
799 tracing::debug!("Deleting wiki page TestWikiPage");
800 let delete_endpoint = DeleteProjectWikiPage::builder()
801 .project_id_or_name(project_id.to_string())
802 .title("TestWikiPage")
803 .build()?;
804 redmine.ignore_response_body(&delete_endpoint)?;
805
806 Ok(())
807 })
808 }
809}