diff --git a/src/api/pulls.rs b/src/api/pulls.rs index 4e9caf41..bdc3add3 100644 --- a/src/api/pulls.rs +++ b/src/api/pulls.rs @@ -1,17 +1,15 @@ //! The pull request API. -mod comment; -mod create; -mod list; -mod merge; -mod update; - use http::request::Builder; use http::{Method, Uri}; - +use serde_json::json; use snafu::ResultExt; use crate::error::HttpSnafu; +use crate::models::pulls::ReviewComment; +use crate::models::CommentId; +use crate::pulls::specific_pr::pr_reviews::specific_review::SpecificReviewBuilder; +use crate::pulls::specific_pr::SpecificPullRequestBuilder; use crate::{Octocrab, Page}; pub use self::{ @@ -19,6 +17,13 @@ pub use self::{ update::UpdatePullRequestBuilder, }; +mod comment; +mod create; +mod list; +mod merge; +mod specific_pr; +mod update; + /// A client to GitHub's pull request API. /// /// Created with [`Octocrab::pulls`]. @@ -372,6 +377,90 @@ impl<'octo> PullRequestHandler<'octo> { comment::ListCommentsBuilder::new(self, pr) } + ///creates a new `CommentBuilder` for GET/PATCH/DELETE requests + /// to the `/repos/{owner}/{repo}/pulls/{pr}/comments/{comment_id}` endpoint + /// ```no_run + /// use octocrab::models::CommentId; + /// use octocrab::models::pulls::Comment; + /// async fn run() -> octocrab::Result { + /// let octocrab = octocrab::Octocrab::default(); + /// let _ = octocrab.pulls("owner", "repo").comment(CommentId(21)).delete(); + /// let _ = octocrab.pulls("owner", "repo").comment(CommentId(42)).update("new comment"); + /// let comment = octocrab.pulls("owner", "repo").comment(CommentId(42)).get().await; + /// + /// comment + /// } + /// ``` + pub fn comment(&self, comment_id: CommentId) -> comment::CommentBuilder { + comment::CommentBuilder::new(self, comment_id) + } + + /// creates a builder for the `/repos/{owner}/{repo}/pulls/{pull_number}/......` endpoint + /// working with particular pull request, e.g. + /// * /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/events + /// * /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id} + /// * /repos/{owner}/{repo}/pulls/{pull_number}/commits + /// * /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/comments + /// * /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/dismissals + /// * /repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies + /// + #[deprecated( + since = "0.34.4", + note = "specific PR builder transitioned to pr_review_actions, reply_to_comment, reply_to_comment" + )] + //FIXME: remove? + pub fn pull_number(&self, pull_nr: u64) -> SpecificPullRequestBuilder { + SpecificPullRequestBuilder::new(self, pull_nr) + } + + // /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/events + // /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id} + // repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/comments + // repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/dismissals + pub fn pr_review_actions( + &self, + pull_nr: u64, + review_id: u64, + ) -> SpecificReviewBuilder<'octo, '_> { + SpecificReviewBuilder::new(self, pull_nr, review_id) + } + + /// /repos/{owner}/{repo}/pulls/{pull_number}/commits + // pub fn pr_commits(&self, pull_nr: u64) -> SpecificPullRequestCommentBuilder<'octo, '_> { + // SpecificPullRequestCommentBuilder::new(self, pull_nr, 0) + // } + + // /repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies + /// Creates a reply to a specific comment of a pull request specified in the first argument + /// ```no_run + /// # use octocrab::models::CommentId; + /// async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let page = octocrab.pulls("owner", "repo").reply_to_comment(142, CommentId(24), "This is my reply") + /// .await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn reply_to_comment( + &self, + pull_nr: u64, + comment_id: CommentId, + comment: impl Into, + ) -> crate::Result { + let route = format!( + "/repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies", + owner = self.owner, + repo = self.repo, + pull_number = pull_nr, + comment_id = comment_id + ); + self.crab + .post(route, Some(&json!({ "body": comment.into() }))) + .await + } + /// Creates a new `MergePullRequestsBuilder` that can be configured used to /// merge a pull request. /// ```no_run diff --git a/src/api/pulls/comment.rs b/src/api/pulls/comment.rs index df4506e6..4122ced0 100644 --- a/src/api/pulls/comment.rs +++ b/src/api/pulls/comment.rs @@ -1,3 +1,7 @@ +use serde_json::json; + +use crate::models::pulls::Comment; + use super::*; /// A builder pattern struct for listing comments. @@ -84,6 +88,76 @@ impl<'octo, 'b> ListCommentsBuilder<'octo, 'b> { } } +/// A builder pattern struct for working with specific comment. +/// +/// created by [`PullRequestHandler::comment`] +/// +/// [`PullRequestHandler::comment`]: ./struct.PullRequestHandler.html#method.comment +#[derive(serde::Serialize)] +pub struct CommentBuilder<'octo, 'b> { + #[serde(skip)] + handler: &'b PullRequestHandler<'octo>, + comment_id: CommentId, +} + +impl<'octo, 'b> CommentBuilder<'octo, 'b> { + pub(crate) fn new(handler: &'b PullRequestHandler<'octo>, comment_id: CommentId) -> Self { + Self { + handler, + comment_id, + } + } + + ///https://docs.github.com/en/rest/pulls/comments?apiVersion=2022-11-28#get-a-review-comment-for-a-pull-request + pub async fn get(self) -> crate::Result { + self.handler + .crab + .get( + format!( + "/repos/{owner}/{repo}/pulls/comments/{comment_id}", + owner = self.handler.owner, + repo = self.handler.repo, + comment_id = self.comment_id + ), + None::<&Comment>, + ) + .await + } + + ///https://docs.github.com/en/rest/pulls/comments?apiVersion=2022-11-28#update-a-review-comment-for-a-pull-request + pub async fn update(self, comment: &str) -> crate::Result { + self.handler + .crab + .patch( + format!( + "/repos/{owner}/{repo}/pulls/comments/{comment_id}", + owner = self.handler.owner, + repo = self.handler.repo, + comment_id = self.comment_id + ), + Some(&json!({ "body": comment })), + ) + .await + } + + ///https://docs.github.com/en/rest/pulls/comments?apiVersion=2022-11-28#delete-a-review-comment-for-a-pull-request + pub async fn delete(self) -> crate::Result<()> { + self.handler + .crab + ._delete( + format!( + "/repos/{owner}/{repo}/pulls/comments/{comment_id}", + owner = self.handler.owner, + repo = self.handler.repo, + comment_id = self.comment_id + ), + None::<&()>, + ) + .await?; + Ok(()) + } +} + #[cfg(test)] mod tests { #[tokio::test] diff --git a/src/api/pulls/specific_pr.rs b/src/api/pulls/specific_pr.rs new file mode 100644 index 00000000..b8d7af02 --- /dev/null +++ b/src/api/pulls/specific_pr.rs @@ -0,0 +1,101 @@ +use crate::models::repos::RepoCommit; +use crate::models::CommentId; +use crate::pulls::specific_pr::pr_comment::SpecificPullRequestCommentBuilder; +use crate::pulls::specific_pr::pr_reviews::ReviewsBuilder; +use crate::pulls::PullRequestHandler; +use crate::Page; + +mod pr_comment; +pub(crate) mod pr_reviews; +/// A builder pattern struct for working with a specific pull request data, +/// e.g. reviews, commits, comments, etc. +/// +/// created by [`PullRequestHandler::pull_number`] +/// +/// [`PullRequestHandler::pull_number`]: ./struct.PullRequestHandler.html#method.pull_number +#[derive(serde::Serialize)] +pub struct SpecificPullRequestBuilder<'octo, 'b> { + #[serde(skip)] + handler: &'b PullRequestHandler<'octo>, + pr_number: u64, + #[serde(skip_serializing_if = "Option::is_none")] + per_page: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + page: Option, +} + +impl<'octo, 'b> SpecificPullRequestBuilder<'octo, 'b> { + pub(crate) fn new(handler: &'b PullRequestHandler<'octo>, pr_number: u64) -> Self { + Self { + handler, + pr_number, + per_page: None, + page: None, + } + } + + /// Results per page (max: 100, default: 30). + pub fn per_page(mut self, per_page: impl Into) -> Self { + self.per_page = Some(per_page.into()); + self + } + + /// Page number of the results to fetch. (default: 1) + pub fn page(mut self, page: impl Into) -> Self { + self.page = Some(page.into()); + self + } + + ///Lists a maximum of 250 commits for a pull request. + /// To receive a complete commit list for pull requests with more than 250 commits, + /// use the [List commits](https://docs.github.com/rest/commits/commits#list-commits) endpoint. + pub async fn commits(&self) -> crate::Result> { + let route = format!( + "/repos/{owner}/{repo}/pulls/{pr_number}/commits", + owner = self.handler.owner, + repo = self.handler.repo, + pr_number = self.pr_number + ); + self.handler.crab.get(route, Some(&self)).await + } + + /// Creates a new `ReviewsBuilder` + /// ```no_run + /// # use octocrab::models::CommentId; + /// async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let _ = octocrab.pulls("owner", "repo") + /// .pull_number(42) + /// .reviews() + /// .review(42) + /// .get() + /// .await; + /// Ok(()) + /// } + /// ``` + pub fn reviews(&self) -> ReviewsBuilder<'octo, '_> { + ReviewsBuilder::new(self.handler, self.pr_number) + } + + /// Creates a new `SpecificPullRequestCommentBuilder` + /// ```no_run + /// # use octocrab::models::CommentId; + /// async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let _ = octocrab.pulls("owner", "repo") + /// .pull_number(42) + /// .comment(CommentId(42)) + /// .reply("new comment") + /// .await; + /// Ok(()) + /// } + /// ``` + pub fn comment(&self, comment_id: CommentId) -> SpecificPullRequestCommentBuilder { + SpecificPullRequestCommentBuilder::new(self.handler, self.pr_number, comment_id) + } +} diff --git a/src/api/pulls/specific_pr/pr_comment.rs b/src/api/pulls/specific_pr/pr_comment.rs new file mode 100644 index 00000000..db687438 --- /dev/null +++ b/src/api/pulls/specific_pr/pr_comment.rs @@ -0,0 +1,41 @@ +use serde_json::json; + +use crate::models::pulls::ReviewComment; +use crate::models::CommentId; +use crate::pulls::PullRequestHandler; + +#[derive(serde::Serialize)] +pub struct SpecificPullRequestCommentBuilder<'octo, 'b> { + #[serde(skip)] + handler: &'b PullRequestHandler<'octo>, + pr_number: u64, + comment_id: CommentId, +} + +impl<'octo, 'b> SpecificPullRequestCommentBuilder<'octo, 'b> { + pub(crate) fn new( + handler: &'b PullRequestHandler<'octo>, + pr_number: u64, + comment_id: CommentId, + ) -> Self { + Self { + handler, + comment_id, + pr_number, + } + } + + pub async fn reply(&self, comment: impl Into) -> crate::Result { + let route = format!( + "/repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies", + owner = self.handler.owner, + repo = self.handler.repo, + pull_number = self.pr_number, + comment_id = self.comment_id + ); + self.handler + .crab + .post(route, Some(&json!({ "body": comment.into() }))) + .await + } +} diff --git a/src/api/pulls/specific_pr/pr_reviews.rs b/src/api/pulls/specific_pr/pr_reviews.rs new file mode 100644 index 00000000..e23b67c6 --- /dev/null +++ b/src/api/pulls/specific_pr/pr_reviews.rs @@ -0,0 +1,45 @@ +use crate::pulls::specific_pr::pr_reviews::specific_review::SpecificReviewBuilder; +use crate::pulls::PullRequestHandler; + +pub mod specific_review; + +#[derive(serde::Serialize)] +pub struct ReviewsBuilder<'octo, 'b> { + #[serde(skip)] + handler: &'b PullRequestHandler<'octo>, + #[serde(skip_serializing_if = "Option::is_none")] + per_page: Option, + #[serde(skip_serializing_if = "Option::is_none")] + page: Option, + pr_number: u64, +} + +impl<'octo, 'b> ReviewsBuilder<'octo, 'b> { + pub(crate) fn new(handler: &'b PullRequestHandler<'octo>, pr_number: u64) -> Self { + Self { + handler, + per_page: None, + page: None, + pr_number, + } + } + + /// Creates a new `SpecificReviewBuilder` + /// ```no_run + /// # async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let _ = octocrab.pulls("owner", "repo") + /// .pull_number(42) + /// .reviews() + /// .review(42) + /// .get() // + update, delete_pending, submit, dismiss, list_comments + /// .await?; + /// # Ok(()) + /// # } + /// ``` + pub fn review(&self, review_id: u64) -> SpecificReviewBuilder<'octo, '_> { + SpecificReviewBuilder::new(self.handler, self.pr_number, review_id) + } +} diff --git a/src/api/pulls/specific_pr/pr_reviews/specific_review.rs b/src/api/pulls/specific_pr/pr_reviews/specific_review.rs new file mode 100644 index 00000000..5f9f8960 --- /dev/null +++ b/src/api/pulls/specific_pr/pr_reviews/specific_review.rs @@ -0,0 +1,205 @@ +use crate::models::pulls::{Review, ReviewAction}; +use crate::pulls::specific_pr::pr_reviews::specific_review::list_comments::ListReviewCommentsBuilder; +use crate::pulls::PullRequestHandler; + +mod list_comments; + +#[derive(serde::Serialize)] +pub struct SpecificReviewBuilder<'octo, 'b> { + #[serde(skip)] + handler: &'b PullRequestHandler<'octo>, + pr_number: u64, + review_id: u64, +} + +impl<'octo, 'b> SpecificReviewBuilder<'octo, 'b> { + pub(crate) fn new( + handler: &'b PullRequestHandler<'octo>, + pr_number: u64, + review_id: u64, + ) -> Self { + Self { + handler, + pr_number, + review_id, + } + } + + ///Retrieves a pull request review by its ID. + ///see https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#get-a-review-for-a-pull-request + /// ```no_run + /// # async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let _ = octocrab.pulls("owner", "repo") + /// .pull_number(42) + /// .reviews() + /// .review(42) + /// .get() + /// .await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn get(&self) -> crate::Result { + let route = format!( + "/repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}", + owner = self.handler.owner, + repo = self.handler.repo, + pull_number = self.pr_number, + review_id = self.review_id + ); + self.handler.crab.get(route, Some(&self)).await + } + + ///Updates the contents of a specified review summary comment. + ///see https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#update-a-review-for-a-pull-request + /// ```no_run + /// # async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let _ = octocrab.pulls("owner", "repo") + /// .pull_number(42) + /// .reviews() + /// .review(42) + /// .update("this is a new body") + /// .await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn update( + &self, + body: impl Into, + ) -> crate::Result { + let route = format!( + "/repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}", + owner = self.handler.owner, + repo = self.handler.repo, + pull_number = self.pr_number, + review_id = self.review_id + ); + self.handler.crab.patch(route, Some(&body.into())).await + } + + ///Deletes a pull request review that has not been submitted. Submitted reviews cannot be deleted. + ///see https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#delete-a-pending-review-for-a-pull-request + /// ```no_run + /// # async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let _ = octocrab.pulls("owner", "repo") + /// .pull_number(42) + /// .reviews() + /// .review(42) + /// .delete_pending() + /// .await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn delete_pending(&self) -> crate::Result { + let route = format!( + "/repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}", + owner = self.handler.owner, + repo = self.handler.repo, + pull_number = self.pr_number, + review_id = self.review_id + ); + self.handler.crab.delete(route, None::<&()>).await + } + + ///Submits a pending review for a pull request. + ///see https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#submit-a-review-for-a-pull-request + ///```no_run + /// # use octocrab::models::pulls::ReviewAction; + /// async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let _ = octocrab.pulls("owner", "repo") + /// .pull_number(42) + /// .reviews() + /// .review(42) + /// .submit(ReviewAction::RequestChanges, "comment body") + /// .await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn submit( + &self, + action: ReviewAction, + body: impl Into, + ) -> crate::Result { + let route = format!( + "/repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/events", + owner = self.handler.owner, + repo = self.handler.repo, + pull_number = self.pr_number, + review_id = self.review_id + ); + self.handler + .crab + .post( + route, + Some(&serde_json::json!({ "body": body.into(), "event": action })), + ) + .await + } + + ///Dismisses a specified review on a pull request. + ///see https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#dismiss-a-review-for-a-pull-request + /// ```no_run + /// # async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let _ = octocrab.pulls("owner", "repo") + /// .pull_number(42) + /// .reviews() + /// .review(42) + /// .dismiss("message") + /// .await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn dismiss(&self, message: impl Into) -> crate::Result { + let route = format!( + "/repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/dismissals", + owner = self.handler.owner, + repo = self.handler.repo, + pull_number = self.pr_number, + review_id = self.review_id + ); + self.handler + .crab + .put( + route, + Some(&serde_json::json!({ "message": message.into(), "event": "DISMISS" })), + ) + .await + } + + ///Lists comments for a specific pull request review. + ///see https://docs.github.com/en/rest/pulls/reviews?apiVersion=2022-11-28#list-comments-for-a-pull-request-review + /// ```no_run + /// # async fn run() -> octocrab::Result<()> { + /// # let octocrab = octocrab::Octocrab::default(); + /// use octocrab::params; + /// + /// let _ = octocrab.pulls("owner", "repo") + /// .pull_number(42) + /// .reviews() + /// .review(42) + /// .list_comments() + /// .per_page(10) + /// .page(3u32) + /// .send() + /// .await?; + /// # Ok(()) + /// # } + /// ``` + pub fn list_comments(&self) -> ListReviewCommentsBuilder<'octo, '_> { + ListReviewCommentsBuilder::new(self.handler, self.pr_number, self.review_id) + } +} diff --git a/src/api/pulls/specific_pr/pr_reviews/specific_review/list_comments.rs b/src/api/pulls/specific_pr/pr_reviews/specific_review/list_comments.rs new file mode 100644 index 00000000..11e60be8 --- /dev/null +++ b/src/api/pulls/specific_pr/pr_reviews/specific_review/list_comments.rs @@ -0,0 +1,53 @@ +use super::*; + +#[derive(serde::Serialize)] +pub struct ListReviewCommentsBuilder<'octo, 'b> { + #[serde(skip)] + handler: &'b PullRequestHandler<'octo>, + pr_number: u64, + review_id: u64, + #[serde(skip_serializing_if = "Option::is_none")] + per_page: Option, + #[serde(skip_serializing_if = "Option::is_none")] + page: Option, +} + +impl<'octo, 'b> ListReviewCommentsBuilder<'octo, 'b> { + pub(crate) fn new( + handler: &'b PullRequestHandler<'octo>, + pr_number: u64, + review_id: u64, + ) -> Self { + Self { + handler, + pr_number, + review_id, + per_page: None, + page: None, + } + } + + /// Results per page (max 100). + pub fn per_page(mut self, per_page: impl Into) -> Self { + self.per_page = Some(per_page.into()); + self + } + + /// Page number of the results to fetch. + pub fn page(mut self, page: impl Into) -> Self { + self.page = Some(page.into()); + self + } + + /// Sends the actual request. + pub async fn send(self) -> crate::Result> { + let route = format!( + "/repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/comments", + owner = self.handler.owner, + repo = self.handler.repo, + pull_number = self.pr_number, + review_id = self.review_id + ); + self.handler.crab.get(route, Some(&self)).await + } +} diff --git a/src/models/commits.rs b/src/models/commits.rs index 278f4846..042e54e4 100644 --- a/src/models/commits.rs +++ b/src/models/commits.rs @@ -5,6 +5,7 @@ use super::{reactions::ReactionContent, *}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[non_exhaustive] pub struct Comment { + // TODO check actuality comparing with github json schema and pulls::ReviewComment pub html_url: Url, pub url: Url, pub id: CommentId, diff --git a/src/models/pulls.rs b/src/models/pulls.rs index 8ed188eb..68fe289c 100644 --- a/src/models/pulls.rs +++ b/src/models/pulls.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::commits::CommentReactions; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[non_exhaustive] @@ -229,6 +230,8 @@ pub struct Review { #[serde(rename = "_links")] #[serde(skip_serializing_if = "Option::is_none")] pub links: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub author_association: Option, } #[derive(Debug, Copy, Clone, PartialEq, Serialize)] @@ -243,6 +246,15 @@ pub enum ReviewState { Dismissed, } +#[derive(Debug, Copy, Clone, PartialEq, Serialize)] +#[serde(rename_all(serialize = "SCREAMING_SNAKE_CASE"))] +#[non_exhaustive] +pub enum ReviewAction { + Approve, + RequestChanges, + Comment, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[non_exhaustive] pub struct Comment { @@ -274,6 +286,53 @@ pub struct Comment { pub side: Option, } +///Legacy Review Comment +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[non_exhaustive] +pub struct ReviewComment { + pub url: Url, + pub pull_request_review_id: Option, + pub id: CommentId, + pub node_id: String, + pub diff_hunk: String, + pub path: String, + pub position: Option, + pub original_position: Option, + pub commit_id: String, + pub original_commit_id: String, + #[serde(default)] + pub in_reply_to_id: Option, + pub user: Option, + pub body: String, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub html_url: String, + pub pull_request_url: String, + pub author_association: AuthorAssociation, + #[serde(rename = "_links")] + pub links: Links, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_text: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub body_html: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub reactions: Option, + pub side: Option, + pub start_side: Option, + pub line: Option, + pub original_line: Option, + pub start_line: Option, + pub original_start_line: Option, +} + +#[derive(Debug, Copy, Clone, PartialEq, Serialize)] +#[serde(rename_all(serialize = "SCREAMING_SNAKE_CASE"))] +#[non_exhaustive] +pub enum Side { + Left, + Right, +} + /// A Thread in a pull request review #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[non_exhaustive] @@ -320,6 +379,41 @@ impl<'de> Deserialize<'de> for ReviewState { } } +//same, see above +impl<'de> Deserialize<'de> for Side { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = Side; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + Ok(match value.to_uppercase().as_str() { + "LEFT" => Side::Left, + "RIGHT" => Side::Right, + unknown => { + return Err(E::custom(format!( + "unknown variant `{unknown}`, expected one of `left`, `right`" + ))) + } + }) + } + } + + deserializer.deserialize_str(Visitor) + } +} + #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] #[non_exhaustive] diff --git a/tests/pull_request_commits_test.rs b/tests/pull_request_commits_test.rs new file mode 100644 index 00000000..8e096928 --- /dev/null +++ b/tests/pull_request_commits_test.rs @@ -0,0 +1,71 @@ +use wiremock::{ + matchers::{method, path}, + Mock, MockServer, ResponseTemplate, +}; + +use mock_error::setup_error_handler; +use octocrab::models::repos::RepoCommit; +use octocrab::Octocrab; + +/// Unit test for calls to the `/repos/OWNER/REPO/contributors` endpoint +mod mock_error; + +const OWNER: &str = "XAMPPRocky"; +const REPO: &str = "octocrab"; +const PULL_NUMBER: u64 = 42; + +async fn setup_api(template: ResponseTemplate) -> MockServer { + let mock_server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/commits" + ))) + .respond_with(template) + .mount(&mock_server) + .await; + setup_error_handler( + &mock_server, + "GET on /repos/OWNER/REPO/pulls/{PULL_NUMBER}/commits not called", + ) + .await; + mock_server +} + +fn setup_octocrab(uri: &str) -> Octocrab { + Octocrab::builder().base_uri(uri).unwrap().build().unwrap() +} + +#[tokio::test] +async fn should_return_pull_request_commits() { + let pull_request_commits_response: Vec = + serde_json::from_str(include_str!("resources/pull_request_commits.json")).unwrap(); + let template = ResponseTemplate::new(200).set_body_json(&pull_request_commits_response); + let mock_server = setup_api(template).await; + let client = setup_octocrab(&mock_server.uri()); + + let result = client + .pulls(OWNER, REPO) + .pull_number(PULL_NUMBER) + .page(0u32) + .commits() + .await; + + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); + + let commits = result.unwrap(); + + assert!(!commits.items.is_empty()); + assert!(commits.items.first().unwrap().author.is_some()); + assert!(commits.items.first().unwrap().committer.is_some()); + + let RepoCommit { author, .. } = commits.items.first().unwrap(); + + { + assert_eq!(author.clone().unwrap().login, "octocat"); + } +} diff --git a/tests/pull_request_review_comment_test.rs b/tests/pull_request_review_comment_test.rs new file mode 100644 index 00000000..c062b8f1 --- /dev/null +++ b/tests/pull_request_review_comment_test.rs @@ -0,0 +1,77 @@ +use wiremock::matchers::{method, path}; +use wiremock::{Mock, MockServer, ResponseTemplate}; + +use octocrab::models::pulls::Comment; +use octocrab::models::CommentId; +use octocrab::Octocrab; + +use crate::mock_error::setup_error_handler; + +/// Unit test for calls to the `/repos/{owner}/{repo}/pulls/comments/{comment_id}` endpoint +mod mock_error; + +const OWNER: &str = "XAMPPRocky"; +const REPO: &str = "octocrab"; +const COMMENT_ID: u64 = 42; + +fn setup_octocrab(uri: &str) -> Octocrab { + Octocrab::builder().base_uri(uri).unwrap().build().unwrap() +} + +#[tokio::test] +async fn should_work_with_review_comment() { + let review_comment_response: Comment = + serde_json::from_str(include_str!("resources/pull_request_review_comment.json")).unwrap(); + let template = ResponseTemplate::new(200).set_body_json(&review_comment_response); + let mock_server = MockServer::start().await; + Mock::given(method("GET")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/comments/{COMMENT_ID}" + ))) + .respond_with(template.clone()) + .mount(&mock_server) + .await; + Mock::given(method("PATCH")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/comments/{COMMENT_ID}" + ))) + .respond_with(template.clone()) + .mount(&mock_server) + .await; + Mock::given(method("DELETE")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/comments/{COMMENT_ID}" + ))) + .respond_with(ResponseTemplate::new(204)) + .mount(&mock_server) + .await; + setup_error_handler( + &mock_server, + &format!("request on /repos/{OWNER}/{REPO}/pulls/comments/{COMMENT_ID} was not received"), + ) + .await; + let client = setup_octocrab(&mock_server.uri()); + + let result = client + .pulls(OWNER, REPO) + .comment(CommentId(COMMENT_ID)) + .get() + .await; + assert_eq!(result.unwrap(), review_comment_response); + let result = client + .pulls(OWNER, REPO) + .comment(CommentId(COMMENT_ID)) + .update("test") + .await; + assert_eq!(result.unwrap(), review_comment_response); + let result = client + .pulls(OWNER, REPO) + .comment(CommentId(COMMENT_ID)) + .delete() + .await; + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); +} diff --git a/tests/pull_request_review_operations_test.rs b/tests/pull_request_review_operations_test.rs new file mode 100644 index 00000000..06ac116f --- /dev/null +++ b/tests/pull_request_review_operations_test.rs @@ -0,0 +1,154 @@ +use wiremock::matchers::{method, path}; +use wiremock::{Mock, MockServer, ResponseTemplate}; + +use octocrab::models::pulls::{Review, ReviewAction, ReviewComment}; +use octocrab::Octocrab; + +use crate::mock_error::setup_error_handler; + +/// Unit test for calls to the `/repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}` endpoint +mod mock_error; + +const OWNER: &str = "XAMPPRocky"; +const REPO: &str = "octocrab"; +const PULL_NUMBER: u64 = 42; +const REVIEW_ID: u64 = 42; +const COMMENT_ID: u64 = 42; + +fn setup_octocrab(uri: &str) -> Octocrab { + Octocrab::builder().base_uri(uri).unwrap().build().unwrap() +} + +#[tokio::test] +async fn should_work_with_specific_review() { + let review_ops_response: Review = + serde_json::from_str(include_str!("resources/get_pull_request_review.json")).unwrap(); + let review_comments_response: Vec = serde_json::from_str(include_str!( + "resources/get_pull_request_review_comments.json" + )) + .unwrap(); + let pr_comment_response: ReviewComment = + serde_json::from_str(include_str!("resources/pull_request_review_comment.json")).unwrap(); + let template = ResponseTemplate::new(200).set_body_json(&review_ops_response); + let comments_template = ResponseTemplate::new(200).set_body_json(&review_comments_response); + let pr_comment_template = ResponseTemplate::new(200).set_body_json(&pr_comment_response); + let mock_server = MockServer::start().await; + Mock::given(method("GET")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/reviews/{REVIEW_ID}" + ))) + .respond_with(template.clone()) + .mount(&mock_server) + .await; + Mock::given(method("PATCH")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/reviews/{REVIEW_ID}" + ))) + .respond_with(template.clone()) + .mount(&mock_server) + .await; + Mock::given(method("DELETE")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/reviews/{REVIEW_ID}" + ))) + .respond_with(template.clone()) + .mount(&mock_server) + .await; + Mock::given(method("POST")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/reviews/{REVIEW_ID}/events" + ))) + .respond_with(template.clone()) + .mount(&mock_server) + .await; + Mock::given(method("PUT")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/reviews/{REVIEW_ID}/dismissals" + ))) + .respond_with(template.clone()) + .mount(&mock_server) + .await; + Mock::given(method("GET")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/reviews/{REVIEW_ID}/comments" + ))) + .respond_with(comments_template.clone()) + .mount(&mock_server) + .await; + + Mock::given(method("POST")) + .and(path(format!( + "/repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/comments/{COMMENT_ID}/replies" + ))) + .respond_with(pr_comment_template.clone()) + .mount(&mock_server) + .await; + + setup_error_handler( + &mock_server, + &format!("request on /repos/{OWNER}/{REPO}/pulls/{PULL_NUMBER}/reviews/{REVIEW_ID} was not received"), + ) + .await; + let client = setup_octocrab(&mock_server.uri()); + + let result = client + .pulls(OWNER, REPO) + .pull_number(PULL_NUMBER) + .reviews() + .review(REVIEW_ID) + .get() + .await; + assert_eq!(result.unwrap(), review_ops_response); + let result = client + .pulls(OWNER, REPO) + .pull_number(PULL_NUMBER) + .reviews() + .review(REVIEW_ID) + .update("test") + .await; + assert_eq!(result.unwrap(), review_ops_response); + let result = client + .pulls(OWNER, REPO) + .pull_number(PULL_NUMBER) + .reviews() + .review(REVIEW_ID) + .delete_pending() + .await; + assert_eq!(result.unwrap(), review_ops_response); + let result = client + .pulls(OWNER, REPO) + .pull_number(PULL_NUMBER) + .reviews() + .review(REVIEW_ID) + .submit(ReviewAction::Comment, "test") + .await; + assert_eq!(result.unwrap(), review_ops_response); + let result = client + .pulls(OWNER, REPO) + .pull_number(PULL_NUMBER) + .reviews() + .review(REVIEW_ID) + .dismiss("test") + .await; + assert_eq!(result.unwrap(), review_ops_response); + + let result = client + .pulls(OWNER, REPO) + .pull_number(PULL_NUMBER) + .reviews() + .review(REVIEW_ID) + .list_comments() + .per_page(15) + .send() + .await; + let result_items = result.unwrap(); + assert_eq!(result_items.items, review_comments_response); + + let result = client + .pulls(OWNER, REPO) + .pull_number(PULL_NUMBER) + .comment(COMMENT_ID.into()) + .reply("test") + .await; + assert_eq!(result.unwrap(), pr_comment_response); +} diff --git a/tests/resources/get_pull_request_review.json b/tests/resources/get_pull_request_review.json new file mode 100644 index 00000000..bb3f74eb --- /dev/null +++ b/tests/resources/get_pull_request_review.json @@ -0,0 +1,39 @@ +{ + "id": 80, + "node_id": "MDE3OlB1bGxSZXF1ZXN0UmV2aWV3ODA=", + "user": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "body": "Here is the body for the review.", + "state": "APPROVED", + "html_url": "https://github.com/octocat/Hello-World/pull/12#pullrequestreview-80", + "pull_request_url": "https://api.github.com/repos/octocat/Hello-World/pulls/12", + "_links": { + "html": { + "href": "https://github.com/octocat/Hello-World/pull/12#pullrequestreview-80" + }, + "pull_request": { + "href": "https://api.github.com/repos/octocat/Hello-World/pulls/12" + } + }, + "submitted_at": "2019-11-17T17:43:43Z", + "commit_id": "ecdd80bb57125d7ba9641ffaa4d7d2c19d3f3091", + "author_association": "COLLABORATOR" +} diff --git a/tests/resources/get_pull_request_review_comments.json b/tests/resources/get_pull_request_review_comments.json new file mode 100644 index 00000000..2c266910 --- /dev/null +++ b/tests/resources/get_pull_request_review_comments.json @@ -0,0 +1,52 @@ +[ + { + "url": "https://api.github.com/repos/octocat/Hello-World/pulls/comments/1", + "pull_request_review_id": 42, + "id": 10, + "node_id": "MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDEw", + "diff_hunk": "@@ -16,33 +16,40 @@ public class Connection : IConnection...", + "path": "file1.txt", + "position": 1, + "original_position": 4, + "commit_id": "6dcb09b5b57875f334f61aebed695e2e4193db5e", + "original_commit_id": "9c48853fa3dc5c1c3d6f1f1cd1f2743e72652840", + "in_reply_to_id": 8, + "user": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "body": "Great stuff!", + "created_at": "2011-04-14T16:00:49Z", + "updated_at": "2011-04-14T16:00:49Z", + "html_url": "https://github.com/octocat/Hello-World/pull/1#discussion-diff-1", + "pull_request_url": "https://api.github.com/repos/octocat/Hello-World/pulls/1", + "author_association": "NONE", + "_links": { + "self": { + "href": "https://api.github.com/repos/octocat/Hello-World/pulls/comments/1" + }, + "html": { + "href": "https://github.com/octocat/Hello-World/pull/1#discussion-diff-1" + }, + "pull_request": { + "href": "https://api.github.com/repos/octocat/Hello-World/pulls/1" + } + } + } +] diff --git a/tests/resources/pull_request_commits.json b/tests/resources/pull_request_commits.json new file mode 100644 index 00000000..1c39d6e1 --- /dev/null +++ b/tests/resources/pull_request_commits.json @@ -0,0 +1,80 @@ +[ + { + "url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", + "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", + "node_id": "MDY6Q29tbWl0NmRjYjA5YjViNTc4NzVmMzM0ZjYxYWViZWQ2OTVlMmU0MTkzZGI1ZQ==", + "html_url": "https://github.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e", + "comments_url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/comments", + "commit": { + "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", + "author": { + "name": "Monalisa Octocat", + "email": "support@github.com", + "date": "2011-04-14T16:00:49Z" + }, + "committer": { + "name": "Monalisa Octocat", + "email": "support@github.com", + "date": "2011-04-14T16:00:49Z" + }, + "message": "Fix all the bugs", + "tree": { + "url": "https://api.github.com/repos/octocat/Hello-World/tree/6dcb09b5b57875f334f61aebed695e2e4193db5e", + "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e" + }, + "comment_count": 0, + "verification": { + "verified": false, + "reason": "unsigned", + "signature": null, + "payload": null + } + }, + "author": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "committer": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "parents": [ + { + "url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", + "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e" + } + ] + } +] diff --git a/tests/resources/pull_request_review_comment.json b/tests/resources/pull_request_review_comment.json new file mode 100644 index 00000000..45c12619 --- /dev/null +++ b/tests/resources/pull_request_review_comment.json @@ -0,0 +1,56 @@ +{ + "url": "https://api.github.com/repos/octocat/Hello-World/pulls/comments/1", + "pull_request_review_id": 42, + "id": 10, + "node_id": "MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDEw", + "diff_hunk": "@@ -16,33 +16,40 @@ public class Connection : IConnection...", + "path": "file1.txt", + "position": 1, + "original_position": 4, + "commit_id": "6dcb09b5b57875f334f61aebed695e2e4193db5e", + "original_commit_id": "9c48853fa3dc5c1c3d6f1f1cd1f2743e72652840", + "in_reply_to_id": 8, + "user": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "body": "Great stuff!", + "created_at": "2011-04-14T16:00:49Z", + "updated_at": "2011-04-14T16:00:49Z", + "html_url": "https://github.com/octocat/Hello-World/pull/1#discussion-diff-1", + "pull_request_url": "https://api.github.com/repos/octocat/Hello-World/pulls/1", + "author_association": "NONE", + "_links": { + "self": { + "href": "https://api.github.com/repos/octocat/Hello-World/pulls/comments/1" + }, + "html": { + "href": "https://github.com/octocat/Hello-World/pull/1#discussion-diff-1" + }, + "pull_request": { + "href": "https://api.github.com/repos/octocat/Hello-World/pulls/1" + } + }, + "start_line": 1, + "original_start_line": 1, + "start_side": "RIGHT", + "line": 2, + "original_line": 2, + "side": "RIGHT" +}