1use derive_builder::Builder;
91use reqwest::Method;
92use std::borrow::Cow;
93
94use crate::api::attachments::Attachment;
95use crate::api::custom_fields::CustomFieldEssentialsWithValue;
96use crate::api::enumerations::IssuePriorityEssentials;
97use crate::api::groups::{Group, GroupEssentials};
98use crate::api::issue_categories::IssueCategoryEssentials;
99use crate::api::issue_relations::IssueRelation;
100use crate::api::issue_statuses::IssueStatusEssentials;
101use crate::api::projects::ProjectEssentials;
102use crate::api::trackers::TrackerEssentials;
103use crate::api::users::UserEssentials;
104use crate::api::versions::VersionEssentials;
105use crate::api::{Endpoint, NoPagination, Pageable, QueryParams, ReturnsJsonResponse};
106use serde::Serialize;
107
108#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
111pub struct AssigneeEssentials {
112 pub id: u64,
114 pub name: String,
116}
117
118impl From<UserEssentials> for AssigneeEssentials {
119 fn from(v: UserEssentials) -> Self {
120 AssigneeEssentials {
121 id: v.id,
122 name: v.name,
123 }
124 }
125}
126
127impl From<&UserEssentials> for AssigneeEssentials {
128 fn from(v: &UserEssentials) -> Self {
129 AssigneeEssentials {
130 id: v.id,
131 name: v.name.to_owned(),
132 }
133 }
134}
135
136impl From<GroupEssentials> for AssigneeEssentials {
137 fn from(v: GroupEssentials) -> Self {
138 AssigneeEssentials {
139 id: v.id,
140 name: v.name,
141 }
142 }
143}
144
145impl From<&GroupEssentials> for AssigneeEssentials {
146 fn from(v: &GroupEssentials) -> Self {
147 AssigneeEssentials {
148 id: v.id,
149 name: v.name.to_owned(),
150 }
151 }
152}
153
154impl From<Group> for AssigneeEssentials {
155 fn from(v: Group) -> Self {
156 AssigneeEssentials {
157 id: v.id,
158 name: v.name,
159 }
160 }
161}
162
163impl From<&Group> for AssigneeEssentials {
164 fn from(v: &Group) -> Self {
165 AssigneeEssentials {
166 id: v.id,
167 name: v.name.to_owned(),
168 }
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
175pub struct IssueEssentials {
176 pub id: u64,
178}
179
180impl From<Issue> for IssueEssentials {
181 fn from(v: Issue) -> Self {
182 IssueEssentials { id: v.id }
183 }
184}
185
186impl From<&Issue> for IssueEssentials {
187 fn from(v: &Issue) -> Self {
188 IssueEssentials { id: v.id }
189 }
190}
191
192#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
194pub enum ChangePropertyType {
195 #[serde(rename = "attr")]
197 Attr,
198 #[serde(rename = "cf")]
200 Cf,
201 #[serde(rename = "relation")]
203 Relation,
204 #[serde(rename = "attachment")]
206 Attachment,
207}
208
209#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
211pub struct JournalChange {
212 pub name: String,
214 pub old_value: Option<String>,
216 pub new_value: Option<String>,
218 pub property: ChangePropertyType,
220}
221
222#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
224pub struct Journal {
225 pub id: u64,
227 pub user: UserEssentials,
229 pub notes: Option<String>,
231 pub private_notes: bool,
233 #[serde(
235 serialize_with = "crate::api::serialize_rfc3339",
236 deserialize_with = "crate::api::deserialize_rfc3339"
237 )]
238 pub created_on: time::OffsetDateTime,
239 #[serde(
241 serialize_with = "crate::api::serialize_rfc3339",
242 deserialize_with = "crate::api::deserialize_rfc3339"
243 )]
244 pub updated_on: time::OffsetDateTime,
245 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub updated_by: Option<UserEssentials>,
248 pub details: Vec<JournalChange>,
250}
251
252#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
254pub struct ChildIssue {
255 pub id: u64,
257 pub subject: String,
259 pub tracker: TrackerEssentials,
261 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub children: Option<Vec<ChildIssue>>,
264}
265
266#[derive(Debug, Clone, Serialize, serde::Deserialize)]
270pub struct Issue {
271 pub id: u64,
273 #[serde(skip_serializing_if = "Option::is_none")]
275 pub parent: Option<IssueEssentials>,
276 pub project: ProjectEssentials,
278 pub tracker: TrackerEssentials,
280 pub status: IssueStatusEssentials,
282 pub priority: IssuePriorityEssentials,
284 pub author: UserEssentials,
286 #[serde(skip_serializing_if = "Option::is_none")]
288 pub assigned_to: Option<AssigneeEssentials>,
289 #[serde(skip_serializing_if = "Option::is_none")]
291 pub category: Option<IssueCategoryEssentials>,
292 #[serde(rename = "fixed_version", skip_serializing_if = "Option::is_none")]
294 pub version: Option<VersionEssentials>,
295 #[serde(skip_serializing_if = "Option::is_none")]
297 pub subject: Option<String>,
298 pub description: Option<String>,
300 is_private: Option<bool>,
302 pub start_date: Option<time::Date>,
304 pub due_date: Option<time::Date>,
306 #[serde(
308 serialize_with = "crate::api::serialize_optional_rfc3339",
309 deserialize_with = "crate::api::deserialize_optional_rfc3339"
310 )]
311 pub closed_on: Option<time::OffsetDateTime>,
312 pub done_ratio: u64,
314 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub custom_fields: Option<Vec<CustomFieldEssentialsWithValue>>,
317 pub estimated_hours: Option<f64>,
319 #[serde(
321 serialize_with = "crate::api::serialize_rfc3339",
322 deserialize_with = "crate::api::deserialize_rfc3339"
323 )]
324 pub created_on: time::OffsetDateTime,
325 #[serde(
327 serialize_with = "crate::api::serialize_rfc3339",
328 deserialize_with = "crate::api::deserialize_rfc3339"
329 )]
330 pub updated_on: time::OffsetDateTime,
331 #[serde(default, skip_serializing_if = "Option::is_none")]
333 pub attachments: Option<Vec<Attachment>>,
334 #[serde(default, skip_serializing_if = "Option::is_none")]
336 pub relations: Option<Vec<IssueRelation>>,
337 #[serde(default, skip_serializing_if = "Option::is_none")]
339 pub journals: Option<Vec<Journal>>,
340 #[serde(default, skip_serializing_if = "Option::is_none")]
342 pub children: Option<Vec<ChildIssue>>,
343 #[serde(default, skip_serializing_if = "Option::is_none")]
345 pub watchers: Option<Vec<UserEssentials>>,
346 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub spent_hours: Option<f64>,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
351 pub total_spent_hours: Option<f64>,
352 #[serde(default, skip_serializing_if = "Option::is_none")]
354 pub total_estimated_hours: Option<f64>,
355}
356
357#[derive(Debug, Clone)]
359pub enum SubProjectFilter {
360 OnlyParentProject,
362 TheseSubProjects(Vec<u64>),
364 NotTheseSubProjects(Vec<u64>),
366}
367
368impl std::fmt::Display for SubProjectFilter {
369 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370 match self {
371 SubProjectFilter::OnlyParentProject => {
372 write!(f, "!*")
373 }
374 SubProjectFilter::TheseSubProjects(ids) => {
375 let s: String = ids
376 .iter()
377 .map(|e| e.to_string())
378 .collect::<Vec<_>>()
379 .join(",");
380 write!(f, "{s}")
381 }
382 SubProjectFilter::NotTheseSubProjects(ids) => {
383 let s: String = ids
384 .iter()
385 .map(|e| format!("!{e}"))
386 .collect::<Vec<_>>()
387 .join(",");
388 write!(f, "{s}")
389 }
390 }
391 }
392}
393
394#[derive(Debug, Clone)]
396pub enum StatusFilter {
397 Open,
399 Closed,
401 All,
403 TheseStatuses(Vec<u64>),
405 NotTheseStatuses(Vec<u64>),
407}
408
409impl std::fmt::Display for StatusFilter {
410 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411 match self {
412 StatusFilter::Open => {
413 write!(f, "open")
414 }
415 StatusFilter::Closed => {
416 write!(f, "closed")
417 }
418 StatusFilter::All => {
419 write!(f, "*")
420 }
421 StatusFilter::TheseStatuses(ids) => {
422 let s: String = ids
423 .iter()
424 .map(|e| e.to_string())
425 .collect::<Vec<_>>()
426 .join(",");
427 write!(f, "{s}")
428 }
429 StatusFilter::NotTheseStatuses(ids) => {
430 let s: String = ids
431 .iter()
432 .map(|e| format!("!{e}"))
433 .collect::<Vec<_>>()
434 .join(",");
435 write!(f, "{s}")
436 }
437 }
438 }
439}
440
441#[derive(Debug, Clone)]
443pub enum AuthorFilter {
444 AnyAuthor,
446 Me,
448 NotMe,
450 TheseAuthors(Vec<u64>),
452 NotTheseAuthors(Vec<u64>),
454}
455
456impl std::fmt::Display for AuthorFilter {
457 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
458 match self {
459 AuthorFilter::AnyAuthor => {
460 write!(f, "*")
461 }
462 AuthorFilter::Me => {
463 write!(f, "me")
464 }
465 AuthorFilter::NotMe => {
466 write!(f, "!me")
467 }
468 AuthorFilter::TheseAuthors(ids) => {
469 let s: String = ids
470 .iter()
471 .map(|e| e.to_string())
472 .collect::<Vec<_>>()
473 .join(",");
474 write!(f, "{s}")
475 }
476 AuthorFilter::NotTheseAuthors(ids) => {
477 let s: String = ids
478 .iter()
479 .map(|e| format!("!{e}"))
480 .collect::<Vec<_>>()
481 .join(",");
482 write!(f, "{s}")
483 }
484 }
485 }
486}
487
488#[derive(Debug, Clone)]
490pub enum AssigneeFilter {
491 AnyAssignee,
493 Me,
495 NotMe,
497 TheseAssignees(Vec<u64>),
499 NotTheseAssignees(Vec<u64>),
501 NoAssignee,
503}
504
505impl std::fmt::Display for AssigneeFilter {
506 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
507 match self {
508 AssigneeFilter::AnyAssignee => {
509 write!(f, "*")
510 }
511 AssigneeFilter::Me => {
512 write!(f, "me")
513 }
514 AssigneeFilter::NotMe => {
515 write!(f, "!me")
516 }
517 AssigneeFilter::TheseAssignees(ids) => {
518 let s: String = ids
519 .iter()
520 .map(|e| e.to_string())
521 .collect::<Vec<_>>()
522 .join(",");
523 write!(f, "{s}")
524 }
525 AssigneeFilter::NotTheseAssignees(ids) => {
526 let s: String = ids
527 .iter()
528 .map(|e| format!("!{e}"))
529 .collect::<Vec<_>>()
530 .join(",");
531 write!(f, "{s}")
532 }
533 AssigneeFilter::NoAssignee => {
534 write!(f, "!*")
535 }
536 }
537 }
538}
539
540#[derive(Debug, Clone)]
542pub enum StringFieldFilter {
543 ExactMatch(String),
545 SubStringMatch(String),
547}
548
549impl std::fmt::Display for StringFieldFilter {
550 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
551 match self {
552 StringFieldFilter::ExactMatch(s) => {
553 write!(f, "{s}")
554 }
555 StringFieldFilter::SubStringMatch(s) => {
556 write!(f, "~{s}")
557 }
558 }
559 }
560}
561
562pub trait ComparableFilterValue {
565 fn value_string(&self) -> Cow<'static, str>;
569}
570
571impl ComparableFilterValue for time::Date {
572 fn value_string(&self) -> Cow<'static, str> {
573 let format = time::format_description::parse("[year]-[month]-[day]").unwrap();
574 self.format(&format).unwrap().into()
575 }
576}
577
578impl ComparableFilterValue for time::OffsetDateTime {
579 fn value_string(&self) -> Cow<'static, str> {
580 self.format(&time::format_description::well_known::Rfc3339)
581 .unwrap()
582 .into()
583 }
584}
585
586#[derive(Debug, Clone)]
588pub enum ComparableFilter<V> {
589 ExactMatch(V),
591 Range(V, V),
593 LessThan(V),
595 LessThanOrEqual(V),
597 GreaterThan(V),
599 GreaterThanOrEqual(V),
601}
602
603impl<V> std::fmt::Display for ComparableFilter<V>
604where
605 V: ComparableFilterValue,
606{
607 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
608 match self {
609 ComparableFilter::ExactMatch(v) => {
610 write!(f, "{}", v.value_string())
611 }
612 ComparableFilter::Range(v_start, v_end) => {
613 write!(f, "><{}|{}", v_start.value_string(), v_end.value_string())
614 }
615 ComparableFilter::LessThan(v) => {
616 write!(f, "<{}", v.value_string())
617 }
618 ComparableFilter::LessThanOrEqual(v) => {
619 write!(f, "<={}", v.value_string())
620 }
621 ComparableFilter::GreaterThan(v) => {
622 write!(f, ">{}", v.value_string())
623 }
624 ComparableFilter::GreaterThanOrEqual(v) => {
625 write!(f, ">={}", v.value_string())
626 }
627 }
628 }
629}
630
631#[derive(Debug, Clone)]
633pub enum SortByColumn {
634 Forward {
636 column_name: String,
638 },
639 Reverse {
641 column_name: String,
643 },
644}
645
646impl std::fmt::Display for SortByColumn {
647 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
648 match self {
649 SortByColumn::Forward { column_name } => {
650 write!(f, "{column_name}")
651 }
652 SortByColumn::Reverse { column_name } => {
653 write!(f, "{column_name}:desc")
654 }
655 }
656 }
657}
658
659#[derive(Debug, Clone)]
661pub enum IssueListInclude {
662 Attachments,
664 Relations,
666}
667
668impl std::fmt::Display for IssueListInclude {
669 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
670 match self {
671 Self::Attachments => {
672 write!(f, "attachments")
673 }
674 Self::Relations => {
675 write!(f, "relations")
676 }
677 }
678 }
679}
680
681#[derive(Debug, Clone, Builder)]
683#[builder(setter(strip_option))]
684pub struct ListIssues {
685 #[builder(default)]
687 include: Option<Vec<IssueListInclude>>,
688 #[builder(default)]
690 sort: Option<Vec<SortByColumn>>,
691 #[builder(default)]
693 issue_id: Option<Vec<u64>>,
694 #[builder(default)]
696 project_id: Option<Vec<u64>>,
697 #[builder(default)]
699 subproject_id: Option<SubProjectFilter>,
700 #[builder(default)]
702 tracker_id: Option<Vec<u64>>,
703 #[builder(default)]
705 priority_id: Option<Vec<u64>>,
706 #[builder(default)]
708 parent_id: Option<Vec<u64>>,
709 #[builder(default)]
711 category_id: Option<Vec<u64>>,
712 #[builder(default)]
714 status_id: Option<StatusFilter>,
715 #[builder(default)]
717 subject: Option<StringFieldFilter>,
718 #[builder(default)]
720 description: Option<StringFieldFilter>,
721 #[builder(default)]
723 author: Option<AuthorFilter>,
724 #[builder(default)]
726 assignee: Option<AssigneeFilter>,
727 #[builder(default)]
729 query_id: Option<u64>,
730 #[builder(default)]
732 version_id: Option<Vec<u64>>,
733 #[builder(default)]
735 created_on: Option<ComparableFilter<time::OffsetDateTime>>,
736 #[builder(default)]
738 updated_on: Option<ComparableFilter<time::OffsetDateTime>>,
739 #[builder(default)]
741 start_date: Option<ComparableFilter<time::Date>>,
742 #[builder(default)]
744 due_date: Option<ComparableFilter<time::Date>>,
745}
746
747impl ReturnsJsonResponse for ListIssues {}
748
749impl Pageable for ListIssues {
750 fn response_wrapper_key(&self) -> String {
751 "issues".to_string()
752 }
753}
754
755impl ListIssues {
756 #[must_use]
758 pub fn builder() -> ListIssuesBuilder {
759 ListIssuesBuilder::default()
760 }
761}
762
763impl Endpoint for ListIssues {
764 fn method(&self) -> Method {
765 Method::GET
766 }
767
768 fn endpoint(&self) -> Cow<'static, str> {
769 "issues.json".into()
770 }
771
772 fn parameters(&self) -> QueryParams {
773 let mut params = QueryParams::default();
774 params.push_opt("include", self.include.as_ref());
775 params.push_opt("sort", self.sort.as_ref());
776 params.push_opt("issue_id", self.issue_id.as_ref());
777 params.push_opt("project_id", self.project_id.as_ref());
778 params.push_opt(
779 "subproject_id",
780 self.subproject_id.as_ref().map(|s| s.to_string()),
781 );
782 params.push_opt("tracker_id", self.tracker_id.as_ref());
783 params.push_opt("priority_id", self.priority_id.as_ref());
784 params.push_opt("parent_id", self.parent_id.as_ref());
785 params.push_opt("category_id", self.category_id.as_ref());
786 params.push_opt("status_id", self.status_id.as_ref().map(|s| s.to_string()));
787 params.push_opt("subject", self.subject.as_ref().map(|s| s.to_string()));
788 params.push_opt(
789 "description",
790 self.description.as_ref().map(|s| s.to_string()),
791 );
792 params.push_opt("author_id", self.author.as_ref().map(|s| s.to_string()));
793 params.push_opt(
794 "assigned_to_id",
795 self.assignee.as_ref().map(|s| s.to_string()),
796 );
797 params.push_opt("query_id", self.query_id);
798 params.push_opt("fixed_version_id", self.version_id.as_ref());
799 params.push_opt(
800 "created_on",
801 self.created_on.as_ref().map(|s| s.to_string()),
802 );
803 params.push_opt(
804 "updated_on",
805 self.updated_on.as_ref().map(|s| s.to_string()),
806 );
807 params.push_opt(
808 "start_date",
809 self.start_date.as_ref().map(|s| s.to_string()),
810 );
811 params.push_opt("due_date", self.due_date.as_ref().map(|s| s.to_string()));
812 params
813 }
814}
815
816#[derive(Debug, Clone)]
818pub enum IssueInclude {
819 Children,
821 Attachments,
823 Relations,
825 Changesets,
827 Journals,
829 Watchers,
831 AllowedStatuses,
844}
845
846impl std::fmt::Display for IssueInclude {
847 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
848 match self {
849 Self::Children => {
850 write!(f, "children")
851 }
852 Self::Attachments => {
853 write!(f, "attachments")
854 }
855 Self::Relations => {
856 write!(f, "relations")
857 }
858 Self::Changesets => {
859 write!(f, "relations")
860 }
861 Self::Journals => {
862 write!(f, "journals")
863 }
864 Self::Watchers => {
865 write!(f, "watchers")
866 }
867 Self::AllowedStatuses => {
868 write!(f, "allowed_statuses")
869 }
870 }
871 }
872}
873
874#[derive(Debug, Clone, Builder)]
876#[builder(setter(strip_option))]
877pub struct GetIssue {
878 id: u64,
880 #[builder(default)]
882 include: Option<Vec<IssueInclude>>,
883}
884
885impl ReturnsJsonResponse for GetIssue {}
886impl NoPagination for GetIssue {}
887
888impl GetIssue {
889 #[must_use]
891 pub fn builder() -> GetIssueBuilder {
892 GetIssueBuilder::default()
893 }
894}
895
896impl Endpoint for GetIssue {
897 fn method(&self) -> Method {
898 Method::GET
899 }
900
901 fn endpoint(&self) -> Cow<'static, str> {
902 format!("issues/{}.json", &self.id).into()
903 }
904
905 fn parameters(&self) -> QueryParams {
906 let mut params = QueryParams::default();
907 params.push_opt("include", self.include.as_ref());
908 params
909 }
910}
911
912#[derive(Debug, Clone, Serialize, serde::Deserialize)]
914pub struct CustomField<'a> {
915 pub id: u64,
917 pub name: Option<Cow<'a, str>>,
919 pub value: Cow<'a, str>,
921}
922
923#[derive(Debug, Clone, Serialize)]
926pub struct UploadedAttachment<'a> {
927 pub token: Cow<'a, str>,
929 pub filename: Cow<'a, str>,
931 #[serde(skip_serializing_if = "Option::is_none")]
933 pub description: Option<Cow<'a, str>>,
934 pub content_type: Cow<'a, str>,
936}
937
938#[serde_with::skip_serializing_none]
940#[derive(Debug, Clone, Builder, Serialize)]
941#[builder(setter(strip_option))]
942pub struct CreateIssue<'a> {
943 project_id: u64,
945 #[builder(default)]
947 tracker_id: Option<u64>,
948 #[builder(default)]
950 status_id: Option<u64>,
951 #[builder(default)]
953 priority_id: Option<u64>,
954 #[builder(setter(into), default)]
956 subject: Option<Cow<'a, str>>,
957 #[builder(setter(into), default)]
959 description: Option<Cow<'a, str>>,
960 #[builder(default)]
962 category_id: Option<u64>,
963 #[builder(default, setter(name = "version"))]
965 fixed_version_id: Option<u64>,
966 #[builder(default)]
968 assigned_to_id: Option<u64>,
969 #[builder(default)]
971 parent_issue_id: Option<u64>,
972 #[builder(default)]
974 custom_fields: Option<Vec<CustomField<'a>>>,
975 #[builder(default)]
977 watcher_user_ids: Option<Vec<u64>>,
978 #[builder(default)]
980 is_private: Option<bool>,
981 #[builder(default)]
983 estimated_hours: Option<f64>,
984 #[builder(default)]
986 uploads: Option<Vec<UploadedAttachment<'a>>>,
987}
988
989impl<'a> CreateIssue<'a> {
990 #[must_use]
992 pub fn builder() -> CreateIssueBuilder<'a> {
993 CreateIssueBuilder::default()
994 }
995}
996
997impl ReturnsJsonResponse for CreateIssue<'_> {}
998impl NoPagination for CreateIssue<'_> {}
999
1000impl Endpoint for CreateIssue<'_> {
1001 fn method(&self) -> Method {
1002 Method::POST
1003 }
1004
1005 fn endpoint(&self) -> Cow<'static, str> {
1006 "issues.json".into()
1007 }
1008
1009 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
1010 Ok(Some((
1011 "application/json",
1012 serde_json::to_vec(&IssueWrapper::<CreateIssue> {
1013 issue: (*self).to_owned(),
1014 })?,
1015 )))
1016 }
1017}
1018
1019#[serde_with::skip_serializing_none]
1021#[derive(Debug, Clone, Builder, Serialize)]
1022#[builder(setter(strip_option))]
1023pub struct UpdateIssue<'a> {
1024 #[serde(skip_serializing)]
1026 id: u64,
1027 #[builder(default)]
1029 project_id: Option<u64>,
1030 #[builder(default)]
1032 tracker_id: Option<u64>,
1033 #[builder(default)]
1035 status_id: Option<u64>,
1036 #[builder(default)]
1038 priority_id: Option<u64>,
1039 #[builder(setter(into), default)]
1041 subject: Option<Cow<'a, str>>,
1042 #[builder(setter(into), default)]
1044 description: Option<Cow<'a, str>>,
1045 #[builder(default)]
1047 category_id: Option<u64>,
1048 #[builder(default, setter(name = "version"))]
1050 fixed_version_id: Option<u64>,
1051 #[builder(default)]
1053 assigned_to_id: Option<u64>,
1054 #[builder(default)]
1056 parent_issue_id: Option<u64>,
1057 #[builder(default)]
1059 custom_fields: Option<Vec<CustomField<'a>>>,
1060 #[builder(default)]
1062 watcher_user_ids: Option<Vec<u64>>,
1063 #[builder(default)]
1065 is_private: Option<bool>,
1066 #[builder(default)]
1068 estimated_hours: Option<f64>,
1069 #[builder(default)]
1071 notes: Option<Cow<'a, str>>,
1072 #[builder(default)]
1074 private_notes: Option<bool>,
1075 #[builder(default)]
1077 uploads: Option<Vec<UploadedAttachment<'a>>>,
1078}
1079
1080impl<'a> UpdateIssue<'a> {
1081 #[must_use]
1083 pub fn builder() -> UpdateIssueBuilder<'a> {
1084 UpdateIssueBuilder::default()
1085 }
1086}
1087
1088impl Endpoint for UpdateIssue<'_> {
1089 fn method(&self) -> Method {
1090 Method::PUT
1091 }
1092
1093 fn endpoint(&self) -> Cow<'static, str> {
1094 format!("issues/{}.json", self.id).into()
1095 }
1096
1097 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
1098 Ok(Some((
1099 "application/json",
1100 serde_json::to_vec(&IssueWrapper::<UpdateIssue> {
1101 issue: (*self).to_owned(),
1102 })?,
1103 )))
1104 }
1105}
1106
1107#[derive(Debug, Clone, Builder)]
1109#[builder(setter(strip_option))]
1110pub struct DeleteIssue {
1111 id: u64,
1113}
1114
1115impl DeleteIssue {
1116 #[must_use]
1118 pub fn builder() -> DeleteIssueBuilder {
1119 DeleteIssueBuilder::default()
1120 }
1121}
1122
1123impl Endpoint for DeleteIssue {
1124 fn method(&self) -> Method {
1125 Method::DELETE
1126 }
1127
1128 fn endpoint(&self) -> Cow<'static, str> {
1129 format!("issues/{}.json", &self.id).into()
1130 }
1131}
1132
1133#[derive(Debug, Clone, Builder, Serialize)]
1135#[builder(setter(strip_option))]
1136pub struct AddWatcher {
1137 #[serde(skip_serializing)]
1139 issue_id: u64,
1140 user_id: u64,
1142}
1143
1144impl AddWatcher {
1145 #[must_use]
1147 pub fn builder() -> AddWatcherBuilder {
1148 AddWatcherBuilder::default()
1149 }
1150}
1151
1152impl Endpoint for AddWatcher {
1153 fn method(&self) -> Method {
1154 Method::POST
1155 }
1156
1157 fn endpoint(&self) -> Cow<'static, str> {
1158 format!("issues/{}/watchers.json", &self.issue_id).into()
1159 }
1160
1161 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
1162 Ok(Some(("application/json", serde_json::to_vec(self)?)))
1163 }
1164}
1165
1166#[derive(Debug, Clone, Builder)]
1168#[builder(setter(strip_option))]
1169pub struct RemoveWatcher {
1170 issue_id: u64,
1172 user_id: u64,
1174}
1175
1176impl RemoveWatcher {
1177 #[must_use]
1179 pub fn builder() -> RemoveWatcherBuilder {
1180 RemoveWatcherBuilder::default()
1181 }
1182}
1183
1184impl Endpoint for RemoveWatcher {
1185 fn method(&self) -> Method {
1186 Method::DELETE
1187 }
1188
1189 fn endpoint(&self) -> Cow<'static, str> {
1190 format!("issues/{}/watchers/{}.json", &self.issue_id, &self.user_id).into()
1191 }
1192}
1193
1194#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
1196pub struct IssuesWrapper<T> {
1197 pub issues: Vec<T>,
1199}
1200
1201#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
1204pub struct IssueWrapper<T> {
1205 pub issue: T,
1207}
1208
1209#[cfg(test)]
1210pub(crate) mod test {
1211 use super::*;
1212 use crate::api::test_helpers::with_project;
1213 use crate::api::ResponsePage;
1214 use pretty_assertions::assert_eq;
1215 use std::error::Error;
1216 use tokio::sync::RwLock;
1217 use tracing_test::traced_test;
1218
1219 pub static ISSUES_LOCK: RwLock<()> = RwLock::const_new(());
1222
1223 #[traced_test]
1224 #[test]
1225 fn test_list_issues_first_page() -> Result<(), Box<dyn Error>> {
1226 let _r_issues = ISSUES_LOCK.read();
1227 dotenvy::dotenv()?;
1228 let redmine = crate::api::Redmine::from_env(
1229 reqwest::blocking::Client::builder()
1230 .use_rustls_tls()
1231 .build()?,
1232 )?;
1233 let endpoint = ListIssues::builder().build()?;
1234 redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
1235 Ok(())
1236 }
1237
1238 #[traced_test]
1242 #[test]
1243 #[ignore]
1244 fn test_list_issues_all_pages() -> Result<(), Box<dyn Error>> {
1245 let _r_issues = ISSUES_LOCK.read();
1246 dotenvy::dotenv()?;
1247 let redmine = crate::api::Redmine::from_env(
1248 reqwest::blocking::Client::builder()
1249 .use_rustls_tls()
1250 .build()?,
1251 )?;
1252 let endpoint = ListIssues::builder().build()?;
1253 redmine.json_response_body_all_pages::<_, Issue>(&endpoint)?;
1254 Ok(())
1255 }
1256
1257 #[traced_test]
1258 #[test]
1259 fn test_get_issue() -> Result<(), Box<dyn Error>> {
1260 let _r_issues = ISSUES_LOCK.read();
1261 dotenvy::dotenv()?;
1262 let redmine = crate::api::Redmine::from_env(
1263 reqwest::blocking::Client::builder()
1264 .use_rustls_tls()
1265 .build()?,
1266 )?;
1267 let endpoint = GetIssue::builder().id(40000).build()?;
1268 redmine.json_response_body::<_, IssueWrapper<Issue>>(&endpoint)?;
1269 Ok(())
1270 }
1271
1272 #[function_name::named]
1273 #[traced_test]
1274 #[test]
1275 fn test_create_issue() -> Result<(), Box<dyn Error>> {
1276 let _w_issues = ISSUES_LOCK.write();
1277 let name = format!("unittest_{}", function_name!());
1278 with_project(&name, |redmine, project_id, _| {
1279 let create_endpoint = super::CreateIssue::builder()
1280 .project_id(project_id)
1281 .subject("old test subject")
1282 .build()?;
1283 redmine.json_response_body::<_, IssueWrapper<Issue>>(&create_endpoint)?;
1284 Ok(())
1285 })?;
1286 Ok(())
1287 }
1288
1289 #[function_name::named]
1290 #[traced_test]
1291 #[test]
1292 fn test_update_issue() -> Result<(), Box<dyn Error>> {
1293 let _w_issues = ISSUES_LOCK.write();
1294 let name = format!("unittest_{}", function_name!());
1295 with_project(&name, |redmine, project_id, _name| {
1296 let create_endpoint = super::CreateIssue::builder()
1297 .project_id(project_id)
1298 .subject("old test subject")
1299 .build()?;
1300 let IssueWrapper { issue }: IssueWrapper<Issue> =
1301 redmine.json_response_body::<_, _>(&create_endpoint)?;
1302 let update_endpoint = super::UpdateIssue::builder()
1303 .id(issue.id)
1304 .subject("New test subject")
1305 .build()?;
1306 redmine.ignore_response_body::<_>(&update_endpoint)?;
1307 Ok(())
1308 })?;
1309 Ok(())
1310 }
1311
1312 #[function_name::named]
1313 #[traced_test]
1314 #[test]
1315 fn test_delete_issue() -> Result<(), Box<dyn Error>> {
1316 let _w_issues = ISSUES_LOCK.write();
1317 let name = format!("unittest_{}", function_name!());
1318 with_project(&name, |redmine, project_id, _name| {
1319 let create_endpoint = super::CreateIssue::builder()
1320 .project_id(project_id)
1321 .subject("test subject")
1322 .build()?;
1323 let IssueWrapper { issue }: IssueWrapper<Issue> =
1324 redmine.json_response_body::<_, _>(&create_endpoint)?;
1325 let delete_endpoint = super::DeleteIssue::builder().id(issue.id).build()?;
1326 redmine.ignore_response_body::<_>(&delete_endpoint)?;
1327 Ok(())
1328 })?;
1329 Ok(())
1330 }
1331
1332 #[traced_test]
1337 #[test]
1338 fn test_completeness_issue_type_first_page() -> Result<(), Box<dyn Error>> {
1339 let _r_issues = ISSUES_LOCK.read();
1340 dotenvy::dotenv()?;
1341 let redmine = crate::api::Redmine::from_env(
1342 reqwest::blocking::Client::builder()
1343 .use_rustls_tls()
1344 .build()?,
1345 )?;
1346 let endpoint = ListIssues::builder()
1347 .include(vec![
1348 IssueListInclude::Attachments,
1349 IssueListInclude::Relations,
1350 ])
1351 .build()?;
1352 let ResponsePage {
1353 values,
1354 total_count: _,
1355 offset: _,
1356 limit: _,
1357 } = redmine.json_response_body_page::<_, serde_json::Value>(&endpoint, 0, 100)?;
1358 for value in values {
1359 let o: Issue = serde_json::from_value(value.clone())?;
1360 let reserialized = serde_json::to_value(o)?;
1361 assert_eq!(value, reserialized);
1362 }
1363 Ok(())
1364 }
1365
1366 #[traced_test]
1375 #[test]
1376 #[ignore]
1377 fn test_completeness_issue_type_all_pages() -> Result<(), Box<dyn Error>> {
1378 let _r_issues = ISSUES_LOCK.read();
1379 dotenvy::dotenv()?;
1380 let redmine = crate::api::Redmine::from_env(
1381 reqwest::blocking::Client::builder()
1382 .use_rustls_tls()
1383 .build()?,
1384 )?;
1385 let endpoint = ListIssues::builder()
1386 .include(vec![
1387 IssueListInclude::Attachments,
1388 IssueListInclude::Relations,
1389 ])
1390 .build()?;
1391 let values = redmine.json_response_body_all_pages::<_, serde_json::Value>(&endpoint)?;
1392 for value in values {
1393 let o: Issue = serde_json::from_value(value.clone())?;
1394 let reserialized = serde_json::to_value(o)?;
1395 assert_eq!(value, reserialized);
1396 }
1397 Ok(())
1398 }
1399
1400 #[traced_test]
1410 #[test]
1411 #[ignore]
1412 fn test_completeness_issue_type_all_pages_all_issue_details() -> Result<(), Box<dyn Error>> {
1413 let _r_issues = ISSUES_LOCK.read();
1414 dotenvy::dotenv()?;
1415 let redmine = crate::api::Redmine::from_env(
1416 reqwest::blocking::Client::builder()
1417 .use_rustls_tls()
1418 .build()?,
1419 )?;
1420 let endpoint = ListIssues::builder()
1421 .include(vec![
1422 IssueListInclude::Attachments,
1423 IssueListInclude::Relations,
1424 ])
1425 .build()?;
1426 let issues = redmine.json_response_body_all_pages::<_, Issue>(&endpoint)?;
1427 for issue in issues {
1428 let get_endpoint = GetIssue::builder()
1429 .id(issue.id)
1430 .include(vec![
1431 IssueInclude::Attachments,
1432 IssueInclude::Children,
1433 IssueInclude::Changesets,
1434 IssueInclude::Relations,
1435 IssueInclude::Journals,
1436 IssueInclude::Watchers,
1437 ])
1438 .build()?;
1439 let IssueWrapper { issue: mut value } =
1440 redmine.json_response_body::<_, IssueWrapper<serde_json::Value>>(&get_endpoint)?;
1441 let o: Issue = serde_json::from_value(value.clone())?;
1442 let value_object = value.as_object_mut().unwrap();
1448 if value_object.get("total_estimated_hours") == Some(&serde_json::Value::Null) {
1449 value_object.remove("total_estimated_hours");
1450 }
1451 let reserialized = serde_json::to_value(o)?;
1452 assert_eq!(value, reserialized);
1453 }
1454 Ok(())
1455 }
1456}