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, NoPagination, 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<'_> {}
266impl NoPagination for GetProject<'_> {}
267
268impl<'a> GetProject<'a> {
269 #[must_use]
271 pub fn builder() -> GetProjectBuilder<'a> {
272 GetProjectBuilder::default()
273 }
274}
275
276impl Endpoint for GetProject<'_> {
277 fn method(&self) -> Method {
278 Method::GET
279 }
280
281 fn endpoint(&self) -> Cow<'static, str> {
282 format!("projects/{}.json", &self.project_id_or_name).into()
283 }
284
285 fn parameters(&self) -> QueryParams {
286 let mut params = QueryParams::default();
287 params.push_opt("include", self.include.as_ref());
288 params
289 }
290}
291
292#[derive(Debug, Clone, Builder)]
294#[builder(setter(strip_option))]
295pub struct ArchiveProject<'a> {
296 #[builder(setter(into))]
298 project_id_or_name: Cow<'a, str>,
299}
300
301impl<'a> ArchiveProject<'a> {
302 #[must_use]
304 pub fn builder() -> ArchiveProjectBuilder<'a> {
305 ArchiveProjectBuilder::default()
306 }
307}
308
309impl Endpoint for ArchiveProject<'_> {
310 fn method(&self) -> Method {
311 Method::PUT
312 }
313
314 fn endpoint(&self) -> Cow<'static, str> {
315 format!("projects/{}/archive.json", &self.project_id_or_name).into()
316 }
317}
318
319#[derive(Debug, Clone, Builder)]
321#[builder(setter(strip_option))]
322pub struct UnarchiveProject<'a> {
323 #[builder(setter(into))]
325 project_id_or_name: Cow<'a, str>,
326}
327
328impl<'a> UnarchiveProject<'a> {
329 #[must_use]
331 pub fn builder() -> UnarchiveProjectBuilder<'a> {
332 UnarchiveProjectBuilder::default()
333 }
334}
335
336impl Endpoint for UnarchiveProject<'_> {
337 fn method(&self) -> Method {
338 Method::PUT
339 }
340
341 fn endpoint(&self) -> Cow<'static, str> {
342 format!("projects/{}/unarchive.json", &self.project_id_or_name).into()
343 }
344}
345
346#[serde_with::skip_serializing_none]
348#[derive(Debug, Clone, Builder, Serialize)]
349#[builder(setter(strip_option))]
350pub struct CreateProject<'a> {
351 #[builder(setter(into))]
353 name: Cow<'a, str>,
354 #[builder(setter(into))]
356 identifier: Cow<'a, str>,
357 #[builder(setter(into), default)]
359 description: Option<Cow<'a, str>>,
360 #[builder(setter(into), default)]
362 homepage: Option<Cow<'a, str>>,
363 #[builder(default)]
365 is_public: Option<bool>,
366 #[builder(default)]
368 parent_id: Option<u64>,
369 #[builder(default)]
371 inherit_members: Option<bool>,
372 #[builder(default)]
374 default_assigned_to_id: Option<u64>,
375 #[builder(default)]
377 default_version_id: Option<u64>,
378 #[builder(default)]
380 tracker_ids: Option<Vec<u64>>,
381 #[builder(default)]
383 enabled_module_names: Option<Vec<Cow<'a, str>>>,
384 #[builder(default)]
386 issue_custom_field_id: Option<Vec<u64>>,
387 #[builder(default)]
389 custom_field_values: Option<HashMap<u64, Cow<'a, str>>>,
390}
391
392impl ReturnsJsonResponse for CreateProject<'_> {}
393impl NoPagination for CreateProject<'_> {}
394
395impl<'a> CreateProject<'a> {
396 #[must_use]
398 pub fn builder() -> CreateProjectBuilder<'a> {
399 CreateProjectBuilder::default()
400 }
401}
402
403impl Endpoint for CreateProject<'_> {
404 fn method(&self) -> Method {
405 Method::POST
406 }
407
408 fn endpoint(&self) -> Cow<'static, str> {
409 "projects.json".into()
410 }
411
412 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
413 Ok(Some((
414 "application/json",
415 serde_json::to_vec(&ProjectWrapper::<CreateProject> {
416 project: (*self).to_owned(),
417 })?,
418 )))
419 }
420}
421
422#[serde_with::skip_serializing_none]
424#[derive(Debug, Clone, Builder, Serialize)]
425#[builder(setter(strip_option))]
426pub struct UpdateProject<'a> {
427 #[serde(skip_serializing)]
429 #[builder(setter(into))]
430 project_id_or_name: Cow<'a, str>,
431 #[builder(setter(into), default)]
433 name: Option<Cow<'a, str>>,
434 #[builder(setter(into), default)]
436 identifier: Option<Cow<'a, str>>,
437 #[builder(setter(into), default)]
439 description: Option<Cow<'a, str>>,
440 #[builder(setter(into), default)]
442 homepage: Option<Cow<'a, str>>,
443 #[builder(default)]
445 is_public: Option<bool>,
446 #[builder(default)]
448 parent_id: Option<u64>,
449 #[builder(default)]
451 inherit_members: Option<bool>,
452 #[builder(default)]
454 default_assigned_to_id: Option<u64>,
455 #[builder(default)]
457 default_version_id: Option<u64>,
458 #[builder(default)]
460 tracker_ids: Option<Vec<u64>>,
461 #[builder(default)]
463 enabled_module_names: Option<Vec<Cow<'a, str>>>,
464 #[builder(default)]
466 issue_custom_field_id: Option<Vec<u64>>,
467 #[builder(default)]
469 custom_field_values: Option<HashMap<u64, Cow<'a, str>>>,
470}
471
472impl<'a> UpdateProject<'a> {
473 #[must_use]
475 pub fn builder() -> UpdateProjectBuilder<'a> {
476 UpdateProjectBuilder::default()
477 }
478}
479
480impl Endpoint for UpdateProject<'_> {
481 fn method(&self) -> Method {
482 Method::PUT
483 }
484
485 fn endpoint(&self) -> Cow<'static, str> {
486 format!("projects/{}.json", self.project_id_or_name).into()
487 }
488
489 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
490 Ok(Some((
491 "application/json",
492 serde_json::to_vec(&ProjectWrapper::<UpdateProject> {
493 project: (*self).to_owned(),
494 })?,
495 )))
496 }
497}
498
499#[derive(Debug, Clone, Builder)]
501#[builder(setter(strip_option))]
502pub struct DeleteProject<'a> {
503 #[builder(setter(into))]
505 project_id_or_name: Cow<'a, str>,
506}
507
508impl<'a> DeleteProject<'a> {
509 #[must_use]
511 pub fn builder() -> DeleteProjectBuilder<'a> {
512 DeleteProjectBuilder::default()
513 }
514}
515
516impl Endpoint for DeleteProject<'_> {
517 fn method(&self) -> Method {
518 Method::DELETE
519 }
520
521 fn endpoint(&self) -> Cow<'static, str> {
522 format!("projects/{}.json", &self.project_id_or_name).into()
523 }
524}
525
526#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
528pub struct ProjectsWrapper<T> {
529 pub projects: Vec<T>,
531}
532
533#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
536pub struct ProjectWrapper<T> {
537 pub project: T,
539}
540
541#[cfg(test)]
542pub(crate) mod test {
543 use super::*;
544 use crate::api::test_helpers::with_project;
545 use pretty_assertions::assert_eq;
546 use std::error::Error;
547 use tokio::sync::RwLock;
548 use tracing_test::traced_test;
549
550 pub static PROJECT_LOCK: RwLock<()> = RwLock::const_new(());
553
554 #[traced_test]
555 #[test]
556 fn test_list_projects_first_page() -> Result<(), Box<dyn Error>> {
557 let _r_project = PROJECT_LOCK.read();
558 dotenvy::dotenv()?;
559 let redmine = crate::api::Redmine::from_env()?;
560 let endpoint = ListProjects::builder().build()?;
561 redmine.json_response_body_page::<_, Project>(&endpoint, 0, 25)?;
562 Ok(())
563 }
564
565 #[traced_test]
566 #[test]
567 fn test_list_projects_all_pages() -> Result<(), Box<dyn Error>> {
568 let _r_project = PROJECT_LOCK.read();
569 dotenvy::dotenv()?;
570 let redmine = crate::api::Redmine::from_env()?;
571 let endpoint = ListProjects::builder().build()?;
572 redmine.json_response_body_all_pages::<_, Project>(&endpoint)?;
573 Ok(())
574 }
575
576 #[traced_test]
577 #[tokio::test]
578 async fn test_list_projects_async_first_page() -> Result<(), Box<dyn Error>> {
579 let _r_project = PROJECT_LOCK.read();
580 dotenvy::dotenv()?;
581 let redmine = crate::api::RedmineAsync::from_env()?;
582 let endpoint = ListProjects::builder().build()?;
583 redmine
584 .json_response_body_page::<_, Project>(&endpoint, 0, 25)
585 .await?;
586 Ok(())
587 }
588
589 #[traced_test]
590 #[tokio::test]
591 async fn test_list_projects_async_all_pages() -> Result<(), Box<dyn Error>> {
592 let _r_project = PROJECT_LOCK.read();
593 dotenvy::dotenv()?;
594 let redmine = crate::api::RedmineAsync::from_env()?;
595 let endpoint = ListProjects::builder().build()?;
596 redmine
597 .json_response_body_all_pages::<_, Project>(&endpoint)
598 .await?;
599 Ok(())
600 }
601
602 #[traced_test]
603 #[test]
604 fn test_get_project() -> Result<(), Box<dyn Error>> {
605 let _r_project = PROJECT_LOCK.read();
606 dotenvy::dotenv()?;
607 let redmine = crate::api::Redmine::from_env()?;
608 let endpoint = GetProject::builder()
609 .project_id_or_name("sandbox")
610 .build()?;
611 redmine.json_response_body::<_, ProjectWrapper<Project>>(&endpoint)?;
612 Ok(())
613 }
614
615 #[function_name::named]
616 #[traced_test]
617 #[test]
618 fn test_create_project() -> Result<(), Box<dyn Error>> {
619 let name = format!("unittest_{}", function_name!());
620 with_project(&name, |_, _, _| Ok(()))?;
621 Ok(())
622 }
623
624 #[function_name::named]
625 #[traced_test]
626 #[test]
627 fn test_update_project() -> Result<(), Box<dyn Error>> {
628 let name = format!("unittest_{}", function_name!());
629 with_project(&name, |redmine, _id, name| {
630 let update_endpoint = super::UpdateProject::builder()
631 .project_id_or_name(name)
632 .description("Test-Description")
633 .build()?;
634 redmine.ignore_response_body::<_>(&update_endpoint)?;
635 Ok(())
636 })?;
637 Ok(())
638 }
639
640 #[traced_test]
645 #[test]
646 fn test_completeness_project_type() -> Result<(), Box<dyn Error>> {
647 let _r_project = PROJECT_LOCK.read();
648 dotenvy::dotenv()?;
649 let redmine = crate::api::Redmine::from_env()?;
650 let endpoint = ListProjects::builder().build()?;
651 let values: Vec<serde_json::Value> = redmine.json_response_body_all_pages(&endpoint)?;
652 for value in values {
653 let o: Project = serde_json::from_value(value.clone())?;
654 let reserialized = serde_json::to_value(o)?;
655 assert_eq!(value, reserialized);
656 }
657 Ok(())
658 }
659
660 #[traced_test]
670 #[test]
671 fn test_completeness_project_type_all_pages_all_project_details() -> Result<(), Box<dyn Error>>
672 {
673 let _r_project = PROJECT_LOCK.read();
674 dotenvy::dotenv()?;
675 let redmine = crate::api::Redmine::from_env()?;
676 let endpoint = ListProjects::builder()
677 .include(vec![
678 ProjectsInclude::Trackers,
679 ProjectsInclude::IssueCategories,
680 ProjectsInclude::EnabledModules,
681 ProjectsInclude::TimeEntryActivities,
682 ProjectsInclude::IssueCustomFields,
683 ])
684 .build()?;
685 let projects = redmine.json_response_body_all_pages::<_, Project>(&endpoint)?;
686 for project in projects {
687 let get_endpoint = GetProject::builder()
688 .project_id_or_name(project.id.to_string())
689 .include(vec![
690 ProjectInclude::Trackers,
691 ProjectInclude::IssueCategories,
692 ProjectInclude::EnabledModules,
693 ProjectInclude::TimeEntryActivities,
694 ProjectInclude::IssueCustomFields,
695 ])
696 .build()?;
697 let ProjectWrapper { project: value } = redmine
698 .json_response_body::<_, ProjectWrapper<serde_json::Value>>(&get_endpoint)?;
699 let o: Project = serde_json::from_value(value.clone())?;
700 let reserialized = serde_json::to_value(o)?;
701 assert_eq!(value, reserialized);
702 }
703 Ok(())
704 }
705}