use std::collections::HashMap;
use std::fmt;
use serde::{Deserialize, Serialize};
use url::form_urlencoded;
use crate::comments::Comments;
use crate::issues::{IssueAssignees, IssueLabels, Sort as IssueSort, State};
use crate::labels::Label;
use crate::pull_commits::PullCommits;
use crate::review_comments::ReviewComments;
use crate::review_requests::ReviewRequests;
use crate::users::User;
use crate::{Future, Github, SortDirection, Stream};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Sort {
Created,
Updated,
Popularity,
LongRunning,
}
impl fmt::Display for Sort {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Sort::Created => "created",
Sort::Updated => "updated",
Sort::Popularity => "popularity",
Sort::LongRunning => "long-running",
}
.fmt(f)
}
}
impl Default for Sort {
fn default() -> Sort {
Sort::Created
}
}
pub struct PullRequest {
github: Github,
owner: String,
repo: String,
number: u64,
}
impl PullRequest {
#[doc(hidden)]
pub fn new<O, R>(github: Github, owner: O, repo: R, number: u64) -> Self
where
O: Into<String>,
R: Into<String>,
{
PullRequest {
github,
owner: owner.into(),
repo: repo.into(),
number,
}
}
fn path(&self, more: &str) -> String {
format!(
"/repos/{}/{}/pulls/{}{}",
self.owner, self.repo, self.number, more
)
}
pub fn get(&self) -> Future<Pull> {
self.github.get(&self.path(""))
}
pub fn labels(&self) -> IssueLabels {
IssueLabels::new(
self.github.clone(),
self.owner.as_str(),
self.repo.as_str(),
self.number,
)
}
pub fn assignees(&self) -> IssueAssignees {
IssueAssignees::new(
self.github.clone(),
self.owner.as_str(),
self.repo.as_str(),
self.number,
)
}
pub fn open(&self) -> Future<Pull> {
self.edit(&PullEditOptions::builder().state("open").build())
}
pub fn close(&self) -> Future<Pull> {
self.edit(&PullEditOptions::builder().state("closed").build())
}
pub fn edit(&self, pr: &PullEditOptions) -> Future<Pull> {
self.github.patch::<Pull>(&self.path(""), json!(pr))
}
pub fn files(&self) -> Future<Vec<FileDiff>> {
self.github.get(&self.path("/files"))
}
pub fn comments(&self) -> Comments {
Comments::new(
self.github.clone(),
self.owner.clone(),
self.repo.clone(),
self.number,
)
}
pub fn review_comments(&self) -> ReviewComments {
ReviewComments::new(
self.github.clone(),
self.owner.clone(),
self.repo.clone(),
self.number,
)
}
pub fn review_requests(&self) -> ReviewRequests {
ReviewRequests::new(
self.github.clone(),
self.owner.clone(),
self.repo.clone(),
self.number,
)
}
pub fn commits(&self) -> PullCommits {
PullCommits::new(
self.github.clone(),
self.owner.clone(),
self.repo.clone(),
self.number,
)
}
}
pub struct PullRequests {
github: Github,
owner: String,
repo: String,
}
impl PullRequests {
#[doc(hidden)]
pub fn new<O, R>(github: Github, owner: O, repo: R) -> Self
where
O: Into<String>,
R: Into<String>,
{
PullRequests {
github,
owner: owner.into(),
repo: repo.into(),
}
}
fn path(&self, more: &str) -> String {
format!("/repos/{}/{}/pulls{}", self.owner, self.repo, more)
}
pub fn get(&self, number: u64) -> PullRequest {
PullRequest::new(
self.github.clone(),
self.owner.as_str(),
self.repo.as_str(),
number,
)
}
pub fn create(&self, pr: &PullOptions) -> Future<Pull> {
self.github.post(&self.path(""), json!(pr))
}
pub fn list(&self, options: &PullListOptions) -> Future<Vec<Pull>> {
let mut uri = vec![self.path("")];
if let Some(query) = options.serialize() {
uri.push(query);
}
self.github.get::<Vec<Pull>>(&uri.join("?"))
}
pub fn iter(&self, options: &PullListOptions) -> Stream<Pull> {
let mut uri = vec![self.path("")];
if let Some(query) = options.serialize() {
uri.push(query);
}
self.github.get_stream(&uri.join("?"))
}
}
#[derive(Debug, Deserialize)]
pub struct Pull {
pub id: u64,
pub url: String,
pub html_url: String,
pub diff_url: String,
pub patch_url: String,
pub issue_url: String,
pub commits_url: String,
pub review_comments_url: String,
pub review_comment_url: String,
pub comments_url: String,
pub statuses_url: String,
pub number: u64,
pub state: String,
pub title: String,
pub body: Option<String>,
pub created_at: String,
pub updated_at: String,
pub closed_at: Option<String>,
pub merged_at: Option<String>,
pub head: Commit,
pub base: Commit,
pub user: User,
pub assignee: Option<User>,
pub assignees: Vec<User>,
pub merge_commit_sha: Option<String>,
pub merged: bool,
pub mergeable: Option<bool>,
pub merged_by: Option<User>,
pub comments: Option<u64>,
pub commits: Option<u64>,
pub additions: Option<u64>,
pub deletions: Option<u64>,
pub changed_files: Option<u64>,
pub labels: Vec<Label>,
}
#[derive(Debug, Deserialize)]
pub struct Commit {
pub label: String,
#[serde(rename = "ref")]
pub commit_ref: String,
pub sha: String,
pub user: User, }
#[derive(Default)]
pub struct PullEditOptionsBuilder(PullEditOptions);
impl PullEditOptionsBuilder {
pub fn title<T>(&mut self, title: T) -> &mut Self
where
T: Into<String>,
{
self.0.title = Some(title.into());
self
}
pub fn body<B>(&mut self, body: B) -> &mut Self
where
B: Into<String>,
{
self.0.body = Some(body.into());
self
}
pub fn state<S>(&mut self, state: S) -> &mut Self
where
S: Into<String>,
{
self.0.state = Some(state.into());
self
}
pub fn build(&self) -> PullEditOptions {
PullEditOptions {
title: self.0.title.clone(),
body: self.0.body.clone(),
state: self.0.state.clone(),
}
}
}
#[derive(Debug, Default, Serialize)]
pub struct PullEditOptions {
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
body: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
state: Option<String>,
}
impl PullEditOptions {
pub fn new<T, B, S>(title: Option<T>, body: Option<B>, state: Option<S>) -> PullEditOptions
where
T: Into<String>,
B: Into<String>,
S: Into<String>,
{
PullEditOptions {
title: title.map(|t| t.into()),
body: body.map(|b| b.into()),
state: state.map(|s| s.into()),
}
}
pub fn builder() -> PullEditOptionsBuilder {
PullEditOptionsBuilder::default()
}
}
#[derive(Debug, Serialize)]
pub struct PullOptions {
pub title: String,
pub head: String,
pub base: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
}
impl PullOptions {
pub fn new<T, H, BS, B>(title: T, head: H, base: BS, body: Option<B>) -> PullOptions
where
T: Into<String>,
H: Into<String>,
BS: Into<String>,
B: Into<String>,
{
PullOptions {
title: title.into(),
head: head.into(),
base: base.into(),
body: body.map(|b| b.into()),
}
}
}
#[derive(Debug, Deserialize)]
pub struct FileDiff {
pub sha: Option<String>,
pub filename: String,
pub status: String,
pub additions: u64,
pub deletions: u64,
pub changes: u64,
pub blob_url: String,
pub raw_url: String,
pub contents_url: String,
pub patch: Option<String>,
}
#[derive(Default)]
pub struct PullListOptions {
params: HashMap<&'static str, String>,
}
impl PullListOptions {
pub fn builder() -> PullListOptionsBuilder {
PullListOptionsBuilder::default()
}
pub fn serialize(&self) -> Option<String> {
if self.params.is_empty() {
None
} else {
let encoded: String = form_urlencoded::Serializer::new(String::new())
.extend_pairs(&self.params)
.finish();
Some(encoded)
}
}
}
#[derive(Default)]
pub struct PullListOptionsBuilder(PullListOptions);
impl PullListOptionsBuilder {
pub fn state(&mut self, state: State) -> &mut Self {
self.0.params.insert("state", state.to_string());
self
}
pub fn sort(&mut self, sort: IssueSort) -> &mut Self {
self.0.params.insert("sort", sort.to_string());
self
}
pub fn direction(&mut self, direction: SortDirection) -> &mut Self {
self.0.params.insert("direction", direction.to_string());
self
}
pub fn build(&self) -> PullListOptions {
PullListOptions {
params: self.0.params.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::ser::Serialize;
fn test_encoding<E: Serialize>(tests: Vec<(E, &str)>) {
for test in tests {
let (k, v) = test;
assert_eq!(serde_json::to_string(&k).unwrap(), v);
}
}
#[test]
fn pull_list_reqs() {
fn test_serialize(tests: Vec<(PullListOptions, Option<String>)>) {
for test in tests {
let (k, v) = test;
assert_eq!(k.serialize(), v);
}
}
let tests = vec![
(PullListOptions::builder().build(), None),
(
PullListOptions::builder().state(State::Closed).build(),
Some("state=closed".to_owned()),
),
];
test_serialize(tests)
}
#[test]
fn pullreq_edits() {
let tests = vec![
(
PullEditOptions::builder().title("test").build(),
r#"{"title":"test"}"#,
),
(
PullEditOptions::builder()
.title("test")
.body("desc")
.build(),
r#"{"title":"test","body":"desc"}"#,
),
(
PullEditOptions::builder().state("closed").build(),
r#"{"state":"closed"}"#,
),
];
test_encoding(tests)
}
#[test]
fn default_sort() {
let default: Sort = Default::default();
assert_eq!(default, Sort::Created)
}
}