hubcaps_ex/
issues.rs

1//! Issues interface
2use std::collections::HashMap;
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6use url::form_urlencoded;
7
8use crate::comments::Comments;
9use crate::labels::Label;
10use crate::users::User;
11use crate::utils::{percent_encode, PATH_SEGMENT};
12use crate::{Future, Github, SortDirection, Stream};
13use crate::milestone::Milestone;
14
15/// enum representation of github pull and issue state
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub enum State {
18    /// Only open issues
19    Open,
20    /// Only closed issues
21    Closed,
22    /// All issues, open or closed
23    All,
24}
25
26impl fmt::Display for State {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match *self {
29            State::Open => "open",
30            State::Closed => "closed",
31            State::All => "all",
32        }
33        .fmt(f)
34    }
35}
36
37impl Default for State {
38    fn default() -> State {
39        State::Open
40    }
41}
42
43/// Sort options available for github issues
44#[derive(Clone, Copy, Debug, PartialEq)]
45pub enum Sort {
46    /// sort by creation time of issue
47    Created,
48    /// sort by the last time issue was updated
49    Updated,
50    /// sort by number of comments
51    Comments,
52}
53
54impl fmt::Display for Sort {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        match *self {
57            Sort::Created => "created",
58            Sort::Updated => "updated",
59            Sort::Comments => "comments",
60        }
61        .fmt(f)
62    }
63}
64
65impl Default for Sort {
66    fn default() -> Sort {
67        Sort::Created
68    }
69}
70
71/// Provides access to assignee operations available for an individual issue
72pub struct IssueAssignees {
73    github: Github,
74    owner: String,
75    repo: String,
76    number: u64,
77}
78
79impl IssueAssignees {
80    #[doc(hidden)]
81    pub fn new<O, R>(github: Github, owner: O, repo: R, number: u64) -> Self
82    where
83        O: Into<String>,
84        R: Into<String>,
85    {
86        IssueAssignees {
87            github,
88            owner: owner.into(),
89            repo: repo.into(),
90            number,
91        }
92    }
93
94    fn path(&self, more: &str) -> String {
95        format!(
96            "/repos/{}/{}/issues/{}/assignees{}",
97            self.owner, self.repo, self.number, more
98        )
99    }
100
101    /// add a set of assignees
102    pub fn add(&self, assignees: Vec<&str>) -> Future<Issue> {
103        self.github
104            .post(&self.path(""), json_lit!({ "assignees": assignees }))
105    }
106}
107
108/// Provides access to label operations available for an individual issue
109pub struct IssueLabels {
110    github: Github,
111    owner: String,
112    repo: String,
113    number: u64,
114}
115
116impl IssueLabels {
117    #[doc(hidden)]
118    pub fn new<O, R>(github: Github, owner: O, repo: R, number: u64) -> Self
119    where
120        O: Into<String>,
121        R: Into<String>,
122    {
123        IssueLabels {
124            github,
125            owner: owner.into(),
126            repo: repo.into(),
127            number,
128        }
129    }
130
131    fn path(&self, more: &str) -> String {
132        format!(
133            "/repos/{}/{}/issues/{}/labels{}",
134            self.owner, self.repo, self.number, more
135        )
136    }
137
138    /// add a set of labels to this issue ref
139    #[allow(clippy::needless_pass_by_value)] // shipped public API
140    pub fn add(&self, labels: Vec<&str>) -> Future<Vec<Label>> {
141        self.github.post(&self.path(""), json!(labels))
142    }
143
144    /// remove a label from this issue
145    pub fn remove(&self, label: &str) -> Future<()> {
146        let label = percent_encode(label.as_ref(), PATH_SEGMENT);
147        self.github.delete(&self.path(&format!("/{}", label)))
148    }
149
150    /// replace all labels associated with this issue with a new set.
151    /// providing an empty set of labels is the same as clearing the
152    /// current labels
153    #[allow(clippy::needless_pass_by_value)] // shipped public API
154    pub fn set(&self, labels: Vec<&str>) -> Future<Vec<Label>> {
155        self.github.put(&self.path(""), json!(labels))
156    }
157
158    /// remove all labels from an issue
159    pub fn clear(&self) -> Future<()> {
160        self.github.delete(&self.path(""))
161    }
162}
163
164/// Provides access to operations available for a single issue
165/// Typically accessed from `github.repo(.., ..).issues().get(number)`
166pub struct IssueRef {
167    github: Github,
168    owner: String,
169    repo: String,
170    number: u64,
171}
172
173impl IssueRef {
174    #[doc(hidden)]
175    pub fn new<O, R>(github: Github, owner: O, repo: R, number: u64) -> Self
176    where
177        O: Into<String>,
178        R: Into<String>,
179    {
180        IssueRef {
181            github,
182            owner: owner.into(),
183            repo: repo.into(),
184            number,
185        }
186    }
187
188    /// Request an issue's information
189    pub fn get(&self) -> Future<Issue> {
190        self.github.get(&self.path(""))
191    }
192
193    fn path(&self, more: &str) -> String {
194        format!(
195            "/repos/{}/{}/issues/{}{}",
196            self.owner, self.repo, self.number, more
197        )
198    }
199
200    /// Return a reference to labels operations available for this issue
201    pub fn labels(&self) -> IssueLabels {
202        IssueLabels::new(
203            self.github.clone(),
204            self.owner.as_str(),
205            self.repo.as_str(),
206            self.number,
207        )
208    }
209
210    /// Return a reference to assignee operations available for this issue
211    pub fn assignees(&self) -> IssueAssignees {
212        IssueAssignees::new(
213            self.github.clone(),
214            self.owner.as_str(),
215            self.repo.as_str(),
216            self.number,
217        )
218    }
219
220    /// Edit the issues options
221    pub fn edit(&self, is: &IssueOptions) -> Future<Issue> {
222        self.github.patch(&self.path(""), json!(is))
223    }
224
225    /// Return a reference to comment operations available for this issue
226    pub fn comments(&self) -> Comments {
227        Comments::new(
228            self.github.clone(),
229            self.owner.clone(),
230            self.repo.clone(),
231            self.number,
232        )
233    }
234}
235
236/// Provides access to operations available for a repository issues
237/// Typically accessed via `github.repo(..., ...).issues()`
238pub struct Issues {
239    github: Github,
240    owner: String,
241    repo: String,
242}
243
244impl Issues {
245    /// create a new instance of a github repo issue ref
246    pub fn new<O, R>(github: Github, owner: O, repo: R) -> Self
247    where
248        O: Into<String>,
249        R: Into<String>,
250    {
251        Issues {
252            github,
253            owner: owner.into(),
254            repo: repo.into(),
255        }
256    }
257
258    fn path(&self, more: &str) -> String {
259        format!("/repos/{}/{}/issues{}", self.owner, self.repo, more)
260    }
261
262    pub fn get(&self, number: u64) -> IssueRef {
263        IssueRef::new(
264            self.github.clone(),
265            self.owner.as_str(),
266            self.repo.as_str(),
267            number,
268        )
269    }
270
271    pub fn create(&self, is: &IssueOptions) -> Future<Issue> {
272        self.github.post(&self.path(""), json!(is))
273    }
274
275    pub fn update(&self, number: &u64, is: &IssueOptions) -> Future<Issue> {
276        self.github.patch(&self.path(&format!("/{}", number)), json!(is))
277    }
278
279    /// Return the first page of issues for this repisotiry
280    /// See the [github docs](https://developer.github.com/v3/issues/#list-issues-for-a-repository)
281    /// for more information
282    pub fn list(&self, options: &IssueListOptions) -> Future<Vec<Issue>> {
283        let mut uri = vec![self.path("")];
284        if let Some(query) = options.serialize() {
285            uri.push(query);
286        }
287        self.github.get(&uri.join("?"))
288    }
289
290    /// Return a stream of all issues for this repository
291    ///
292    /// See the [github docs](https://developer.github.com/v3/issues/#list-issues-for-a-repository)
293    /// for more information
294    ///
295    /// Note: You'll typically want to use a `IssueListOptions` with a `per_page`
296    /// of 100 for maximum api credential rate limit efficency
297    pub fn iter(&self, options: &IssueListOptions) -> Stream<Issue> {
298        let mut uri = vec![self.path("")];
299        if let Some(query) = options.serialize() {
300            uri.push(query);
301        }
302        self.github.get_stream(&uri.join("?"))
303    }
304}
305
306// representations
307
308/// Options used to filter repository issue listings
309///
310/// See the [github docs](https://developer.github.com/v3/issues/#list-issues-for-a-repository)
311/// for more information
312///
313/// By default this returns up to `30` items. You can
314/// request up to `100` using the [per_page](https://developer.github.com/v3/#pagination)
315/// parameter
316#[derive(Default)]
317pub struct IssueListOptions {
318    params: HashMap<&'static str, String>,
319}
320
321impl IssueListOptions {
322    pub fn builder() -> IssueListOptionsBuilder {
323        IssueListOptionsBuilder::default()
324    }
325
326    pub fn serialize(&self) -> Option<String> {
327        if self.params.is_empty() {
328            None
329        } else {
330            let encoded: String = form_urlencoded::Serializer::new(String::new())
331                .extend_pairs(&self.params)
332                .finish();
333            Some(encoded)
334        }
335    }
336}
337
338/// a mutable issue list builder
339#[derive(Default)]
340pub struct IssueListOptionsBuilder(IssueListOptions);
341
342impl IssueListOptionsBuilder {
343    pub fn state(&mut self, state: State) -> &mut Self {
344        self.0.params.insert("state", state.to_string());
345        self
346    }
347
348    pub fn sort(&mut self, sort: Sort) -> &mut Self {
349        self.0.params.insert("sort", sort.to_string());
350        self
351    }
352
353    pub fn asc(&mut self) -> &mut Self {
354        self.direction(SortDirection::Asc)
355    }
356
357    pub fn desc(&mut self) -> &mut Self {
358        self.direction(SortDirection::Desc)
359    }
360
361    pub fn direction(&mut self, direction: SortDirection) -> &mut Self {
362        self.0.params.insert("direction", direction.to_string());
363        self
364    }
365
366    pub fn assignee<A>(&mut self, assignee: A) -> &mut Self
367    where
368        A: Into<String>,
369    {
370        self.0.params.insert("assignee", assignee.into());
371        self
372    }
373
374    pub fn creator<C>(&mut self, creator: C) -> &mut Self
375    where
376        C: Into<String>,
377    {
378        self.0.params.insert("creator", creator.into());
379        self
380    }
381
382    pub fn mentioned<M>(&mut self, mentioned: M) -> &mut Self
383    where
384        M: Into<String>,
385    {
386        self.0.params.insert("mentioned", mentioned.into());
387        self
388    }
389
390    pub fn labels<L>(&mut self, labels: Vec<L>) -> &mut Self
391    where
392        L: Into<String>,
393    {
394        self.0.params.insert(
395            "labels",
396            labels
397                .into_iter()
398                .map(|l| l.into())
399                .collect::<Vec<_>>()
400                .join(","),
401        );
402        self
403    }
404
405    pub fn since<S>(&mut self, since: S) -> &mut Self
406    where
407        S: Into<String>,
408    {
409        self.0.params.insert("since", since.into());
410        self
411    }
412
413    pub fn per_page(&mut self, n: u32) -> &mut Self {
414        self.0.params.insert("per_page", n.to_string());
415        self
416    }
417
418    pub fn build(&self) -> IssueListOptions {
419        IssueListOptions {
420            params: self.0.params.clone(),
421        }
422    }
423}
424
425#[derive(Debug, Serialize)]
426pub struct IssueOptions {
427    pub title: String,
428    #[serde(skip_serializing_if = "Option::is_none")]
429    pub body: Option<String>,
430    #[serde(skip_serializing_if = "Option::is_none")]
431    pub assignee: Option<String>,
432    #[serde(skip_serializing_if = "Option::is_none")]
433    pub assignees: Option<Vec<String>>,
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub milestone: Option<u64>,
436    pub labels: Vec<String>,
437    pub state: String,
438}
439
440impl IssueOptions {
441    pub fn new<T, B, A, L>(
442        title: T,
443        body: Option<B>,
444        assignee: Option<A>,
445        milestone: Option<u64>,
446        labels: Vec<L>,
447    ) -> IssueOptions
448    where
449        T: Into<String>,
450        B: Into<String>,
451        A: Into<String>,
452        L: Into<String>,
453    {
454        IssueOptions {
455            title: title.into(),
456            body: body.map(|b| b.into()),
457            assignee: assignee.map(|a| a.into()),
458            assignees: None,
459            milestone,
460            labels: labels
461                .into_iter()
462                .map(|l| l.into())
463                .collect::<Vec<String>>(),
464            state: "open".to_string()
465        }
466    }
467}
468
469#[derive(Debug, Deserialize, Serialize)]
470pub struct Issue {
471    pub id: u64,
472    pub url: String,
473    pub repository_url: String,
474    pub labels_url: String,
475    pub comments_url: String,
476    pub events_url: String,
477    pub html_url: String,
478    pub number: u64,
479    pub state: String,
480    pub title: String,
481    pub body: Option<String>,
482    pub user: User,
483    pub labels: Vec<Label>,
484    pub assignee: Option<User>,
485    pub locked: bool,
486    pub comments: u64,
487    pub pull_request: Option<PullRef>,
488    pub closed_at: Option<String>,
489    pub created_at: String,
490    pub updated_at: String,
491    pub assignees: Vec<User>,
492    pub milestone: Option<Milestone>,
493}
494
495/// A reference to a pull request.
496#[derive(Debug, Deserialize, Serialize)]
497pub struct PullRef {
498    pub url: String,
499    pub html_url: String,
500    pub diff_url: String,
501    pub patch_url: String,
502}
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507
508    #[test]
509    fn default_state() {
510        let default: State = Default::default();
511        assert_eq!(default, State::Open)
512    }
513
514    #[test]
515    fn issue_list_reqs() {
516        fn test_serialize(tests: Vec<(IssueListOptions, Option<String>)>) {
517            for test in tests {
518                let (k, v) = test;
519                assert_eq!(k.serialize(), v);
520            }
521        }
522        let tests = vec![
523            (IssueListOptions::builder().build(), None),
524            (
525                IssueListOptions::builder().state(State::Closed).build(),
526                Some("state=closed".to_owned()),
527            ),
528            (
529                IssueListOptions::builder()
530                    .labels(vec!["foo", "bar"])
531                    .build(),
532                Some("labels=foo%2Cbar".to_owned()),
533            ),
534        ];
535        test_serialize(tests)
536    }
537
538    #[test]
539    fn sort_default() {
540        let default: Sort = Default::default();
541        assert_eq!(default, Sort::Created)
542    }
543
544    #[test]
545    fn sort_display() {
546        for (k, v) in &[
547            (Sort::Created, "created"),
548            (Sort::Updated, "updated"),
549            (Sort::Comments, "comments"),
550        ] {
551            assert_eq!(k.to_string(), *v)
552        }
553    }
554}