hubcaps_ex/
pulls.rs

1//! Pull requests interface
2use std::collections::HashMap;
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6use url::form_urlencoded;
7
8use crate::comments::Comments;
9use crate::issues::{IssueAssignees, IssueLabels, Sort as IssueSort, State};
10use crate::labels::Label;
11use crate::pull_commits::PullCommits;
12use crate::review_comments::ReviewComments;
13use crate::review_requests::ReviewRequests;
14use crate::users::User;
15use crate::{Future, Github, SortDirection, Stream};
16
17/// Sort directions for pull requests
18#[derive(Clone, Copy, Debug, PartialEq)]
19pub enum Sort {
20    /// Sort by time created
21    Created,
22    /// Sort by last updated
23    Updated,
24    /// Sort by popularity
25    Popularity,
26    /// Sort by long running issues
27    LongRunning,
28}
29
30impl fmt::Display for Sort {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        match *self {
33            Sort::Created => "created",
34            Sort::Updated => "updated",
35            Sort::Popularity => "popularity",
36            Sort::LongRunning => "long-running",
37        }
38        .fmt(f)
39    }
40}
41
42impl Default for Sort {
43    fn default() -> Sort {
44        Sort::Created
45    }
46}
47
48/// A structure for accessing interfacing with a specific pull request
49pub struct PullRequest {
50    github: Github,
51    owner: String,
52    repo: String,
53    number: u64,
54}
55
56impl PullRequest {
57    #[doc(hidden)]
58    pub fn new<O, R>(github: Github, owner: O, repo: R, number: u64) -> Self
59    where
60        O: Into<String>,
61        R: Into<String>,
62    {
63        PullRequest {
64            github,
65            owner: owner.into(),
66            repo: repo.into(),
67            number,
68        }
69    }
70
71    fn path(&self, more: &str) -> String {
72        format!(
73            "/repos/{}/{}/pulls/{}{}",
74            self.owner, self.repo, self.number, more
75        )
76    }
77
78    /// Request a pull requests information
79    pub fn get(&self) -> Future<Pull> {
80        self.github.get(&self.path(""))
81    }
82
83    /// Return a reference to labels operations available for this pull request
84    pub fn labels(&self) -> IssueLabels {
85        IssueLabels::new(
86            self.github.clone(),
87            self.owner.as_str(),
88            self.repo.as_str(),
89            self.number,
90        )
91    }
92
93    /// Return a reference to assignee operations available for this pull request
94    pub fn assignees(&self) -> IssueAssignees {
95        IssueAssignees::new(
96            self.github.clone(),
97            self.owner.as_str(),
98            self.repo.as_str(),
99            self.number,
100        )
101    }
102
103    /// short hand for editing state = open
104    pub fn open(&self) -> Future<Pull> {
105        self.edit(&PullEditOptions::builder().state("open").build())
106    }
107
108    /// shorthand for editing state = closed
109    pub fn close(&self) -> Future<Pull> {
110        self.edit(&PullEditOptions::builder().state("closed").build())
111    }
112
113    /// Edit a pull request
114    pub fn edit(&self, pr: &PullEditOptions) -> Future<Pull> {
115        self.github.patch::<Pull>(&self.path(""), json!(pr))
116    }
117
118    /// Returns a vector of file diffs associated with this pull
119    pub fn files(&self) -> Future<Vec<FileDiff>> {
120        self.github.get(&self.path("/files"))
121    }
122
123    /// returns issue comments interface
124    pub fn comments(&self) -> Comments {
125        Comments::new(
126            self.github.clone(),
127            self.owner.clone(),
128            self.repo.clone(),
129            self.number,
130        )
131    }
132
133    /// returns review comments interface
134    pub fn review_comments(&self) -> ReviewComments {
135        ReviewComments::new(
136            self.github.clone(),
137            self.owner.clone(),
138            self.repo.clone(),
139            self.number,
140        )
141    }
142
143    pub fn review_requests(&self) -> ReviewRequests {
144        ReviewRequests::new(
145            self.github.clone(),
146            self.owner.clone(),
147            self.repo.clone(),
148            self.number,
149        )
150    }
151
152    /// returns pull commits interface
153    pub fn commits(&self) -> PullCommits {
154        PullCommits::new(
155            self.github.clone(),
156            self.owner.clone(),
157            self.repo.clone(),
158            self.number,
159        )
160    }
161}
162
163/// A structure for interfacing with a repositories list of pull requests
164pub struct PullRequests {
165    github: Github,
166    owner: String,
167    repo: String,
168}
169
170impl PullRequests {
171    #[doc(hidden)]
172    pub fn new<O, R>(github: Github, owner: O, repo: R) -> Self
173    where
174        O: Into<String>,
175        R: Into<String>,
176    {
177        PullRequests {
178            github,
179            owner: owner.into(),
180            repo: repo.into(),
181        }
182    }
183
184    fn path(&self, more: &str) -> String {
185        format!("/repos/{}/{}/pulls{}", self.owner, self.repo, more)
186    }
187
188    /// Get a reference to a structure for interfacing with a specific pull request
189    pub fn get(&self, number: u64) -> PullRequest {
190        PullRequest::new(
191            self.github.clone(),
192            self.owner.as_str(),
193            self.repo.as_str(),
194            number,
195        )
196    }
197
198    /// Create a new pull request
199    pub fn create(&self, pr: &PullOptions) -> Future<Pull> {
200        self.github.post(&self.path(""), json!(pr))
201    }
202
203    /// list pull requests
204    pub fn list(&self, options: &PullListOptions) -> Future<Vec<Pull>> {
205        let mut uri = vec![self.path("")];
206        if let Some(query) = options.serialize() {
207            uri.push(query);
208        }
209        self.github.get::<Vec<Pull>>(&uri.join("?"))
210    }
211
212    /// provides a stream over all pages of pull requests
213    pub fn iter(&self, options: &PullListOptions) -> Stream<Pull> {
214        let mut uri = vec![self.path("")];
215        if let Some(query) = options.serialize() {
216            uri.push(query);
217        }
218        self.github.get_stream(&uri.join("?"))
219    }
220}
221
222// representations (todo: replace with derive_builder)
223
224/// representation of a github pull request
225#[derive(Debug, Deserialize)]
226pub struct Pull {
227    pub id: u64,
228    pub url: String,
229    pub html_url: String,
230    pub diff_url: String,
231    pub patch_url: String,
232    pub issue_url: String,
233    pub commits_url: String,
234    pub review_comments_url: String,
235    pub review_comment_url: String,
236    pub comments_url: String,
237    pub statuses_url: String,
238    pub number: u64,
239    pub state: String,
240    pub title: String,
241    pub body: Option<String>,
242    pub created_at: String,
243    pub updated_at: String,
244    pub closed_at: Option<String>,
245    pub merged_at: Option<String>,
246    pub head: Commit,
247    pub base: Commit,
248    // links
249    pub user: User,
250    pub assignee: Option<User>,
251    pub assignees: Vec<User>,
252    pub merge_commit_sha: Option<String>,
253    pub merged: bool,
254    pub mergeable: Option<bool>,
255    pub merged_by: Option<User>,
256    pub comments: Option<u64>,
257    pub commits: Option<u64>,
258    pub additions: Option<u64>,
259    pub deletions: Option<u64>,
260    pub changed_files: Option<u64>,
261    pub labels: Vec<Label>,
262}
263
264#[derive(Debug, Deserialize)]
265pub struct Commit {
266    pub label: String,
267    #[serde(rename = "ref")]
268    pub commit_ref: String,
269    pub sha: String,
270    pub user: User, //    pub repo: Option<Repo>,
271}
272
273#[derive(Default)]
274pub struct PullEditOptionsBuilder(PullEditOptions);
275
276impl PullEditOptionsBuilder {
277    /// set the title of the pull
278    pub fn title<T>(&mut self, title: T) -> &mut Self
279    where
280        T: Into<String>,
281    {
282        self.0.title = Some(title.into());
283        self
284    }
285
286    /// set the body of the pull
287    pub fn body<B>(&mut self, body: B) -> &mut Self
288    where
289        B: Into<String>,
290    {
291        self.0.body = Some(body.into());
292        self
293    }
294
295    /// set the state of the pull
296    pub fn state<S>(&mut self, state: S) -> &mut Self
297    where
298        S: Into<String>,
299    {
300        self.0.state = Some(state.into());
301        self
302    }
303
304    /// create a new set of pull edit options
305    pub fn build(&self) -> PullEditOptions {
306        PullEditOptions {
307            title: self.0.title.clone(),
308            body: self.0.body.clone(),
309            state: self.0.state.clone(),
310        }
311    }
312}
313
314#[derive(Debug, Default, Serialize)]
315pub struct PullEditOptions {
316    #[serde(skip_serializing_if = "Option::is_none")]
317    title: Option<String>,
318    #[serde(skip_serializing_if = "Option::is_none")]
319    body: Option<String>,
320    #[serde(skip_serializing_if = "Option::is_none")]
321    state: Option<String>,
322}
323
324impl PullEditOptions {
325    // todo represent state as enum
326    pub fn new<T, B, S>(title: Option<T>, body: Option<B>, state: Option<S>) -> PullEditOptions
327    where
328        T: Into<String>,
329        B: Into<String>,
330        S: Into<String>,
331    {
332        PullEditOptions {
333            title: title.map(|t| t.into()),
334            body: body.map(|b| b.into()),
335            state: state.map(|s| s.into()),
336        }
337    }
338    pub fn builder() -> PullEditOptionsBuilder {
339        PullEditOptionsBuilder::default()
340    }
341}
342
343#[derive(Debug, Serialize)]
344pub struct PullOptions {
345    pub title: String,
346    pub head: String,
347    pub base: String,
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub body: Option<String>,
350}
351
352impl PullOptions {
353    pub fn new<T, H, BS, B>(title: T, head: H, base: BS, body: Option<B>) -> PullOptions
354    where
355        T: Into<String>,
356        H: Into<String>,
357        BS: Into<String>,
358        B: Into<String>,
359    {
360        PullOptions {
361            title: title.into(),
362            head: head.into(),
363            base: base.into(),
364            body: body.map(|b| b.into()),
365        }
366    }
367}
368
369#[derive(Debug, Deserialize)]
370pub struct FileDiff {
371    /// sha from GitHub may be null when file mode changed without contents changing
372    pub sha: Option<String>,
373    pub filename: String,
374    pub status: String,
375    pub additions: u64,
376    pub deletions: u64,
377    pub changes: u64,
378    pub blob_url: String,
379    pub raw_url: String,
380    pub contents_url: String,
381    /// patch is typically None for binary files
382    pub patch: Option<String>,
383}
384
385#[derive(Default)]
386pub struct PullListOptions {
387    params: HashMap<&'static str, String>,
388}
389
390impl PullListOptions {
391    pub fn builder() -> PullListOptionsBuilder {
392        PullListOptionsBuilder::default()
393    }
394
395    /// serialize options as a string. returns None if no options are defined
396    pub fn serialize(&self) -> Option<String> {
397        if self.params.is_empty() {
398            None
399        } else {
400            let encoded: String = form_urlencoded::Serializer::new(String::new())
401                .extend_pairs(&self.params)
402                .finish();
403            Some(encoded)
404        }
405    }
406}
407
408#[derive(Default)]
409pub struct PullListOptionsBuilder(PullListOptions);
410
411impl PullListOptionsBuilder {
412    pub fn state(&mut self, state: State) -> &mut Self {
413        self.0.params.insert("state", state.to_string());
414        self
415    }
416
417    pub fn sort(&mut self, sort: IssueSort) -> &mut Self {
418        self.0.params.insert("sort", sort.to_string());
419        self
420    }
421
422    pub fn direction(&mut self, direction: SortDirection) -> &mut Self {
423        self.0.params.insert("direction", direction.to_string());
424        self
425    }
426
427    pub fn build(&self) -> PullListOptions {
428        PullListOptions {
429            params: self.0.params.clone(),
430        }
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437    use serde::ser::Serialize;
438    fn test_encoding<E: Serialize>(tests: Vec<(E, &str)>) {
439        for test in tests {
440            let (k, v) = test;
441            assert_eq!(serde_json::to_string(&k).unwrap(), v);
442        }
443    }
444
445    #[test]
446    fn pull_list_reqs() {
447        fn test_serialize(tests: Vec<(PullListOptions, Option<String>)>) {
448            for test in tests {
449                let (k, v) = test;
450                assert_eq!(k.serialize(), v);
451            }
452        }
453        let tests = vec![
454            (PullListOptions::builder().build(), None),
455            (
456                PullListOptions::builder().state(State::Closed).build(),
457                Some("state=closed".to_owned()),
458            ),
459        ];
460        test_serialize(tests)
461    }
462
463    #[test]
464    fn pullreq_edits() {
465        let tests = vec![
466            (
467                PullEditOptions::builder().title("test").build(),
468                r#"{"title":"test"}"#,
469            ),
470            (
471                PullEditOptions::builder()
472                    .title("test")
473                    .body("desc")
474                    .build(),
475                r#"{"title":"test","body":"desc"}"#,
476            ),
477            (
478                PullEditOptions::builder().state("closed").build(),
479                r#"{"state":"closed"}"#,
480            ),
481        ];
482        test_encoding(tests)
483    }
484
485    #[test]
486    fn default_sort() {
487        let default: Sort = Default::default();
488        assert_eq!(default, Sort::Created)
489    }
490}