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