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, 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 {}
878
879impl GetIssue {
880    #[must_use]
882    pub fn builder() -> GetIssueBuilder {
883        GetIssueBuilder::default()
884    }
885}
886
887impl Endpoint for GetIssue {
888    fn method(&self) -> Method {
889        Method::GET
890    }
891
892    fn endpoint(&self) -> Cow<'static, str> {
893        format!("issues/{}.json", &self.id).into()
894    }
895
896    fn parameters(&self) -> QueryParams {
897        let mut params = QueryParams::default();
898        params.push_opt("include", self.include.as_ref());
899        params
900    }
901}
902
903#[derive(Debug, Clone, Serialize, serde::Deserialize)]
905pub struct CustomField<'a> {
906    pub id: u64,
908    pub name: Option<Cow<'a, str>>,
910    pub value: Cow<'a, str>,
912}
913
914#[derive(Debug, Clone, Serialize)]
917pub struct UploadedAttachment<'a> {
918    pub token: Cow<'a, str>,
920    pub filename: Cow<'a, str>,
922    #[serde(skip_serializing_if = "Option::is_none")]
924    pub description: Option<Cow<'a, str>>,
925    pub content_type: Cow<'a, str>,
927}
928
929#[serde_with::skip_serializing_none]
931#[derive(Debug, Clone, Builder, Serialize)]
932#[builder(setter(strip_option))]
933pub struct CreateIssue<'a> {
934    project_id: u64,
936    #[builder(default)]
938    tracker_id: Option<u64>,
939    #[builder(default)]
941    status_id: Option<u64>,
942    #[builder(default)]
944    priority_id: Option<u64>,
945    #[builder(setter(into), default)]
947    subject: Option<Cow<'a, str>>,
948    #[builder(setter(into), default)]
950    description: Option<Cow<'a, str>>,
951    #[builder(default)]
953    category_id: Option<u64>,
954    #[builder(default, setter(name = "version"))]
956    fixed_version_id: Option<u64>,
957    #[builder(default)]
959    assigned_to_id: Option<u64>,
960    #[builder(default)]
962    parent_issue_id: Option<u64>,
963    #[builder(default)]
965    custom_fields: Option<Vec<CustomField<'a>>>,
966    #[builder(default)]
968    watcher_user_ids: Option<Vec<u64>>,
969    #[builder(default)]
971    is_private: Option<bool>,
972    #[builder(default)]
974    estimated_hours: Option<f64>,
975    #[builder(default)]
977    uploads: Option<Vec<UploadedAttachment<'a>>>,
978}
979
980impl<'a> CreateIssue<'a> {
981    #[must_use]
983    pub fn builder() -> CreateIssueBuilder<'a> {
984        CreateIssueBuilder::default()
985    }
986}
987
988impl ReturnsJsonResponse for CreateIssue<'_> {}
989
990impl Endpoint for CreateIssue<'_> {
991    fn method(&self) -> Method {
992        Method::POST
993    }
994
995    fn endpoint(&self) -> Cow<'static, str> {
996        "issues.json".into()
997    }
998
999    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
1000        Ok(Some((
1001            "application/json",
1002            serde_json::to_vec(&IssueWrapper::<CreateIssue> {
1003                issue: (*self).to_owned(),
1004            })?,
1005        )))
1006    }
1007}
1008
1009#[serde_with::skip_serializing_none]
1011#[derive(Debug, Clone, Builder, Serialize)]
1012#[builder(setter(strip_option))]
1013pub struct UpdateIssue<'a> {
1014    #[serde(skip_serializing)]
1016    id: u64,
1017    #[builder(default)]
1019    project_id: Option<u64>,
1020    #[builder(default)]
1022    tracker_id: Option<u64>,
1023    #[builder(default)]
1025    status_id: Option<u64>,
1026    #[builder(default)]
1028    priority_id: Option<u64>,
1029    #[builder(setter(into), default)]
1031    subject: Option<Cow<'a, str>>,
1032    #[builder(setter(into), default)]
1034    description: Option<Cow<'a, str>>,
1035    #[builder(default)]
1037    category_id: Option<u64>,
1038    #[builder(default, setter(name = "version"))]
1040    fixed_version_id: Option<u64>,
1041    #[builder(default)]
1043    assigned_to_id: Option<u64>,
1044    #[builder(default)]
1046    parent_issue_id: Option<u64>,
1047    #[builder(default)]
1049    custom_fields: Option<Vec<CustomField<'a>>>,
1050    #[builder(default)]
1052    watcher_user_ids: Option<Vec<u64>>,
1053    #[builder(default)]
1055    is_private: Option<bool>,
1056    #[builder(default)]
1058    estimated_hours: Option<f64>,
1059    #[builder(default)]
1061    notes: Option<Cow<'a, str>>,
1062    #[builder(default)]
1064    private_notes: Option<bool>,
1065    #[builder(default)]
1067    uploads: Option<Vec<UploadedAttachment<'a>>>,
1068}
1069
1070impl<'a> UpdateIssue<'a> {
1071    #[must_use]
1073    pub fn builder() -> UpdateIssueBuilder<'a> {
1074        UpdateIssueBuilder::default()
1075    }
1076}
1077
1078impl Endpoint for UpdateIssue<'_> {
1079    fn method(&self) -> Method {
1080        Method::PUT
1081    }
1082
1083    fn endpoint(&self) -> Cow<'static, str> {
1084        format!("issues/{}.json", self.id).into()
1085    }
1086
1087    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
1088        Ok(Some((
1089            "application/json",
1090            serde_json::to_vec(&IssueWrapper::<UpdateIssue> {
1091                issue: (*self).to_owned(),
1092            })?,
1093        )))
1094    }
1095}
1096
1097#[derive(Debug, Clone, Builder)]
1099#[builder(setter(strip_option))]
1100pub struct DeleteIssue {
1101    id: u64,
1103}
1104
1105impl DeleteIssue {
1106    #[must_use]
1108    pub fn builder() -> DeleteIssueBuilder {
1109        DeleteIssueBuilder::default()
1110    }
1111}
1112
1113impl Endpoint for DeleteIssue {
1114    fn method(&self) -> Method {
1115        Method::DELETE
1116    }
1117
1118    fn endpoint(&self) -> Cow<'static, str> {
1119        format!("issues/{}.json", &self.id).into()
1120    }
1121}
1122
1123#[derive(Debug, Clone, Builder, Serialize)]
1125#[builder(setter(strip_option))]
1126pub struct AddWatcher {
1127    #[serde(skip_serializing)]
1129    issue_id: u64,
1130    user_id: u64,
1132}
1133
1134impl AddWatcher {
1135    #[must_use]
1137    pub fn builder() -> AddWatcherBuilder {
1138        AddWatcherBuilder::default()
1139    }
1140}
1141
1142impl Endpoint for AddWatcher {
1143    fn method(&self) -> Method {
1144        Method::POST
1145    }
1146
1147    fn endpoint(&self) -> Cow<'static, str> {
1148        format!("issues/{}/watchers.json", &self.issue_id).into()
1149    }
1150
1151    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, crate::Error> {
1152        Ok(Some(("application/json", serde_json::to_vec(self)?)))
1153    }
1154}
1155
1156#[derive(Debug, Clone, Builder)]
1158#[builder(setter(strip_option))]
1159pub struct RemoveWatcher {
1160    issue_id: u64,
1162    user_id: u64,
1164}
1165
1166impl RemoveWatcher {
1167    #[must_use]
1169    pub fn builder() -> RemoveWatcherBuilder {
1170        RemoveWatcherBuilder::default()
1171    }
1172}
1173
1174impl Endpoint for RemoveWatcher {
1175    fn method(&self) -> Method {
1176        Method::DELETE
1177    }
1178
1179    fn endpoint(&self) -> Cow<'static, str> {
1180        format!("issues/{}/watchers/{}.json", &self.issue_id, &self.user_id).into()
1181    }
1182}
1183
1184#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
1186pub struct IssuesWrapper<T> {
1187    pub issues: Vec<T>,
1189}
1190
1191#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
1194pub struct IssueWrapper<T> {
1195    pub issue: T,
1197}
1198
1199#[cfg(test)]
1200pub(crate) mod test {
1201    use super::*;
1202    use crate::api::test_helpers::with_project;
1203    use pretty_assertions::assert_eq;
1204    use std::error::Error;
1205    use tokio::sync::RwLock;
1206    use tracing_test::traced_test;
1207
1208    pub static ISSUES_LOCK: RwLock<()> = RwLock::const_new(());
1211
1212    #[traced_test]
1213    #[test]
1214    fn test_list_issues_no_pagination() -> Result<(), Box<dyn Error>> {
1215        let _r_issues = ISSUES_LOCK.read();
1216        dotenvy::dotenv()?;
1217        let redmine = crate::api::Redmine::from_env()?;
1218        let endpoint = ListIssues::builder().build()?;
1219        redmine.json_response_body::<_, IssuesWrapper<Issue>>(&endpoint)?;
1220        Ok(())
1221    }
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        let endpoint = ListIssues::builder().build()?;
1230        redmine.json_response_body_page::<_, Issue>(&endpoint, 0, 25)?;
1231        Ok(())
1232    }
1233
1234    #[traced_test]
1238    #[test]
1239    #[ignore]
1240    fn test_list_issues_all_pages() -> Result<(), Box<dyn Error>> {
1241        let _r_issues = ISSUES_LOCK.read();
1242        dotenvy::dotenv()?;
1243        let redmine = crate::api::Redmine::from_env()?;
1244        let endpoint = ListIssues::builder().build()?;
1245        redmine.json_response_body_all_pages::<_, Issue>(&endpoint)?;
1246        Ok(())
1247    }
1248
1249    #[traced_test]
1250    #[test]
1251    fn test_get_issue() -> Result<(), Box<dyn Error>> {
1252        let _r_issues = ISSUES_LOCK.read();
1253        dotenvy::dotenv()?;
1254        let redmine = crate::api::Redmine::from_env()?;
1255        let endpoint = GetIssue::builder().id(40000).build()?;
1256        redmine.json_response_body::<_, IssueWrapper<Issue>>(&endpoint)?;
1257        Ok(())
1258    }
1259
1260    #[function_name::named]
1261    #[traced_test]
1262    #[test]
1263    fn test_create_issue() -> Result<(), Box<dyn Error>> {
1264        let _w_issues = ISSUES_LOCK.write();
1265        let name = format!("unittest_{}", function_name!());
1266        with_project(&name, |redmine, project_id, _| {
1267            let create_endpoint = super::CreateIssue::builder()
1268                .project_id(project_id)
1269                .subject("old test subject")
1270                .build()?;
1271            redmine.json_response_body::<_, IssueWrapper<Issue>>(&create_endpoint)?;
1272            Ok(())
1273        })?;
1274        Ok(())
1275    }
1276
1277    #[function_name::named]
1278    #[traced_test]
1279    #[test]
1280    fn test_update_issue() -> Result<(), Box<dyn Error>> {
1281        let _w_issues = ISSUES_LOCK.write();
1282        let name = format!("unittest_{}", function_name!());
1283        with_project(&name, |redmine, project_id, _name| {
1284            let create_endpoint = super::CreateIssue::builder()
1285                .project_id(project_id)
1286                .subject("old test subject")
1287                .build()?;
1288            let IssueWrapper { issue }: IssueWrapper<Issue> =
1289                redmine.json_response_body::<_, _>(&create_endpoint)?;
1290            let update_endpoint = super::UpdateIssue::builder()
1291                .id(issue.id)
1292                .subject("New test subject")
1293                .build()?;
1294            redmine.ignore_response_body::<_>(&update_endpoint)?;
1295            Ok(())
1296        })?;
1297        Ok(())
1298    }
1299
1300    #[function_name::named]
1301    #[traced_test]
1302    #[test]
1303    fn test_delete_issue() -> Result<(), Box<dyn Error>> {
1304        let _w_issues = ISSUES_LOCK.write();
1305        let name = format!("unittest_{}", function_name!());
1306        with_project(&name, |redmine, project_id, _name| {
1307            let create_endpoint = super::CreateIssue::builder()
1308                .project_id(project_id)
1309                .subject("test subject")
1310                .build()?;
1311            let IssueWrapper { issue }: IssueWrapper<Issue> =
1312                redmine.json_response_body::<_, _>(&create_endpoint)?;
1313            let delete_endpoint = super::DeleteIssue::builder().id(issue.id).build()?;
1314            redmine.ignore_response_body::<_>(&delete_endpoint)?;
1315            Ok(())
1316        })?;
1317        Ok(())
1318    }
1319
1320    #[traced_test]
1325    #[test]
1326    fn test_completeness_issue_type() -> Result<(), Box<dyn Error>> {
1327        let _r_issues = ISSUES_LOCK.read();
1328        dotenvy::dotenv()?;
1329        let redmine = crate::api::Redmine::from_env()?;
1330        let endpoint = ListIssues::builder()
1331            .include(vec![
1332                IssueListInclude::Attachments,
1333                IssueListInclude::Relations,
1334            ])
1335            .build()?;
1336        let IssuesWrapper { issues: values } =
1337            redmine.json_response_body::<_, IssuesWrapper<serde_json::Value>>(&endpoint)?;
1338        for value in values {
1339            let o: Issue = serde_json::from_value(value.clone())?;
1340            let reserialized = serde_json::to_value(o)?;
1341            assert_eq!(value, reserialized);
1342        }
1343        Ok(())
1344    }
1345
1346    #[traced_test]
1355    #[test]
1356    #[ignore]
1357    fn test_completeness_issue_type_all_pages() -> Result<(), Box<dyn Error>> {
1358        let _r_issues = ISSUES_LOCK.read();
1359        dotenvy::dotenv()?;
1360        let redmine = crate::api::Redmine::from_env()?;
1361        let endpoint = ListIssues::builder()
1362            .include(vec![
1363                IssueListInclude::Attachments,
1364                IssueListInclude::Relations,
1365            ])
1366            .build()?;
1367        let values = redmine.json_response_body_all_pages::<_, serde_json::Value>(&endpoint)?;
1368        for value in values {
1369            let o: Issue = serde_json::from_value(value.clone())?;
1370            let reserialized = serde_json::to_value(o)?;
1371            assert_eq!(value, reserialized);
1372        }
1373        Ok(())
1374    }
1375
1376    #[traced_test]
1386    #[test]
1387    #[ignore]
1388    fn test_completeness_issue_type_all_pages_all_issue_details() -> Result<(), Box<dyn Error>> {
1389        let _r_issues = ISSUES_LOCK.read();
1390        dotenvy::dotenv()?;
1391        let redmine = crate::api::Redmine::from_env()?;
1392        let endpoint = ListIssues::builder()
1393            .include(vec![
1394                IssueListInclude::Attachments,
1395                IssueListInclude::Relations,
1396            ])
1397            .build()?;
1398        let issues = redmine.json_response_body_all_pages::<_, Issue>(&endpoint)?;
1399        for issue in issues {
1400            let get_endpoint = GetIssue::builder()
1401                .id(issue.id)
1402                .include(vec![
1403                    IssueInclude::Attachments,
1404                    IssueInclude::Children,
1405                    IssueInclude::Changesets,
1406                    IssueInclude::Relations,
1407                    IssueInclude::Journals,
1408                    IssueInclude::Watchers,
1409                ])
1410                .build()?;
1411            let IssueWrapper { issue: mut value } =
1412                redmine.json_response_body::<_, IssueWrapper<serde_json::Value>>(&get_endpoint)?;
1413            let o: Issue = serde_json::from_value(value.clone())?;
1414            let value_object = value.as_object_mut().unwrap();
1420            if value_object.get("total_estimated_hours") == Some(&serde_json::Value::Null) {
1421                value_object.remove("total_estimated_hours");
1422            }
1423            let reserialized = serde_json::to_value(o)?;
1424            assert_eq!(value, reserialized);
1425        }
1426        Ok(())
1427    }
1428}