1use derive_builder::Builder;
14use reqwest::Method;
15use std::borrow::Cow;
16
17use crate::api::custom_fields::CustomFieldName;
18use crate::api::enumerations::TimeEntryActivityEssentials;
19use crate::api::issue_categories::IssueCategoryEssentials;
20use crate::api::issues::AssigneeEssentials;
21use crate::api::trackers::TrackerEssentials;
22use crate::api::versions::VersionEssentials;
23use crate::api::{Endpoint, Pageable, QueryParams, ReturnsJsonResponse};
24use serde::Serialize;
25use std::collections::HashMap;
26
27#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
29pub struct Module {
30 pub id: u64,
32 pub name: String,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
39pub struct ProjectEssentials {
40 pub id: u64,
42 pub name: String,
44}
45
46impl From<Project> for ProjectEssentials {
47 fn from(v: Project) -> Self {
48 ProjectEssentials {
49 id: v.id,
50 name: v.name,
51 }
52 }
53}
54
55impl From<&Project> for ProjectEssentials {
56 fn from(v: &Project) -> Self {
57 ProjectEssentials {
58 id: v.id,
59 name: v.name.to_owned(),
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
68pub struct Project {
69 pub id: u64,
71 pub name: String,
73 pub identifier: String,
75 pub description: Option<String>,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub homepage: Option<String>,
80 pub is_public: Option<bool>,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub parent: Option<ProjectEssentials>,
85 pub inherit_members: Option<bool>,
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub default_assignee: Option<AssigneeEssentials>,
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub default_version: Option<VersionEssentials>,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub default_version_id: Option<u64>,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub tracker_ids: Option<Vec<u64>>,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub enabled_module_names: Option<Vec<String>>,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub issue_custom_field_id: Option<Vec<u64>>,
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub custom_field_values: Option<HashMap<u64, String>>,
108 pub status: u64,
110 #[serde(
112 serialize_with = "crate::api::serialize_rfc3339",
113 deserialize_with = "crate::api::deserialize_rfc3339"
114 )]
115 pub created_on: time::OffsetDateTime,
116 #[serde(
118 serialize_with = "crate::api::serialize_rfc3339",
119 deserialize_with = "crate::api::deserialize_rfc3339"
120 )]
121 pub updated_on: time::OffsetDateTime,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub issue_categories: Option<Vec<IssueCategoryEssentials>>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub time_entry_activities: Option<Vec<TimeEntryActivityEssentials>>,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub enabled_modules: Option<Vec<Module>>,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub trackers: Option<Vec<TrackerEssentials>>,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub issue_custom_fields: Option<Vec<CustomFieldName>>,
137}
138
139#[derive(Debug, Clone)]
141pub enum ProjectsInclude {
142 Trackers,
144 IssueCategories,
146 EnabledModules,
148 TimeEntryActivities,
150 IssueCustomFields,
152}
153
154impl std::fmt::Display for ProjectsInclude {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 match self {
157 Self::Trackers => {
158 write!(f, "trackers")
159 }
160 Self::IssueCategories => {
161 write!(f, "issue_categories")
162 }
163 Self::EnabledModules => {
164 write!(f, "enabled_modules")
165 }
166 Self::TimeEntryActivities => {
167 write!(f, "time_entry_activities")
168 }
169 Self::IssueCustomFields => {
170 write!(f, "issue_custom_fields")
171 }
172 }
173 }
174}
175
176#[derive(Debug, Clone, Builder)]
178#[builder(setter(strip_option))]
179pub struct ListProjects {
180 #[builder(default)]
182 include: Option<Vec<ProjectsInclude>>,
183}
184
185impl ReturnsJsonResponse for ListProjects {}
186impl Pageable for ListProjects {
187 fn response_wrapper_key(&self) -> String {
188 "projects".to_string()
189 }
190}
191
192impl ListProjects {
193 #[must_use]
195 pub fn builder() -> ListProjectsBuilder {
196 ListProjectsBuilder::default()
197 }
198}
199
200impl Endpoint for ListProjects {
201 fn method(&self) -> Method {
202 Method::GET
203 }
204
205 fn endpoint(&self) -> Cow<'static, str> {
206 "projects.json".into()
207 }
208
209 fn parameters(&self) -> QueryParams {
210 let mut params = QueryParams::default();
211 params.push_opt("include", self.include.as_ref());
212 params
213 }
214}
215
216#[derive(Debug, Clone)]
218pub enum ProjectInclude {
219 Trackers,
221 IssueCategories,
223 EnabledModules,
225 TimeEntryActivities,
227 IssueCustomFields,
229}
230
231impl std::fmt::Display for ProjectInclude {
232 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233 match self {
234 Self::Trackers => {
235 write!(f, "trackers")
236 }
237 Self::IssueCategories => {
238 write!(f, "issue_categories")
239 }
240 Self::EnabledModules => {
241 write!(f, "enabled_modules")
242 }
243 Self::TimeEntryActivities => {
244 write!(f, "time_entry_activities")
245 }
246 Self::IssueCustomFields => {
247 write!(f, "issue_custom_fields")
248 }
249 }
250 }
251}
252
253#[derive(Debug, Clone, Builder)]
255#[builder(setter(strip_option))]
256pub struct GetProject<'a> {
257 #[builder(setter(into))]
259 project_id_or_name: Cow<'a, str>,
260 #[builder(default)]
262 include: Option<Vec<ProjectInclude>>,
263}
264
265impl ReturnsJsonResponse for GetProject<'_> {}
266
267impl<'a> GetProject<'a> {
268 #[must_use]
270 pub fn builder() -> GetProjectBuilder<'a> {
271 GetProjectBuilder::default()
272 }
273}
274
275impl Endpoint for GetProject<'_> {
276 fn method(&self) -> Method {
277 Method::GET
278 }
279
280 fn endpoint(&self) -> Cow<'static, str> {
281 format!("projects/{}.json", &self.project_id_or_name).into()
282 }
283
284 fn parameters(&self) -> QueryParams {
285 let mut params = QueryParams::default();
286 params.push_opt("include", self.include.as_ref());
287 params
288 }
289}
290
291#[derive(Debug, Clone, Builder)]
293#[builder(setter(strip_option))]
294pub struct ArchiveProject<'a> {
295 #[builder(setter(into))]
297 project_id_or_name: Cow<'a, str>,
298}
299
300impl<'a> ArchiveProject<'a> {
301 #[must_use]
303 pub fn builder() -> ArchiveProjectBuilder<'a> {
304 ArchiveProjectBuilder::default()
305 }
306}
307
308impl Endpoint for ArchiveProject<'_> {
309 fn method(&self) -> Method {
310 Method::PUT
311 }
312
313 fn endpoint(&self) -> Cow<'static, str> {
314 format!("projects/{}/archive.json", &self.project_id_or_name).into()
315 }
316}
317
318#[derive(Debug, Clone, Builder)]
320#[builder(setter(strip_option))]
321pub struct UnarchiveProject<'a> {
322 #[builder(setter(into))]
324 project_id_or_name: Cow<'a, str>,
325}
326
327impl<'a> UnarchiveProject<'a> {
328 #[must_use]
330 pub fn builder() -> UnarchiveProjectBuilder<'a> {
331 UnarchiveProjectBuilder::default()
332 }
333}
334
335impl Endpoint for UnarchiveProject<'_> {
336 fn method(&self) -> Method {
337 Method::PUT
338 }
339
340 fn endpoint(&self) -> Cow<'static, str> {
341 format!("projects/{}/unarchive.json", &self.project_id_or_name).into()
342 }
343}
344
345#[serde_with::skip_serializing_none]
347#[derive(Debug, Clone, Builder, Serialize)]
348#[builder(setter(strip_option))]
349pub struct CreateProject<'a> {
350 #[builder(setter(into))]
352 name: Cow<'a, str>,
353 #[builder(setter(into))]
355 identifier: Cow<'a, str>,
356 #[builder(setter(into), default)]
358 description: Option<Cow<'a, str>>,
359 #[builder(setter(into), default)]
361 homepage: Option<Cow<'a, str>>,
362 #[builder(default)]
364 is_public: Option<bool>,
365 #[builder(default)]
367 parent_id: Option<u64>,
368 #[builder(default)]
370 inherit_members: Option<bool>,
371 #[builder(default)]
373 default_assigned_to_id: Option<u64>,
374 #[builder(default)]
376 default_version_id: Option<u64>,
377 #[builder(default)]
379 tracker_ids: Option<Vec<u64>>,
380 #[builder(default)]
382 enabled_module_names: Option<Vec<Cow<'a, str>>>,
383 #[builder(default)]
385 issue_custom_field_id: Option<Vec<u64>>,
386 #[builder(default)]
388 custom_field_values: Option<HashMap<u64, Cow<'a, str>>>,
389}
390
391impl ReturnsJsonResponse for CreateProject<'_> {}
392
393impl<'a> CreateProject<'a> {
394 #[must_use]
396 pub fn builder() -> CreateProjectBuilder<'a> {
397 CreateProjectBuilder::default()
398 }
399}
400
401impl Endpoint for CreateProject<'_> {
402 fn method(&self) -> Method {
403 Method::POST
404 }
405
406 fn endpoint(&self) -> Cow<'static, str> {
407 "projects.json".into()
408 }
409
410 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
411 Ok(Some((
412 "application/json",
413 serde_json::to_vec(&ProjectWrapper::<CreateProject> {
414 project: (*self).to_owned(),
415 })?,
416 )))
417 }
418}
419
420#[serde_with::skip_serializing_none]
422#[derive(Debug, Clone, Builder, Serialize)]
423#[builder(setter(strip_option))]
424pub struct UpdateProject<'a> {
425 #[serde(skip_serializing)]
427 #[builder(setter(into))]
428 project_id_or_name: Cow<'a, str>,
429 #[builder(setter(into), default)]
431 name: Option<Cow<'a, str>>,
432 #[builder(setter(into), default)]
434 identifier: Option<Cow<'a, str>>,
435 #[builder(setter(into), default)]
437 description: Option<Cow<'a, str>>,
438 #[builder(setter(into), default)]
440 homepage: Option<Cow<'a, str>>,
441 #[builder(default)]
443 is_public: Option<bool>,
444 #[builder(default)]
446 parent_id: Option<u64>,
447 #[builder(default)]
449 inherit_members: Option<bool>,
450 #[builder(default)]
452 default_assigned_to_id: Option<u64>,
453 #[builder(default)]
455 default_version_id: Option<u64>,
456 #[builder(default)]
458 tracker_ids: Option<Vec<u64>>,
459 #[builder(default)]
461 enabled_module_names: Option<Vec<Cow<'a, str>>>,
462 #[builder(default)]
464 issue_custom_field_id: Option<Vec<u64>>,
465 #[builder(default)]
467 custom_field_values: Option<HashMap<u64, Cow<'a, str>>>,
468}
469
470impl<'a> UpdateProject<'a> {
471 #[must_use]
473 pub fn builder() -> UpdateProjectBuilder<'a> {
474 UpdateProjectBuilder::default()
475 }
476}
477
478impl Endpoint for UpdateProject<'_> {
479 fn method(&self) -> Method {
480 Method::PUT
481 }
482
483 fn endpoint(&self) -> Cow<'static, str> {
484 format!("projects/{}.json", self.project_id_or_name).into()
485 }
486
487 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
488 Ok(Some((
489 "application/json",
490 serde_json::to_vec(&ProjectWrapper::<UpdateProject> {
491 project: (*self).to_owned(),
492 })?,
493 )))
494 }
495}
496
497#[derive(Debug, Clone, Builder)]
499#[builder(setter(strip_option))]
500pub struct DeleteProject<'a> {
501 #[builder(setter(into))]
503 project_id_or_name: Cow<'a, str>,
504}
505
506impl<'a> DeleteProject<'a> {
507 #[must_use]
509 pub fn builder() -> DeleteProjectBuilder<'a> {
510 DeleteProjectBuilder::default()
511 }
512}
513
514impl Endpoint for DeleteProject<'_> {
515 fn method(&self) -> Method {
516 Method::DELETE
517 }
518
519 fn endpoint(&self) -> Cow<'static, str> {
520 format!("projects/{}.json", &self.project_id_or_name).into()
521 }
522}
523
524#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
526pub struct ProjectsWrapper<T> {
527 pub projects: Vec<T>,
529}
530
531#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
534pub struct ProjectWrapper<T> {
535 pub project: T,
537}
538
539#[cfg(test)]
540pub(crate) mod test {
541 use super::*;
542 use crate::api::test_helpers::with_project;
543 use pretty_assertions::assert_eq;
544 use std::error::Error;
545 use tokio::sync::RwLock;
546 use tracing_test::traced_test;
547
548 pub static PROJECT_LOCK: RwLock<()> = RwLock::const_new(());
551
552 #[traced_test]
553 #[test]
554 fn test_list_projects_no_pagination() -> Result<(), Box<dyn Error>> {
555 let _r_project = PROJECT_LOCK.read();
556 dotenvy::dotenv()?;
557 let redmine = crate::api::Redmine::from_env()?;
558 let endpoint = ListProjects::builder().build()?;
559 redmine.json_response_body::<_, ProjectsWrapper<Project>>(&endpoint)?;
560 Ok(())
561 }
562
563 #[traced_test]
564 #[test]
565 fn test_list_projects_first_page() -> Result<(), Box<dyn Error>> {
566 let _r_project = PROJECT_LOCK.read();
567 dotenvy::dotenv()?;
568 let redmine = crate::api::Redmine::from_env()?;
569 let endpoint = ListProjects::builder().build()?;
570 redmine.json_response_body_page::<_, Project>(&endpoint, 0, 25)?;
571 Ok(())
572 }
573
574 #[traced_test]
575 #[test]
576 fn test_list_projects_all_pages() -> Result<(), Box<dyn Error>> {
577 let _r_project = PROJECT_LOCK.read();
578 dotenvy::dotenv()?;
579 let redmine = crate::api::Redmine::from_env()?;
580 let endpoint = ListProjects::builder().build()?;
581 redmine.json_response_body_all_pages::<_, Project>(&endpoint)?;
582 Ok(())
583 }
584
585 #[traced_test]
586 #[tokio::test]
587 async fn test_list_projects_async_no_pagination() -> Result<(), Box<dyn Error>> {
588 let _r_project = PROJECT_LOCK.read();
589 dotenvy::dotenv()?;
590 let redmine = crate::api::RedmineAsync::from_env()?;
591 let endpoint = ListProjects::builder().build()?;
592 redmine
593 .json_response_body::<_, ProjectsWrapper<Project>>(&endpoint)
594 .await?;
595 Ok(())
596 }
597
598 #[traced_test]
599 #[tokio::test]
600 async fn test_list_projects_async_first_page() -> Result<(), Box<dyn Error>> {
601 let _r_project = PROJECT_LOCK.read();
602 dotenvy::dotenv()?;
603 let redmine = crate::api::RedmineAsync::from_env()?;
604 let endpoint = ListProjects::builder().build()?;
605 redmine
606 .json_response_body_page::<_, Project>(&endpoint, 0, 25)
607 .await?;
608 Ok(())
609 }
610
611 #[traced_test]
612 #[tokio::test]
613 async fn test_list_projects_async_all_pages() -> Result<(), Box<dyn Error>> {
614 let _r_project = PROJECT_LOCK.read();
615 dotenvy::dotenv()?;
616 let redmine = crate::api::RedmineAsync::from_env()?;
617 let endpoint = ListProjects::builder().build()?;
618 redmine
619 .json_response_body_all_pages::<_, Project>(&endpoint)
620 .await?;
621 Ok(())
622 }
623
624 #[traced_test]
625 #[test]
626 fn test_get_project() -> Result<(), Box<dyn Error>> {
627 let _r_project = PROJECT_LOCK.read();
628 dotenvy::dotenv()?;
629 let redmine = crate::api::Redmine::from_env()?;
630 let endpoint = GetProject::builder()
631 .project_id_or_name("sandbox")
632 .build()?;
633 redmine.json_response_body::<_, ProjectWrapper<Project>>(&endpoint)?;
634 Ok(())
635 }
636
637 #[function_name::named]
638 #[traced_test]
639 #[test]
640 fn test_create_project() -> Result<(), Box<dyn Error>> {
641 let name = format!("unittest_{}", function_name!());
642 with_project(&name, |_, _, _| Ok(()))?;
643 Ok(())
644 }
645
646 #[function_name::named]
647 #[traced_test]
648 #[test]
649 fn test_update_project() -> Result<(), Box<dyn Error>> {
650 let name = format!("unittest_{}", function_name!());
651 with_project(&name, |redmine, _id, name| {
652 let update_endpoint = super::UpdateProject::builder()
653 .project_id_or_name(name)
654 .description("Test-Description")
655 .build()?;
656 redmine.ignore_response_body::<_>(&update_endpoint)?;
657 Ok(())
658 })?;
659 Ok(())
660 }
661
662 #[traced_test]
667 #[test]
668 fn test_completeness_project_type() -> Result<(), Box<dyn Error>> {
669 let _r_project = PROJECT_LOCK.read();
670 dotenvy::dotenv()?;
671 let redmine = crate::api::Redmine::from_env()?;
672 let endpoint = ListProjects::builder().build()?;
673 let ProjectsWrapper { projects: values } =
674 redmine.json_response_body::<_, ProjectsWrapper<serde_json::Value>>(&endpoint)?;
675 for value in values {
676 let o: Project = serde_json::from_value(value.clone())?;
677 let reserialized = serde_json::to_value(o)?;
678 assert_eq!(value, reserialized);
679 }
680 Ok(())
681 }
682
683 #[traced_test]
693 #[test]
694 fn test_completeness_project_type_all_pages_all_project_details() -> Result<(), Box<dyn Error>>
695 {
696 let _r_project = PROJECT_LOCK.read();
697 dotenvy::dotenv()?;
698 let redmine = crate::api::Redmine::from_env()?;
699 let endpoint = ListProjects::builder()
700 .include(vec![
701 ProjectsInclude::Trackers,
702 ProjectsInclude::IssueCategories,
703 ProjectsInclude::EnabledModules,
704 ProjectsInclude::TimeEntryActivities,
705 ProjectsInclude::IssueCustomFields,
706 ])
707 .build()?;
708 let projects = redmine.json_response_body_all_pages::<_, Project>(&endpoint)?;
709 for project in projects {
710 let get_endpoint = GetProject::builder()
711 .project_id_or_name(project.id.to_string())
712 .include(vec![
713 ProjectInclude::Trackers,
714 ProjectInclude::IssueCategories,
715 ProjectInclude::EnabledModules,
716 ProjectInclude::TimeEntryActivities,
717 ProjectInclude::IssueCustomFields,
718 ])
719 .build()?;
720 let ProjectWrapper { project: value } = redmine
721 .json_response_body::<_, ProjectWrapper<serde_json::Value>>(&get_endpoint)?;
722 let o: Project = serde_json::from_value(value.clone())?;
723 let reserialized = serde_json::to_value(o)?;
724 assert_eq!(value, reserialized);
725 }
726 Ok(())
727 }
728}