1use 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#[derive(Clone, Copy, Debug, PartialEq)]
19pub enum Sort {
20 Created,
22 Updated,
24 Popularity,
26 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
48pub 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 pub fn get(&self) -> Future<Pull> {
80 self.github.get(&self.path(""))
81 }
82
83 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 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 pub fn open(&self) -> Future<Pull> {
105 self.edit(&PullEditOptions::builder().state("open").build())
106 }
107
108 pub fn close(&self) -> Future<Pull> {
110 self.edit(&PullEditOptions::builder().state("closed").build())
111 }
112
113 pub fn edit(&self, pr: &PullEditOptions) -> Future<Pull> {
115 self.github.patch::<Pull>(&self.path(""), json!(pr))
116 }
117
118 pub fn files(&self) -> Future<Vec<FileDiff>> {
120 self.github.get(&self.path("/files"))
121 }
122
123 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 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 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
163pub 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 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 pub fn create(&self, pr: &PullOptions) -> Future<Pull> {
200 self.github.post(&self.path(""), json!(pr))
201 }
202
203 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 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#[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 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, }
272
273#[derive(Default)]
274pub struct PullEditOptionsBuilder(PullEditOptions);
275
276impl PullEditOptionsBuilder {
277 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 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 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 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 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 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 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 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}