Skip to main content

gitlab/api/
issues.rs

1// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4// option. This file may not be copied, modified, or distributed
5// except according to those terms.
6
7#![allow(clippy::module_inception)]
8
9//! Issue API endpoints and types.
10//!
11//! These endpoints are used for querying issues from projects, groups or the whole instance.
12
13use std::collections::BTreeSet;
14
15use crate::api::endpoint_prelude::*;
16use crate::api::ParamValue;
17
18pub use groups::{GroupIssues, GroupIssuesBuilder, GroupIssuesBuilderError};
19pub use issues::{Issues, IssuesBuilder, IssuesBuilderError};
20pub use projects::{ProjectIssues, ProjectIssuesBuilder, ProjectIssuesBuilderError};
21
22mod groups;
23mod issues;
24mod projects;
25
26/// Filters for issue states.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28#[non_exhaustive]
29pub enum IssueState {
30    /// Filter issues that are open.
31    Opened,
32    /// Filter issues that are closed.
33    Closed,
34}
35
36impl IssueState {
37    fn as_str(self) -> &'static str {
38        match self {
39            IssueState::Opened => "opened",
40            IssueState::Closed => "closed",
41        }
42    }
43}
44
45impl ParamValue<'static> for IssueState {
46    fn as_value(&self) -> Cow<'static, str> {
47        self.as_str().into()
48    }
49}
50
51/// Filter issues by a scope.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53#[non_exhaustive]
54pub enum IssueScope {
55    /// Filter issues created by the API caller.
56    CreatedByMe,
57    /// Filter issues assigned to the API caller.
58    AssignedToMe,
59    /// Return all issues.
60    All,
61}
62
63impl IssueScope {
64    fn as_str(self) -> &'static str {
65        match self {
66            IssueScope::CreatedByMe => "created_by_me",
67            IssueScope::AssignedToMe => "assigned_to_me",
68            IssueScope::All => "all",
69        }
70    }
71}
72
73impl ParamValue<'static> for IssueScope {
74    fn as_value(&self) -> Cow<'static, str> {
75        self.as_str().into()
76    }
77}
78
79/// Types of issues.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81#[non_exhaustive]
82pub enum IssueType {
83    /// Regular issues.
84    Issue,
85    /// Incident reports.
86    Incident,
87    /// Test case issues.
88    TestCase,
89    /// Tasks.
90    Task,
91}
92
93impl IssueType {
94    fn as_str(self) -> &'static str {
95        match self {
96            IssueType::Issue => "issue",
97            IssueType::Incident => "incident",
98            IssueType::TestCase => "test_case",
99            IssueType::Task => "task",
100        }
101    }
102}
103
104impl ParamValue<'static> for IssueType {
105    fn as_value(&self) -> Cow<'static, str> {
106        self.as_str().into()
107    }
108}
109
110/// Filter values by epic status.
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112#[non_exhaustive]
113pub enum IssueEpic {
114    /// Issues without any epic.
115    None,
116    /// Issues with any epic association.
117    Any,
118    /// Issues with a given epic (by ID).
119    Id(u64),
120}
121
122impl IssueEpic {
123    fn as_str(self) -> Cow<'static, str> {
124        match self {
125            IssueEpic::None => "None".into(),
126            IssueEpic::Any => "Any".into(),
127            IssueEpic::Id(id) => format!("{id}").into(),
128        }
129    }
130}
131
132impl From<u64> for IssueEpic {
133    fn from(id: u64) -> Self {
134        Self::Id(id)
135    }
136}
137
138impl ParamValue<'static> for IssueEpic {
139    fn as_value(&self) -> Cow<'static, str> {
140        self.as_str()
141    }
142}
143
144/// Health statuses of issues.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146#[non_exhaustive]
147pub enum IssueHealthStatus {
148    /// Issues with any health status.
149    Any,
150    /// Issues without a health status.
151    None,
152    /// Issues that are on track.
153    OnTrack,
154    /// Issues that need attention.
155    NeedsAttention,
156    /// Issues that are at risk.
157    AtRisk,
158}
159
160impl IssueHealthStatus {
161    fn as_str(self) -> &'static str {
162        match self {
163            IssueHealthStatus::Any => "Any",
164            IssueHealthStatus::None => "None",
165            IssueHealthStatus::OnTrack => "on_track",
166            IssueHealthStatus::NeedsAttention => "needs_attention",
167            IssueHealthStatus::AtRisk => "at_risk",
168        }
169    }
170}
171
172impl ParamValue<'static> for IssueHealthStatus {
173    fn as_value(&self) -> Cow<'static, str> {
174        self.as_str().into()
175    }
176}
177
178/// Filter values for issue iteration values.
179#[derive(Debug, Clone, PartialEq, Eq)]
180#[non_exhaustive]
181pub enum IssueIteration<'a> {
182    /// Issues without any iteration.
183    None,
184    /// Issues with any iteration association.
185    Any,
186    /// Issues with a given iteration (by ID).
187    Id(u64),
188    /// Issues with a tiven iteration (by title).
189    Title(Cow<'a, str>),
190}
191
192impl IssueIteration<'_> {
193    fn add_params<'b>(&'b self, params: &mut QueryParams<'b>) {
194        match self {
195            IssueIteration::None => {
196                params.push("iteration_id", "None");
197            },
198            IssueIteration::Any => {
199                params.push("iteration_id", "Any");
200            },
201            IssueIteration::Id(id) => {
202                params.push("iteration_id", *id);
203            },
204            IssueIteration::Title(title) => {
205                params.push("iteration_title", title);
206            },
207        }
208    }
209}
210
211#[derive(Debug, Clone)]
212#[non_exhaustive]
213enum Assignee<'a> {
214    Assigned,
215    Unassigned,
216    Id(u64),
217    Usernames(BTreeSet<Cow<'a, str>>),
218}
219
220impl Assignee<'_> {
221    fn add_params<'b>(&'b self, params: &mut QueryParams<'b>) {
222        match self {
223            Assignee::Assigned => {
224                params.push("assignee_id", "Any");
225            },
226            Assignee::Unassigned => {
227                params.push("assignee_id", "None");
228            },
229            Assignee::Id(id) => {
230                params.push("assignee_id", *id);
231            },
232            Assignee::Usernames(usernames) => {
233                params.extend(usernames.iter().map(|value| ("assignee_username[]", value)));
234            },
235        }
236    }
237}
238
239/// Filter issues by weight.
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241#[non_exhaustive]
242pub enum IssueWeight {
243    /// Filter issues with any weight.
244    Any,
245    /// Filter issues with no weight assigned.
246    None,
247    /// Filter issues with a specific weight.
248    Weight(u64),
249}
250
251impl IssueWeight {
252    fn as_str(self) -> Cow<'static, str> {
253        match self {
254            IssueWeight::Any => "Any".into(),
255            IssueWeight::None => "None".into(),
256            IssueWeight::Weight(weight) => weight.to_string().into(),
257        }
258    }
259}
260
261impl ParamValue<'static> for IssueWeight {
262    fn as_value(&self) -> Cow<'static, str> {
263        self.as_str()
264    }
265}
266
267/// The scope to apply search query terms to.
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269#[non_exhaustive]
270pub enum IssueSearchScope {
271    /// Search within titles.
272    Title,
273    /// Search within descriptions.
274    Description,
275}
276
277impl IssueSearchScope {
278    fn as_str(self) -> &'static str {
279        match self {
280            IssueSearchScope::Title => "title",
281            IssueSearchScope::Description => "description",
282        }
283    }
284}
285
286impl ParamValue<'static> for IssueSearchScope {
287    fn as_value(&self) -> Cow<'static, str> {
288        self.as_str().into()
289    }
290}
291
292/// Filter values for due dates.
293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294#[non_exhaustive]
295pub enum IssueDueDateFilter {
296    /// Issues without a due date.
297    None,
298    /// Issues with any a due date.
299    Any,
300    /// Issues due today.
301    Today,
302    /// Issues due tomorrow.
303    Tomorrow,
304    /// Issues due this week.
305    ThisWeek,
306    /// Issues due this month.
307    ThisMonth,
308    /// Issues due between two weeks ago and a month from now.
309    BetweenTwoWeeksAgoAndNextMonth,
310    /// Issues which are overdue.
311    Overdue,
312}
313
314impl IssueDueDateFilter {
315    fn as_str(self) -> &'static str {
316        match self {
317            IssueDueDateFilter::None => "0",
318            IssueDueDateFilter::Any => "any",
319            IssueDueDateFilter::Today => "today",
320            IssueDueDateFilter::Tomorrow => "tomorrow",
321            IssueDueDateFilter::ThisWeek => "week",
322            IssueDueDateFilter::ThisMonth => "month",
323            IssueDueDateFilter::BetweenTwoWeeksAgoAndNextMonth => {
324                "next_month_and_previous_two_weeks"
325            },
326            IssueDueDateFilter::Overdue => "overdue",
327        }
328    }
329}
330
331impl ParamValue<'static> for IssueDueDateFilter {
332    fn as_value(&self) -> Cow<'static, str> {
333        self.as_str().into()
334    }
335}
336
337/// Keys issue results may be ordered by.
338#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
339#[non_exhaustive]
340pub enum IssueOrderBy {
341    /// Sort by creation date.
342    #[default]
343    CreatedAt,
344    /// Sort by last updated date.
345    UpdatedAt,
346    /// Sort by priority.
347    Priority,
348    /// Sort by due date.
349    DueDate,
350    /// Sort by relative position.
351    ///
352    /// TODO: position within what?
353    RelativePosition,
354    /// Sort by priority labels.
355    LabelPriority,
356    /// Sort by milestone due date.
357    MilestoneDue,
358    /// Sort by popularity.
359    Popularity,
360    /// Sort by weight.
361    Weight,
362    /// Sort by type.
363    Title,
364}
365
366impl IssueOrderBy {
367    fn as_str(self) -> &'static str {
368        match self {
369            IssueOrderBy::CreatedAt => "created_at",
370            IssueOrderBy::UpdatedAt => "updated_at",
371            IssueOrderBy::Priority => "priority",
372            IssueOrderBy::DueDate => "due_date",
373            IssueOrderBy::RelativePosition => "relative_position",
374            IssueOrderBy::LabelPriority => "label_priority",
375            IssueOrderBy::MilestoneDue => "milestone_due",
376            IssueOrderBy::Popularity => "popularity",
377            IssueOrderBy::Title => "title",
378            IssueOrderBy::Weight => "weight",
379        }
380    }
381}
382
383impl ParamValue<'static> for IssueOrderBy {
384    fn as_value(&self) -> Cow<'static, str> {
385        self.as_str().into()
386    }
387}
388
389/// Filters available for issue milestones.
390#[derive(Debug, Clone)]
391#[non_exhaustive]
392pub enum IssueMilestone<'a> {
393    /// Issues without any milestone.
394    None,
395    /// Issues with any milestone.
396    Any,
397    /// Issues with milestones with upcoming due dates.
398    Upcoming,
399    /// Issues with milestones that have started.
400    Started,
401    /// Issues with a specific milestone.
402    Named(Cow<'a, str>),
403}
404
405impl<'a> IssueMilestone<'a> {
406    /// Create a named issue milestone filter.
407    pub fn named<N>(name: N) -> Self
408    where
409        N: Into<Cow<'a, str>>,
410    {
411        Self::Named(name.into())
412    }
413
414    fn as_str(&self) -> &str {
415        match self {
416            IssueMilestone::None => "None",
417            IssueMilestone::Any => "Any",
418            IssueMilestone::Upcoming => "Upcoming",
419            IssueMilestone::Started => "Started",
420            IssueMilestone::Named(name) => name.as_ref(),
421        }
422    }
423}
424
425impl<'a, 'b: 'a> ParamValue<'a> for &'b IssueMilestone<'a> {
426    fn as_value(&self) -> Cow<'a, str> {
427        self.as_str().into()
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use crate::api::issues::{
434        IssueDueDateFilter, IssueEpic, IssueHealthStatus, IssueMilestone, IssueOrderBy, IssueScope,
435        IssueSearchScope, IssueState, IssueType, IssueWeight,
436    };
437
438    #[test]
439    fn issue_state_as_str() {
440        let items = &[
441            (IssueState::Opened, "opened"),
442            (IssueState::Closed, "closed"),
443        ];
444
445        for (i, s) in items {
446            assert_eq!(i.as_str(), *s);
447        }
448    }
449
450    #[test]
451    fn issue_scope_as_str() {
452        let items = &[
453            (IssueScope::CreatedByMe, "created_by_me"),
454            (IssueScope::AssignedToMe, "assigned_to_me"),
455            (IssueScope::All, "all"),
456        ];
457
458        for (i, s) in items {
459            assert_eq!(i.as_str(), *s);
460        }
461    }
462
463    #[test]
464    fn issue_epic_from_u64() {
465        let items = &[(IssueEpic::Id(4), 4.into())];
466
467        for (i, s) in items {
468            assert_eq!(i, s);
469        }
470    }
471
472    #[test]
473    fn issue_epic_as_str() {
474        let items = &[
475            (IssueEpic::None, "None"),
476            (IssueEpic::Any, "Any"),
477            (IssueEpic::Id(4), "4"),
478        ];
479
480        for (i, s) in items {
481            assert_eq!(i.as_str(), *s);
482        }
483    }
484
485    #[test]
486    fn issue_health_status_as_str() {
487        let items = &[
488            (IssueHealthStatus::OnTrack, "on_track"),
489            (IssueHealthStatus::NeedsAttention, "needs_attention"),
490            (IssueHealthStatus::AtRisk, "at_risk"),
491        ];
492
493        for (i, s) in items {
494            assert_eq!(i.as_str(), *s);
495        }
496    }
497
498    #[test]
499    fn issue_type_as_str() {
500        let items = &[
501            (IssueType::Issue, "issue"),
502            (IssueType::Incident, "incident"),
503            (IssueType::TestCase, "test_case"),
504            (IssueType::Task, "task"),
505        ];
506
507        for (i, s) in items {
508            assert_eq!(i.as_str(), *s);
509        }
510    }
511
512    #[test]
513    fn issue_weight_as_str() {
514        let items = &[
515            (IssueWeight::Any, "Any"),
516            (IssueWeight::None, "None"),
517            (IssueWeight::Weight(0), "0"),
518        ];
519
520        for (i, s) in items {
521            assert_eq!(i.as_str(), *s);
522        }
523    }
524
525    #[test]
526    fn issue_search_scope_as_str() {
527        let items = &[
528            (IssueSearchScope::Title, "title"),
529            (IssueSearchScope::Description, "description"),
530        ];
531
532        for (i, s) in items {
533            assert_eq!(i.as_str(), *s);
534        }
535    }
536
537    #[test]
538    fn issue_due_date_filter_as_str() {
539        let items = &[
540            (IssueDueDateFilter::None, "0"),
541            (IssueDueDateFilter::Any, "any"),
542            (IssueDueDateFilter::Today, "today"),
543            (IssueDueDateFilter::Tomorrow, "tomorrow"),
544            (IssueDueDateFilter::ThisWeek, "week"),
545            (IssueDueDateFilter::ThisMonth, "month"),
546            (
547                IssueDueDateFilter::BetweenTwoWeeksAgoAndNextMonth,
548                "next_month_and_previous_two_weeks",
549            ),
550            (IssueDueDateFilter::Overdue, "overdue"),
551        ];
552
553        for (i, s) in items {
554            assert_eq!(i.as_str(), *s);
555        }
556    }
557
558    #[test]
559    fn issue_order_by_default() {
560        assert_eq!(IssueOrderBy::default(), IssueOrderBy::CreatedAt);
561    }
562
563    #[test]
564    fn issue_order_by_as_str() {
565        let items = &[
566            (IssueOrderBy::CreatedAt, "created_at"),
567            (IssueOrderBy::UpdatedAt, "updated_at"),
568            (IssueOrderBy::Priority, "priority"),
569            (IssueOrderBy::DueDate, "due_date"),
570            (IssueOrderBy::RelativePosition, "relative_position"),
571            (IssueOrderBy::LabelPriority, "label_priority"),
572            (IssueOrderBy::MilestoneDue, "milestone_due"),
573            (IssueOrderBy::Popularity, "popularity"),
574            (IssueOrderBy::Weight, "weight"),
575            (IssueOrderBy::Title, "title"),
576        ];
577
578        for (i, s) in items {
579            assert_eq!(i.as_str(), *s);
580        }
581    }
582
583    #[test]
584    fn issue_milestone_as_str() {
585        let items = &[
586            (IssueMilestone::Any, "Any"),
587            (IssueMilestone::None, "None"),
588            (IssueMilestone::Upcoming, "Upcoming"),
589            (IssueMilestone::Started, "Started"),
590            (IssueMilestone::Named("milestone".into()), "milestone"),
591        ];
592
593        for (i, s) in items {
594            assert_eq!(i.as_str(), *s);
595        }
596    }
597}