1use derive_builder::Builder;
13use reqwest::Method;
14use serde::Serialize;
15use std::borrow::Cow;
16
17use crate::api::attachments::Attachment;
18use crate::api::users::UserEssentials;
19use crate::api::{Endpoint, NoPagination, QueryParams, ReturnsJsonResponse};
20
21#[derive(Debug, Clone)]
23pub enum WikiPageInclude {
24 Attachments,
26}
27
28impl std::fmt::Display for WikiPageInclude {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self {
31 Self::Attachments => {
32 write!(f, "attachments")
33 }
34 }
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
40pub struct WikiPageParent {
41 pub title: String,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
49pub struct WikiPageEssentials {
50 pub title: String,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub parent: Option<WikiPageParent>,
55 pub version: u64,
57 #[serde(
59 serialize_with = "crate::api::serialize_rfc3339",
60 deserialize_with = "crate::api::deserialize_rfc3339"
61 )]
62 pub created_on: time::OffsetDateTime,
63 #[serde(
65 serialize_with = "crate::api::serialize_rfc3339",
66 deserialize_with = "crate::api::deserialize_rfc3339"
67 )]
68 pub updated_on: time::OffsetDateTime,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub attachments: Option<Vec<Attachment>>,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
78pub struct WikiPage {
79 pub title: String,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub parent: Option<WikiPageParent>,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub author: Option<UserEssentials>,
87 pub text: String,
89 pub version: u64,
91 pub comments: String,
93 #[serde(
95 serialize_with = "crate::api::serialize_rfc3339",
96 deserialize_with = "crate::api::deserialize_rfc3339"
97 )]
98 pub created_on: time::OffsetDateTime,
99 #[serde(
101 serialize_with = "crate::api::serialize_rfc3339",
102 deserialize_with = "crate::api::deserialize_rfc3339"
103 )]
104 pub updated_on: time::OffsetDateTime,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub attachments: Option<Vec<Attachment>>,
108}
109
110#[derive(Debug, Clone, Builder)]
112#[builder(setter(strip_option))]
113pub struct ListProjectWikiPages<'a> {
114 #[builder(setter(into))]
116 project_id_or_name: Cow<'a, str>,
117 #[builder(default)]
119 include: Option<Vec<WikiPageInclude>>,
120}
121
122impl<'a> ReturnsJsonResponse for ListProjectWikiPages<'a> {}
123impl<'a> NoPagination for ListProjectWikiPages<'a> {}
124
125impl<'a> ListProjectWikiPages<'a> {
126 #[must_use]
128 pub fn builder() -> ListProjectWikiPagesBuilder<'a> {
129 ListProjectWikiPagesBuilder::default()
130 }
131}
132
133impl<'a> Endpoint for ListProjectWikiPages<'a> {
134 fn method(&self) -> Method {
135 Method::GET
136 }
137
138 fn endpoint(&self) -> Cow<'static, str> {
139 format!("projects/{}/wiki/index.json", self.project_id_or_name).into()
140 }
141
142 fn parameters(&self) -> QueryParams<'_> {
143 let mut params = QueryParams::default();
144 params.push_opt("include", self.include.as_ref());
145 params
146 }
147}
148
149#[derive(Debug, Clone, Builder)]
151#[builder(setter(strip_option))]
152pub struct GetProjectWikiPage<'a> {
153 #[builder(setter(into))]
155 project_id_or_name: Cow<'a, str>,
156 #[builder(setter(into))]
158 title: Cow<'a, str>,
159 #[builder(default)]
161 include: Option<Vec<WikiPageInclude>>,
162}
163
164impl ReturnsJsonResponse for GetProjectWikiPage<'_> {}
165impl NoPagination for GetProjectWikiPage<'_> {}
166
167impl<'a> GetProjectWikiPage<'a> {
168 #[must_use]
170 pub fn builder() -> GetProjectWikiPageBuilder<'a> {
171 GetProjectWikiPageBuilder::default()
172 }
173}
174
175impl Endpoint for GetProjectWikiPage<'_> {
176 fn method(&self) -> Method {
177 Method::GET
178 }
179
180 fn endpoint(&self) -> Cow<'static, str> {
181 format!(
182 "projects/{}/wiki/{}.json",
183 &self.project_id_or_name, &self.title
184 )
185 .into()
186 }
187
188 fn parameters(&self) -> QueryParams<'_> {
189 let mut params = QueryParams::default();
190 params.push_opt("include", self.include.as_ref());
191 params
192 }
193}
194
195#[derive(Debug, Clone, Builder)]
197#[builder(setter(strip_option))]
198pub struct GetProjectWikiPageVersion<'a> {
199 #[builder(setter(into))]
201 project_id_or_name: Cow<'a, str>,
202 #[builder(setter(into))]
204 title: Cow<'a, str>,
205 version: u64,
207 #[builder(default)]
209 include: Option<Vec<WikiPageInclude>>,
210}
211
212impl ReturnsJsonResponse for GetProjectWikiPageVersion<'_> {}
213impl NoPagination for GetProjectWikiPageVersion<'_> {}
214
215impl<'a> GetProjectWikiPageVersion<'a> {
216 #[must_use]
218 pub fn builder() -> GetProjectWikiPageVersionBuilder<'a> {
219 GetProjectWikiPageVersionBuilder::default()
220 }
221}
222
223impl Endpoint for GetProjectWikiPageVersion<'_> {
224 fn method(&self) -> Method {
225 Method::GET
226 }
227
228 fn endpoint(&self) -> Cow<'static, str> {
229 format!(
230 "projects/{}/wiki/{}/{}.json",
231 &self.project_id_or_name, &self.title, &self.version,
232 )
233 .into()
234 }
235
236 fn parameters(&self) -> QueryParams<'_> {
237 let mut params = QueryParams::default();
238 params.push_opt("include", self.include.as_ref());
239 params
240 }
241}
242
243#[derive(Debug, Clone, Builder, serde::Serialize, serde::Deserialize)]
245#[builder(setter(strip_option))]
246pub struct CreateOrUpdateProjectWikiPage<'a> {
247 #[serde(skip_serializing)]
249 #[builder(setter(into))]
250 project_id_or_name: Cow<'a, str>,
251 #[serde(skip_serializing)]
253 #[builder(setter(into))]
254 title: Cow<'a, str>,
255 #[serde(skip_serializing_if = "Option::is_none")]
257 #[builder(default)]
258 version: Option<u64>,
259 #[builder(setter(into))]
261 text: Cow<'a, str>,
262 #[builder(setter(into))]
264 comments: Cow<'a, str>,
265}
266
267impl<'a> CreateOrUpdateProjectWikiPage<'a> {
268 #[must_use]
270 pub fn builder() -> CreateOrUpdateProjectWikiPageBuilder<'a> {
271 CreateOrUpdateProjectWikiPageBuilder::default()
272 }
273}
274
275impl Endpoint for CreateOrUpdateProjectWikiPage<'_> {
276 fn method(&self) -> Method {
277 Method::PUT
278 }
279
280 fn endpoint(&self) -> Cow<'static, str> {
281 format!(
282 "projects/{}/wiki/{}.json",
283 &self.project_id_or_name, &self.title
284 )
285 .into()
286 }
287
288 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
289 Ok(Some((
290 "application/json",
291 serde_json::to_vec(&WikiPageWrapper::<CreateOrUpdateProjectWikiPage> {
292 wiki_page: (*self).to_owned(),
293 })?,
294 )))
295 }
296}
297
298#[derive(Debug, Clone, Builder)]
300#[builder(setter(strip_option))]
301pub struct DeleteProjectWikiPage<'a> {
302 #[builder(setter(into))]
304 project_id_or_name: Cow<'a, str>,
305 #[builder(setter(into))]
307 title: Cow<'a, str>,
308}
309
310impl<'a> DeleteProjectWikiPage<'a> {
311 #[must_use]
313 pub fn builder() -> DeleteProjectWikiPageBuilder<'a> {
314 DeleteProjectWikiPageBuilder::default()
315 }
316}
317
318impl<'a> Endpoint for DeleteProjectWikiPage<'a> {
319 fn method(&self) -> Method {
320 Method::DELETE
321 }
322
323 fn endpoint(&self) -> Cow<'static, str> {
324 format!(
325 "projects/{}/wiki/{}.json",
326 &self.project_id_or_name, &self.title
327 )
328 .into()
329 }
330}
331
332#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
334pub struct WikiPagesWrapper<T> {
335 pub wiki_pages: Vec<T>,
337}
338
339#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
342pub struct WikiPageWrapper<T> {
343 pub wiki_page: T,
345}
346
347#[cfg(test)]
348pub(crate) mod test {
349 use crate::api::projects::{ListProjects, Project, ProjectsInclude, test::PROJECT_LOCK};
350
351 use super::*;
352 use std::error::Error;
353 use tokio::sync::RwLock;
354 use tracing_test::traced_test;
355
356 pub static PROJECT_WIKI_PAGE_LOCK: RwLock<()> = RwLock::const_new(());
359
360 #[traced_test]
361 #[test]
362 fn test_list_project_wiki_pages() -> Result<(), Box<dyn Error>> {
363 let _r_project = PROJECT_LOCK.blocking_read();
364 let _r_project_wiki_pages = PROJECT_WIKI_PAGE_LOCK.blocking_read();
365 dotenvy::dotenv()?;
366 let redmine = crate::api::Redmine::from_env(
367 reqwest::blocking::Client::builder()
368 .use_rustls_tls()
369 .build()?,
370 )?;
371 let endpoint = ListProjectWikiPages::builder()
372 .project_id_or_name("25")
373 .build()?;
374 redmine.json_response_body::<_, WikiPagesWrapper<WikiPageEssentials>>(&endpoint)?;
375 Ok(())
376 }
377
378 #[traced_test]
383 #[test]
384 fn test_completeness_wiki_page_essentials() -> Result<(), Box<dyn Error>> {
385 let _r_project = PROJECT_LOCK.blocking_read();
386 let _r_issues = PROJECT_WIKI_PAGE_LOCK.blocking_read();
387 dotenvy::dotenv()?;
388 let redmine = crate::api::Redmine::from_env(
389 reqwest::blocking::Client::builder()
390 .use_rustls_tls()
391 .build()?,
392 )?;
393 let endpoint = ListProjects::builder()
394 .include(vec![ProjectsInclude::EnabledModules])
395 .build()?;
396 let projects = redmine.json_response_body_all_pages_iter::<_, Project>(&endpoint);
397 let mut checked_projects = 0;
398 for project in projects {
399 let project = project?;
400 if !project
401 .enabled_modules
402 .is_some_and(|em| em.iter().any(|m| m.name == "wiki"))
403 {
404 continue;
406 }
407 let endpoint = ListProjectWikiPages::builder()
408 .project_id_or_name(project.id.to_string())
409 .include(vec![WikiPageInclude::Attachments])
410 .build()?;
411 let Ok(WikiPagesWrapper { wiki_pages: values }) =
412 redmine.json_response_body::<_, WikiPagesWrapper<serde_json::Value>>(&endpoint)
413 else {
414 continue;
418 };
419 checked_projects += 1;
420 for value in values {
421 let o: WikiPageEssentials = serde_json::from_value(value.clone())?;
422 let reserialized = serde_json::to_value(o)?;
423 assert_eq!(value, reserialized);
424 }
425 }
426 assert!(checked_projects > 0);
427 Ok(())
428 }
429
430 #[traced_test]
431 #[test]
432 fn test_get_project_wiki_page() -> Result<(), Box<dyn Error>> {
433 let _r_project = PROJECT_LOCK.blocking_read();
434 let _r_project_wiki_pages = PROJECT_WIKI_PAGE_LOCK.blocking_read();
435 dotenvy::dotenv()?;
436 let redmine = crate::api::Redmine::from_env(
437 reqwest::blocking::Client::builder()
438 .use_rustls_tls()
439 .build()?,
440 )?;
441 let endpoint = GetProjectWikiPage::builder()
442 .project_id_or_name("25")
443 .title("Administration")
444 .build()?;
445 redmine.json_response_body::<_, WikiPageWrapper<WikiPage>>(&endpoint)?;
446 Ok(())
447 }
448
449 #[traced_test]
454 #[test]
455 fn test_completeness_wiki_page() -> Result<(), Box<dyn Error>> {
456 let _r_project = PROJECT_LOCK.blocking_read();
457 let _r_issues = PROJECT_WIKI_PAGE_LOCK.blocking_read();
458 dotenvy::dotenv()?;
459 let redmine = crate::api::Redmine::from_env(
460 reqwest::blocking::Client::builder()
461 .use_rustls_tls()
462 .build()?,
463 )?;
464 let endpoint = ListProjects::builder()
465 .include(vec![ProjectsInclude::EnabledModules])
466 .build()?;
467 let projects = redmine.json_response_body_all_pages_iter::<_, Project>(&endpoint);
468 let mut checked_pages = 0;
469 for project in projects {
470 let project = project?;
471 if !project
472 .enabled_modules
473 .is_some_and(|em| em.iter().any(|m| m.name == "wiki"))
474 {
475 continue;
477 }
478 let endpoint = ListProjectWikiPages::builder()
479 .project_id_or_name(project.id.to_string())
480 .include(vec![WikiPageInclude::Attachments])
481 .build()?;
482 let Ok(WikiPagesWrapper { wiki_pages }) =
483 redmine.json_response_body::<_, WikiPagesWrapper<WikiPageEssentials>>(&endpoint)
484 else {
485 continue;
489 };
490 checked_pages += 1;
491 for wiki_page in wiki_pages {
492 let endpoint = GetProjectWikiPage::builder()
493 .project_id_or_name(project.id.to_string())
494 .title(wiki_page.title)
495 .include(vec![WikiPageInclude::Attachments])
496 .build()?;
497 let WikiPageWrapper { wiki_page: value } = redmine
498 .json_response_body::<_, WikiPageWrapper<serde_json::Value>>(&endpoint)?;
499 let o: WikiPage = serde_json::from_value(value.clone())?;
500 let reserialized = serde_json::to_value(o)?;
501 assert_eq!(value, reserialized);
502 }
503 }
504 assert!(checked_pages > 0);
505 Ok(())
506 }
507
508 #[traced_test]
509 #[test]
510 fn test_get_project_wiki_page_version() -> Result<(), Box<dyn Error>> {
511 let _r_project = PROJECT_LOCK.blocking_read();
512 let _r_project_wiki_pages = PROJECT_WIKI_PAGE_LOCK.blocking_read();
513 dotenvy::dotenv()?;
514 let redmine = crate::api::Redmine::from_env(
515 reqwest::blocking::Client::builder()
516 .use_rustls_tls()
517 .build()?,
518 )?;
519 let endpoint = GetProjectWikiPageVersion::builder()
520 .project_id_or_name("25")
521 .title("Administration")
522 .version(18)
523 .build()?;
524 redmine.json_response_body::<_, WikiPageWrapper<WikiPage>>(&endpoint)?;
525 Ok(())
526 }
527
528 #[traced_test]
529 #[test]
530 fn test_create_update_and_delete_project_wiki_page() -> Result<(), Box<dyn Error>> {
531 let _r_project = PROJECT_LOCK.blocking_read();
532 let _w_project_wiki_pages = PROJECT_WIKI_PAGE_LOCK.blocking_write();
533 dotenvy::dotenv()?;
534 let redmine = crate::api::Redmine::from_env(
535 reqwest::blocking::Client::builder()
536 .use_rustls_tls()
537 .build()?,
538 )?;
539 let endpoint = GetProjectWikiPage::builder()
540 .project_id_or_name("25")
541 .title("CreateWikiPageTest")
542 .build()?;
543 if redmine.ignore_response_body(&endpoint).is_ok() {
544 let endpoint = DeleteProjectWikiPage::builder()
546 .project_id_or_name("25")
547 .title("CreateWikiPageTest")
548 .build()?;
549 redmine.ignore_response_body(&endpoint)?;
550 }
551 let endpoint = CreateOrUpdateProjectWikiPage::builder()
552 .project_id_or_name("25")
553 .title("CreateWikiPageTest")
554 .text("Test Content")
555 .comments("Create Page Test")
556 .build()?;
557 redmine.ignore_response_body(&endpoint)?;
558 let endpoint = CreateOrUpdateProjectWikiPage::builder()
559 .project_id_or_name("25")
560 .title("CreateWikiPageTest")
561 .text("Test Content Updates")
562 .version(1)
563 .comments("Update Page Test")
564 .build()?;
565 redmine.ignore_response_body(&endpoint)?;
566 let endpoint = DeleteProjectWikiPage::builder()
567 .project_id_or_name("25")
568 .title("CreateWikiPageTest")
569 .build()?;
570 redmine.ignore_response_body(&endpoint)?;
571 Ok(())
572 }
573}