1use derive_builder::Builder;
92use reqwest::Method;
93use std::borrow::Cow;
94
95use crate::api::attachments::Attachment;
96use crate::api::custom_fields::CustomFieldEssentialsWithValue;
97use crate::api::enumerations::IssuePriorityEssentials;
98use crate::api::groups::{Group, GroupEssentials};
99use crate::api::issue_categories::IssueCategoryEssentials;
100use crate::api::issue_relations::IssueRelation;
101use crate::api::issue_statuses::IssueStatusEssentials;
102use crate::api::projects::ProjectEssentials;
103use crate::api::trackers::TrackerEssentials;
104use crate::api::users::UserEssentials;
105use crate::api::versions::VersionEssentials;
106use crate::api::{Endpoint, NoPagination, Pageable, QueryParams, ReturnsJsonResponse};
107use serde::Serialize;
108
109#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
112pub struct AssigneeEssentials {
113 pub id: u64,
115 pub name: String,
117}
118
119impl From<UserEssentials> for AssigneeEssentials {
120 fn from(v: UserEssentials) -> Self {
121 AssigneeEssentials {
122 id: v.id,
123 name: v.name,
124 }
125 }
126}
127
128impl From<&UserEssentials> for AssigneeEssentials {
129 fn from(v: &UserEssentials) -> Self {
130 AssigneeEssentials {
131 id: v.id,
132 name: v.name.to_owned(),
133 }
134 }
135}
136
137impl From<GroupEssentials> for AssigneeEssentials {
138 fn from(v: GroupEssentials) -> Self {
139 AssigneeEssentials {
140 id: v.id,
141 name: v.name,
142 }
143 }
144}
145
146impl From<&GroupEssentials> for AssigneeEssentials {
147 fn from(v: &GroupEssentials) -> Self {
148 AssigneeEssentials {
149 id: v.id,
150 name: v.name.to_owned(),
151 }
152 }
153}
154
155impl From<Group> for AssigneeEssentials {
156 fn from(v: Group) -> Self {
157 AssigneeEssentials {
158 id: v.id,
159 name: v.name,
160 }
161 }
162}
163
164impl From<&Group> for AssigneeEssentials {
165 fn from(v: &Group) -> Self {
166 AssigneeEssentials {
167 id: v.id,
168 name: v.name.to_owned(),
169 }
170 }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
176pub struct IssueEssentials {
177 pub id: u64,
179}
180
181impl From<Issue> for IssueEssentials {
182 fn from(v: Issue) -> Self {
183 IssueEssentials { id: v.id }
184 }
185}
186
187impl From<&Issue> for IssueEssentials {
188 fn from(v: &Issue) -> Self {
189 IssueEssentials { id: v.id }
190 }
191}
192
193#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
195pub enum ChangePropertyType {
196 #[serde(rename = "attr")]
198 Attr,
199 #[serde(rename = "cf")]
201 Cf,
202 #[serde(rename = "relation")]
204 Relation,
205 #[serde(rename = "attachment")]
207 Attachment,
208}
209
210#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
212pub struct JournalChange {
213 pub name: String,
215 pub old_value: Option<String>,
217 pub new_value: Option<String>,
219 pub property: ChangePropertyType,
221}
222
223#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
225pub struct Journal {
226 pub id: u64,
228 pub user: UserEssentials,
230 pub notes: Option<String>,
232 pub private_notes: bool,
234 #[serde(
236 serialize_with = "crate::api::serialize_rfc3339",
237 deserialize_with = "crate::api::deserialize_rfc3339"
238 )]
239 pub created_on: time::OffsetDateTime,
240 pub details: Vec<JournalChange>,
242}
243
244#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
246pub struct ChildIssue {
247 pub id: u64,
249 pub subject: String,
251 pub tracker: TrackerEssentials,
253 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub children: Option<Vec<ChildIssue>>,
256}
257
258#[derive(Debug, Clone, Serialize, serde::Deserialize)]
262pub struct Issue {
263 pub id: u64,
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub parent: Option<IssueEssentials>,
268 pub project: ProjectEssentials,
270 pub tracker: TrackerEssentials,
272 pub status: IssueStatusEssentials,
274 pub priority: IssuePriorityEssentials,
276 pub author: UserEssentials,
278 #[serde(skip_serializing_if = "Option::is_none")]
280 pub assigned_to: Option<AssigneeEssentials>,
281 #[serde(skip_serializing_if = "Option::is_none")]
283 pub category: Option<IssueCategoryEssentials>,
284 #[serde(rename = "fixed_version", skip_serializing_if = "Option::is_none")]
286 pub version: Option<VersionEssentials>,
287 #[serde(skip_serializing_if = "Option::is_none")]
289 pub subject: Option<String>,
290 pub description: Option<String>,
292 is_private: Option<bool>,
294 pub start_date: Option<time::Date>,
296 pub due_date: Option<time::Date>,
298 #[serde(
300 serialize_with = "crate::api::serialize_optional_rfc3339",
301 deserialize_with = "crate::api::deserialize_optional_rfc3339"
302 )]
303 pub closed_on: Option<time::OffsetDateTime>,
304 pub done_ratio: u64,
306 #[serde(default, skip_serializing_if = "Option::is_none")]
308 pub custom_fields: Option<Vec<CustomFieldEssentialsWithValue>>,
309 pub estimated_hours: Option<f64>,
311 #[serde(
313 serialize_with = "crate::api::serialize_rfc3339",
314 deserialize_with = "crate::api::deserialize_rfc3339"
315 )]
316 pub created_on: time::OffsetDateTime,
317 #[serde(
319 serialize_with = "crate::api::serialize_rfc3339",
320 deserialize_with = "crate::api::deserialize_rfc3339"
321 )]
322 pub updated_on: time::OffsetDateTime,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub attachments: Option<Vec<Attachment>>,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub relations: Option<Vec<IssueRelation>>,
329 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub journals: Option<Vec<Journal>>,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub children: Option<Vec<ChildIssue>>,
335 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub watchers: Option<Vec<UserEssentials>>,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
340 pub spent_hours: Option<f64>,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
343 pub total_spent_hours: Option<f64>,
344 #[serde(default)]
346 pub total_estimated_hours: Option<f64>,
347}
348
349#[derive(Debug, Clone)]
351pub enum SubProjectFilter {
352 OnlyParentProject,
354 TheseSubProjects(Vec<u64>),
356 NotTheseSubProjects(Vec<u64>),
358}
359
360impl std::fmt::Display for SubProjectFilter {
361 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
362 match self {
363 SubProjectFilter::OnlyParentProject => {
364 write!(f, "!*")
365 }
366 SubProjectFilter::TheseSubProjects(ids) => {
367 let s: String = ids
368 .iter()
369 .map(|e| e.to_string())
370 .collect::<Vec<_>>()
371 .join(",");
372 write!(f, "{}", s)
373 }
374 SubProjectFilter::NotTheseSubProjects(ids) => {
375 let s: String = ids
376 .iter()
377 .map(|e| format!("!{}", e))
378 .collect::<Vec<_>>()
379 .join(",");
380 write!(f, "{}", s)
381 }
382 }
383 }
384}
385
386#[derive(Debug, Clone)]
388pub enum StatusFilter {
389 Open,
391 Closed,
393 All,
395 TheseStatuses(Vec<u64>),
397 NotTheseStatuses(Vec<u64>),
399}
400
401impl std::fmt::Display for StatusFilter {
402 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
403 match self {
404 StatusFilter::Open => {
405 write!(f, "open")
406 }
407 StatusFilter::Closed => {
408 write!(f, "closed")
409 }
410 StatusFilter::All => {
411 write!(f, "*")
412 }
413 StatusFilter::TheseStatuses(ids) => {
414 let s: String = ids
415 .iter()
416 .map(|e| e.to_string())
417 .collect::<Vec<_>>()
418 .join(",");
419 write!(f, "{}", s)
420 }
421 StatusFilter::NotTheseStatuses(ids) => {
422 let s: String = ids
423 .iter()
424 .map(|e| format!("!{}", e))
425 .collect::<Vec<_>>()
426 .join(",");
427 write!(f, "{}", s)
428 }
429 }
430 }
431}
432
433#[derive(Debug, Clone)]
435pub enum AuthorFilter {
436 AnyAuthor,
438 Me,
440 NotMe,
442 TheseAuthors(Vec<u64>),
444 NotTheseAuthors(Vec<u64>),
446}
447
448impl std::fmt::Display for AuthorFilter {
449 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450 match self {
451 AuthorFilter::AnyAuthor => {
452 write!(f, "*")
453 }
454 AuthorFilter::Me => {
455 write!(f, "me")
456 }
457 AuthorFilter::NotMe => {
458 write!(f, "!me")
459 }
460 AuthorFilter::TheseAuthors(ids) => {
461 let s: String = ids
462 .iter()
463 .map(|e| e.to_string())
464 .collect::<Vec<_>>()
465 .join(",");
466 write!(f, "{}", s)
467 }
468 AuthorFilter::NotTheseAuthors(ids) => {
469 let s: String = ids
470 .iter()
471 .map(|e| format!("!{}", e))
472 .collect::<Vec<_>>()
473 .join(",");
474 write!(f, "{}", s)
475 }
476 }
477 }
478}
479
480#[derive(Debug, Clone)]
482pub enum AssigneeFilter {
483 AnyAssignee,
485 Me,
487 NotMe,
489 TheseAssignees(Vec<u64>),
491 NotTheseAssignees(Vec<u64>),
493 NoAssignee,
495}
496
497impl std::fmt::Display for AssigneeFilter {
498 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
499 match self {
500 AssigneeFilter::AnyAssignee => {
501 write!(f, "*")
502 }
503 AssigneeFilter::Me => {
504 write!(f, "me")
505 }
506 AssigneeFilter::NotMe => {
507 write!(f, "!me")
508 }
509 AssigneeFilter::TheseAssignees(ids) => {
510 let s: String = ids
511 .iter()
512 .map(|e| e.to_string())
513 .collect::<Vec<_>>()
514 .join(",");
515 write!(f, "{}", s)
516 }
517 AssigneeFilter::NotTheseAssignees(ids) => {
518 let s: String = ids
519 .iter()
520 .map(|e| format!("!{}", e))
521 .collect::<Vec<_>>()
522 .join(",");
523 write!(f, "{}", s)
524 }
525 AssigneeFilter::NoAssignee => {
526 write!(f, "!*")
527 }
528 }
529 }
530}
531
532#[derive(Debug, Clone)]
534pub enum StringFieldFilter {
535 ExactMatch(String),
537 SubStringMatch(String),
539}
540
541impl std::fmt::Display for StringFieldFilter {
542 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
543 match self {
544 StringFieldFilter::ExactMatch(s) => {
545 write!(f, "{}", s)
546 }
547 StringFieldFilter::SubStringMatch(s) => {
548 write!(f, "~{}", s)
549 }
550 }
551 }
552}
553
554pub trait ComparableFilterValue {
557 fn value_string(&self) -> Cow<'static, str>;
561}
562
563impl ComparableFilterValue for time::Date {
564 fn value_string(&self) -> Cow<'static, str> {
565 let format = time::format_description::parse("[year]-[month]-[day]").unwrap();
566 self.format(&format).unwrap().into()
567 }
568}
569
570impl ComparableFilterValue for time::OffsetDateTime {
571 fn value_string(&self) -> Cow<'static, str> {
572 self.format(&time::format_description::well_known::Rfc3339)
573 .unwrap()
574 .into()
575 }
576}
577
578#[derive(Debug, Clone)]
580pub enum ComparableFilter<V> {
581 ExactMatch(V),
583 Range(V, V),
585 LessThan(V),
587 LessThanOrEqual(V),
589 GreaterThan(V),
591 GreaterThanOrEqual(V),
593}
594
595impl<V> std::fmt::Display for ComparableFilter<V>
596where
597 V: ComparableFilterValue,
598{
599 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
600 match self {
601 ComparableFilter::ExactMatch(v) => {
602 write!(f, "{}", v.value_string())
603 }
604 ComparableFilter::Range(v_start, v_end) => {
605 write!(f, "><{}|{}", v_start.value_string(), v_end.value_string())
606 }
607 ComparableFilter::LessThan(v) => {
608 write!(f, "<{}", v.value_string())
609 }
610 ComparableFilter::LessThanOrEqual(v) => {
611 write!(f, "<={}", v.value_string())
612 }
613 ComparableFilter::GreaterThan(v) => {
614 write!(f, ">{}", v.value_string())
615 }
616 ComparableFilter::GreaterThanOrEqual(v) => {
617 write!(f, ">={}", v.value_string())
618 }
619 }
620 }
621}
622
623#[derive(Debug, Clone)]
625pub enum SortByColumn {
626 Forward {
628 column_name: String,
630 },
631 Reverse {
633 column_name: String,
635 },
636}
637
638impl std::fmt::Display for SortByColumn {
639 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
640 match self {
641 SortByColumn::Forward { column_name } => {
642 write!(f, "{}", column_name)
643 }
644 SortByColumn::Reverse { column_name } => {
645 write!(f, "{}:desc", column_name)
646 }
647 }
648 }
649}
650
651#[derive(Debug, Clone)]
653pub enum IssueListInclude {
654 Attachments,
656 Relations,
658}
659
660impl std::fmt::Display for IssueListInclude {
661 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
662 match self {
663 Self::Attachments => {
664 write!(f, "attachments")
665 }
666 Self::Relations => {
667 write!(f, "relations")
668 }
669 }
670 }
671}
672
673#[derive(Debug, Clone, Builder)]
675#[builder(setter(strip_option))]
676pub struct ListIssues {
677 #[builder(default)]
679 include: Option<Vec<IssueListInclude>>,
680 #[builder(default)]
682 sort: Option<Vec<SortByColumn>>,
683 #[builder(default)]
685 issue_id: Option<Vec<u64>>,
686 #[builder(default)]
688 project_id: Option<Vec<u64>>,
689 #[builder(default)]
691 subproject_id: Option<SubProjectFilter>,
692 #[builder(default)]
694 tracker_id: Option<Vec<u64>>,
695 #[builder(default)]
697 priority_id: Option<Vec<u64>>,
698 #[builder(default)]
700 parent_id: Option<Vec<u64>>,
701 #[builder(default)]
703 category_id: Option<Vec<u64>>,
704 #[builder(default)]
706 status_id: Option<StatusFilter>,
707 #[builder(default)]
709 subject: Option<StringFieldFilter>,
710 #[builder(default)]
712 description: Option<StringFieldFilter>,
713 #[builder(default)]
715 author: Option<AuthorFilter>,
716 #[builder(default)]
718 assignee: Option<AssigneeFilter>,
719 #[builder(default)]
721 query_id: Option<u64>,
722 #[builder(default)]
724 version_id: Option<Vec<u64>>,
725 #[builder(default)]
727 created_on: Option<ComparableFilter<time::OffsetDateTime>>,
728 #[builder(default)]
730 updated_on: Option<ComparableFilter<time::OffsetDateTime>>,
731 #[builder(default)]
733 start_date: Option<ComparableFilter<time::Date>>,
734 #[builder(default)]
736 due_date: Option<ComparableFilter<time::Date>>,
737}
738
739impl ReturnsJsonResponse for ListIssues {}
740
741impl Pageable for ListIssues {
742 fn response_wrapper_key(&self) -> String {
743 "issues".to_string()
744 }
745}
746
747impl ListIssues {
748 #[must_use]
750 pub fn builder() -> ListIssuesBuilder {
751 ListIssuesBuilder::default()
752 }
753}
754
755impl Endpoint for ListIssues {
756 fn method(&self) -> Method {
757 Method::GET
758 }
759
760 fn endpoint(&self) -> Cow<'static, str> {
761 "issues.json".into()
762 }
763
764 fn parameters(&self) -> QueryParams {
765 let mut params = QueryParams::default();
766 params.push_opt("include", self.include.as_ref());
767 params.push_opt("sort", self.sort.as_ref());
768 params.push_opt("issue_id", self.issue_id.as_ref());
769 params.push_opt("project_id", self.project_id.as_ref());
770 params.push_opt(
771 "subproject_id",
772 self.subproject_id.as_ref().map(|s| s.to_string()),
773 );
774 params.push_opt("tracker_id", self.tracker_id.as_ref());
775 params.push_opt("priority_id", self.priority_id.as_ref());
776 params.push_opt("parent_id", self.parent_id.as_ref());
777 params.push_opt("category_id", self.category_id.as_ref());
778 params.push_opt("status_id", self.status_id.as_ref().map(|s| s.to_string()));
779 params.push_opt("subject", self.subject.as_ref().map(|s| s.to_string()));
780 params.push_opt(
781 "description",
782 self.description.as_ref().map(|s| s.to_string()),
783 );
784 params.push_opt("author_id", self.author.as_ref().map(|s| s.to_string()));
785 params.push_opt(
786 "assigned_to_id",
787 self.assignee.as_ref().map(|s| s.to_string()),
788 );
789 params.push_opt("query_id", self.query_id);
790 params.push_opt("fixed_version_id", self.version_id.as_ref());
791 params.push_opt(
792 "created_on",
793 self.created_on.as_ref().map(|s| s.to_string()),
794 );
795 params.push_opt(
796 "updated_on",
797 self.updated_on.as_ref().map(|s| s.to_string()),
798 );
799 params.push_opt(
800 "start_date",
801 self.start_date.as_ref().map(|s| s.to_string()),
802 );
803 params.push_opt("due_date", self.due_date.as_ref().map(|s| s.to_string()));
804 params
805 }
806}
807
808#[derive(Debug, Clone)]
810pub enum IssueInclude {
811 Children,
813 Attachments,
815 Relations,
817 Changesets,
819 Journals,
821 Watchers,
823 AllowedStatuses,
836}
837
838impl std::fmt::Display for IssueInclude {
839 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
840 match self {
841 Self::Children => {
842 write!(f, "children")
843 }
844 Self::Attachments => {
845 write!(f, "attachments")
846 }
847 Self::Relations => {
848 write!(f, "relations")
849 }
850 Self::Changesets => {
851 write!(f, "relations")
852 }
853 Self::Journals => {
854 write!(f, "journals")
855 }
856 Self::Watchers => {
857 write!(f, "watchers")
858 }
859 Self::AllowedStatuses => {
860 write!(f, "allowed_statuses")
861 }
862 }
863 }
864}
865
866#[derive(Debug, Clone, Builder)]
868#[builder(setter(strip_option))]
869pub struct GetIssue {
870 id: u64,
872 #[builder(default)]
874 include: Option<Vec<IssueInclude>>,
875}
876
877impl ReturnsJsonResponse for GetIssue {}
878impl NoPagination for GetIssue {}
879
880impl GetIssue {
881 #[must_use]
883 pub fn builder() -> GetIssueBuilder {
884 GetIssueBuilder::default()
885 }
886}
887
888impl Endpoint for GetIssue {
889 fn method(&self) -> Method {
890 Method::GET
891 }
892
893 fn endpoint(&self) -> Cow<'static, str> {
894 format!("issues/{}.json", &self.id).into()
895 }
896
897 fn parameters(&self) -> QueryParams {
898 let mut params = QueryParams::default();
899 params.push_opt("include", self.include.as_ref());
900 params
901 }
902}
903
904#[derive(Debug, Clone, Serialize, serde::Deserialize)]
906pub struct CustomField<'a> {
907 pub id: u64,
909 pub name: Option<Cow<'a, str>>,
911 pub value: Cow<'a, str>,
913}
914
915#[derive(Debug, Clone, Serialize)]
918pub struct UploadedAttachment<'a> {
919 pub token: Cow<'a, str>,
921 pub filename: Cow<'a, str>,
923 #[serde(skip_serializing_if = "Option::is_none")]
925 pub description: Option<Cow<'a, str>>,
926 pub content_type: Cow<'a, str>,
928}
929
930#[serde_with::skip_serializing_none]
932#[derive(Debug, Clone, Builder, Serialize)]
933#[builder(setter(strip_option))]
934pub struct CreateIssue<'a> {
935 project_id: u64,
937 #[builder(default)]
939 tracker_id: Option<u64>,
940 #[builder(default)]
942 status_id: Option<u64>,
943 #[builder(default)]
945 priority_id: Option<u64>,
946 #[builder(setter(into), default)]
948 subject: Option<Cow<'a, str>>,
949 #[builder(setter(into), default)]
951 description: Option<Cow<'a, str>>,
952 #[builder(default)]
954 category_id: Option<u64>,
955 #[builder(default, setter(name = "version"))]
957 fixed_version_id: Option<u64>,
958 #[builder(default)]
960 assigned_to_id: Option<u64>,
961 #[builder(default)]
963 parent_issue_id: Option<u64>,
964 #[builder(default)]
966 custom_fields: Option<Vec<CustomField<'a>>>,
967 #[builder(default)]
969 watcher_user_ids: Option<Vec<u64>>,
970 #[builder(default)]
972 is_private: Option<bool>,
973 #[builder(default)]
975 estimated_hours: Option<f64>,
976 #[builder(default)]
978 uploads: Option<Vec<UploadedAttachment<'a>>>,
979}
980
981impl<'a> CreateIssue<'a> {
982 #[must_use]
984 pub fn builder() -> CreateIssueBuilder<'a> {
985 CreateIssueBuilder::default()
986 }
987}
988
989impl ReturnsJsonResponse for CreateIssue<'_> {}
990impl NoPagination for CreateIssue<'_> {}
991
992impl Endpoint for CreateIssue<'_> {
993 fn method(&self) -> Method {
994 Method::POST
995 }
996
997 fn endpoint(&self) -> Cow<'static, str> {
998 "issues.json".into()
999 }
1000
1001 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
1002 Ok(Some((
1003 "application/json",
1004 serde_json::to_vec(&IssueWrapper::<CreateIssue> {
1005 issue: (*self).to_owned(),
1006 })?,
1007 )))
1008 }
1009}
1010
1011#[serde_with::skip_serializing_none]
1013#[derive(Debug, Clone, Builder, Serialize)]
1014#[builder(setter(strip_option))]
1015pub struct UpdateIssue<'a> {
1016 #[serde(skip_serializing)]
1018 id: u64,
1019 #[builder(default)]
1021 project_id: Option<u64>,
1022 #[builder(default)]
1024 tracker_id: Option<u64>,
1025 #[builder(default)]
1027 status_id: Option<u64>,
1028 #[builder(default)]
1030 priority_id: Option<u64>,
1031 #[builder(setter(into), default)]
1033 subject: Option<Cow<'a, str>>,
1034 #[builder(setter(into), default)]
1036 description: Option<Cow<'a, str>>,
1037 #[builder(default)]
1039 category_id: Option<u64>,
1040 #[builder(default, setter(name = "version"))]
1042 fixed_version_id: Option<u64>,
1043 #[builder(default)]
1045 assigned_to_id: Option<u64>,
1046 #[builder(default)]
1048 parent_issue_id: Option<u64>,
1049 #[builder(default)]
1051 custom_fields: Option<Vec<CustomField<'a>>>,
1052 #[builder(default)]
1054 watcher_user_ids: Option<Vec<u64>>,
1055 #[builder(default)]
1057 is_private: Option<bool>,
1058 #[builder(default)]
1060 estimated_hours: Option<f64>,
1061 #[builder(default)]
1063 notes: Option<Cow<'a, str>>,
1064 #[builder(default)]
1066 private_notes: Option<bool>,
1067 #[builder(default)]
1069 uploads: Option<Vec<UploadedAttachment<'a>>>,
1070}
1071
1072impl<'a> UpdateIssue<'a> {
1073 #[must_use]
1075 pub fn builder() -> UpdateIssueBuilder<'a> {
1076 UpdateIssueBuilder::default()
1077 }
1078}
1079
1080impl Endpoint for UpdateIssue<'_> {
1081 fn method(&self) -> Method {
1082 Method::PUT
1083 }
1084
1085 fn endpoint(&self) -> Cow<'static, str> {
1086 format!("issues/{}.json", self.id).into()
1087 }
1088
1089 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
1090 Ok(Some((
1091 "application/json",
1092 serde_json::to_vec(&IssueWrapper::<UpdateIssue> {
1093 issue: (*self).to_owned(),
1094 })?,
1095 )))
1096 }
1097}
1098
1099#[derive(Debug, Clone, Builder)]
1101#[builder(setter(strip_option))]
1102pub struct DeleteIssue {
1103 id: u64,
1105}
1106
1107impl DeleteIssue {
1108 #[must_use]
1110 pub fn builder() -> DeleteIssueBuilder {
1111 DeleteIssueBuilder::default()
1112 }
1113}
1114
1115impl Endpoint for DeleteIssue {
1116 fn method(&self) -> Method {
1117 Method::DELETE
1118 }
1119
1120 fn endpoint(&self) -> Cow<'static, str> {
1121 format!("issues/{}.json", &self.id).into()
1122 }
1123}
1124
1125#[derive(Debug, Clone, Builder, Serialize)]
1127#[builder(setter(strip_option))]
1128pub struct AddWatcher {
1129 #[serde(skip_serializing)]
1131 issue_id: u64,
1132 user_id: u64,
1134}
1135
1136impl AddWatcher {
1137 #[must_use]
1139 pub fn builder() -> AddWatcherBuilder {
1140 AddWatcherBuilder::default()
1141 }
1142}
1143
1144impl Endpoint for AddWatcher {
1145 fn method(&self) -> Method {
1146 Method::POST
1147 }
1148
1149 fn endpoint(&self) -> Cow<'static, str> {
1150 format!("issues/{}/watchers.json", &self.issue_id).into()
1151 }
1152
1153 fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
1154 Ok(Some(("application/json", serde_json::to_vec(self)?)))
1155 }
1156}
1157
1158#[derive(Debug, Clone, Builder)]
1160#[builder(setter(strip_option))]
1161pub struct RemoveWatcher {
1162 issue_id: u64,
1164 user_id: u64,
1166}
1167
1168impl RemoveWatcher {
1169 #[must_use]
1171 pub fn builder() -> RemoveWatcherBuilder {
1172 RemoveWatcherBuilder::default()
1173 }
1174}
1175
1176impl Endpoint for RemoveWatcher {
1177 fn method(&self) -> Method {
1178 Method::DELETE
1179 }
1180
1181 fn endpoint(&self) -> Cow<'static, str> {
1182 format!("issues/{}/watchers/{}.json", &self.issue_id, &self.user_id).into()
1183 }
1184}
1185
1186#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
1188pub struct IssuesWrapper<T> {
1189 pub issues: Vec<T>,
1191}
1192
1193#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
1196pub struct IssueWrapper<T> {
1197 pub issue: T,
1199}
1200
1201#[cfg(test)]
1202pub(crate) mod test {
1203 use super::*;
1204 use crate::api::test_helpers::with_project;
1205 use crate::api::ResponsePage;
1206 use pretty_assertions::assert_eq;
1207 use std::error::Error;
1208 use tokio::sync::RwLock;
1209 use tracing_test::traced_test;
1210
1211 pub static ISSUES_LOCK: RwLock<()> = RwLock::const_new(());
1214
1215 #[traced_test]
1216 #[test]
1217 fn test_list_issues_first_page() -> Result<(), Box<dyn Error>> {
1218 let _r_issues = ISSUES_LOCK.read();
1219 dotenvy::dotenv()?;
1220 let redmine = crate::api::Redmine::from_env()?;
1221 let endpoint = ListIssues::builder().build()?;
1222 redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
1223 Ok(())
1224 }
1225
1226 #[traced_test]
1230 #[test]
1231 #[ignore]
1232 fn test_list_issues_all_pages() -> Result<(), Box<dyn Error>> {
1233 let _r_issues = ISSUES_LOCK.read();
1234 dotenvy::dotenv()?;
1235 let redmine = crate::api::Redmine::from_env()?;
1236 let endpoint = ListIssues::builder().build()?;
1237 redmine.json_response_body_all_pages::<_, Issue>(&endpoint)?;
1238 Ok(())
1239 }
1240
1241 #[traced_test]
1242 #[test]
1243 fn test_get_issue() -> Result<(), Box<dyn Error>> {
1244 let _r_issues = ISSUES_LOCK.read();
1245 dotenvy::dotenv()?;
1246 let redmine = crate::api::Redmine::from_env()?;
1247 let endpoint = GetIssue::builder().id(40000).build()?;
1248 redmine.json_response_body::<_, IssueWrapper<Issue>>(&endpoint)?;
1249 Ok(())
1250 }
1251
1252 #[function_name::named]
1253 #[traced_test]
1254 #[test]
1255 fn test_create_issue() -> Result<(), Box<dyn Error>> {
1256 let _w_issues = ISSUES_LOCK.write();
1257 let name = format!("unittest_{}", function_name!());
1258 with_project(&name, |redmine, project_id, _| {
1259 let create_endpoint = super::CreateIssue::builder()
1260 .project_id(project_id)
1261 .subject("old test subject")
1262 .build()?;
1263 redmine.json_response_body::<_, IssueWrapper<Issue>>(&create_endpoint)?;
1264 Ok(())
1265 })?;
1266 Ok(())
1267 }
1268
1269 #[function_name::named]
1270 #[traced_test]
1271 #[test]
1272 fn test_update_issue() -> Result<(), Box<dyn Error>> {
1273 let _w_issues = ISSUES_LOCK.write();
1274 let name = format!("unittest_{}", function_name!());
1275 with_project(&name, |redmine, project_id, _name| {
1276 let create_endpoint = super::CreateIssue::builder()
1277 .project_id(project_id)
1278 .subject("old test subject")
1279 .build()?;
1280 let IssueWrapper { issue }: IssueWrapper<Issue> =
1281 redmine.json_response_body::<_, _>(&create_endpoint)?;
1282 let update_endpoint = super::UpdateIssue::builder()
1283 .id(issue.id)
1284 .subject("New test subject")
1285 .build()?;
1286 redmine.ignore_response_body::<_>(&update_endpoint)?;
1287 Ok(())
1288 })?;
1289 Ok(())
1290 }
1291
1292 #[function_name::named]
1293 #[traced_test]
1294 #[test]
1295 fn test_delete_issue() -> Result<(), Box<dyn Error>> {
1296 let _w_issues = ISSUES_LOCK.write();
1297 let name = format!("unittest_{}", function_name!());
1298 with_project(&name, |redmine, project_id, _name| {
1299 let create_endpoint = super::CreateIssue::builder()
1300 .project_id(project_id)
1301 .subject("test subject")
1302 .build()?;
1303 let IssueWrapper { issue }: IssueWrapper<Issue> =
1304 redmine.json_response_body::<_, _>(&create_endpoint)?;
1305 let delete_endpoint = super::DeleteIssue::builder().id(issue.id).build()?;
1306 redmine.ignore_response_body::<_>(&delete_endpoint)?;
1307 Ok(())
1308 })?;
1309 Ok(())
1310 }
1311
1312 #[traced_test]
1317 #[test]
1318 fn test_completeness_issue_type_first_page() -> Result<(), Box<dyn Error>> {
1319 let _r_issues = ISSUES_LOCK.read();
1320 dotenvy::dotenv()?;
1321 let redmine = crate::api::Redmine::from_env()?;
1322 let endpoint = ListIssues::builder()
1323 .include(vec![
1324 IssueListInclude::Attachments,
1325 IssueListInclude::Relations,
1326 ])
1327 .build()?;
1328 let ResponsePage {
1329 values,
1330 total_count: _,
1331 offset: _,
1332 limit: _,
1333 } = redmine.json_response_body_page::<_, serde_json::Value>(&endpoint, 0, 100)?;
1334 for value in values {
1335 let o: Issue = serde_json::from_value(value.clone())?;
1336 let reserialized = serde_json::to_value(o)?;
1337 assert_eq!(value, reserialized);
1338 }
1339 Ok(())
1340 }
1341
1342 #[traced_test]
1351 #[test]
1352 #[ignore]
1353 fn test_completeness_issue_type_all_pages() -> Result<(), Box<dyn Error>> {
1354 let _r_issues = ISSUES_LOCK.read();
1355 dotenvy::dotenv()?;
1356 let redmine = crate::api::Redmine::from_env()?;
1357 let endpoint = ListIssues::builder()
1358 .include(vec![
1359 IssueListInclude::Attachments,
1360 IssueListInclude::Relations,
1361 ])
1362 .build()?;
1363 let values = redmine.json_response_body_all_pages::<_, serde_json::Value>(&endpoint)?;
1364 for value in values {
1365 let o: Issue = serde_json::from_value(value.clone())?;
1366 let reserialized = serde_json::to_value(o)?;
1367 assert_eq!(value, reserialized);
1368 }
1369 Ok(())
1370 }
1371
1372 #[traced_test]
1382 #[test]
1383 #[ignore]
1384 fn test_completeness_issue_type_all_pages_all_issue_details() -> Result<(), Box<dyn Error>> {
1385 let _r_issues = ISSUES_LOCK.read();
1386 dotenvy::dotenv()?;
1387 let redmine = crate::api::Redmine::from_env()?;
1388 let endpoint = ListIssues::builder()
1389 .include(vec![
1390 IssueListInclude::Attachments,
1391 IssueListInclude::Relations,
1392 ])
1393 .build()?;
1394 let issues = redmine.json_response_body_all_pages::<_, Issue>(&endpoint)?;
1395 for issue in issues {
1396 let get_endpoint = GetIssue::builder()
1397 .id(issue.id)
1398 .include(vec![
1399 IssueInclude::Attachments,
1400 IssueInclude::Children,
1401 IssueInclude::Changesets,
1402 IssueInclude::Relations,
1403 IssueInclude::Journals,
1404 IssueInclude::Watchers,
1405 ])
1406 .build()?;
1407 let IssueWrapper { issue: mut value } =
1408 redmine.json_response_body::<_, IssueWrapper<serde_json::Value>>(&get_endpoint)?;
1409 let o: Issue = serde_json::from_value(value.clone())?;
1410 let value_object = value.as_object_mut().unwrap();
1416 if value_object.get("total_estimated_hours") == Some(&serde_json::Value::Null) {
1417 value_object.remove("total_estimated_hours");
1418 }
1419 let reserialized = serde_json::to_value(o)?;
1420 assert_eq!(value, reserialized);
1421 }
1422 Ok(())
1423 }
1424}