use crate::schema::schema;
use serde::{Deserialize, Serialize};
use crate::api::change::mutations;
use crate::api::client::{ApiError, GitHub};
use crate::api::model::{Capped, NodeId, RepoId, count};
use crate::api::queries::{NumberVariables, NumberVariablesFields, fragments, nodes};
words! {
#[derive(cynic::Enum, Hash)]
pub enum ReactionContent {
pub fn emoji,
pub fn label,
}
ThumbsUp "👍" "+1";
ThumbsDown "👎" "-1";
Laugh "😄" "Laugh";
Hooray "🎉" "Hooray";
Confused "😕" "Confused";
Heart "❤" "Heart";
Rocket "🚀" "Rocket";
Eyes "👀" "Eyes";
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Reaction {
pub content: ReactionContent,
pub count: u64,
pub mine: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Editable {
pub id: NodeId,
pub can_update: bool,
pub can_delete: bool,
pub can_react: bool,
pub reactions: Vec<Reaction>,
}
impl Editable {
pub fn reacted(&self, content: ReactionContent) -> bool {
self.reactions
.iter()
.any(|r| r.content == content && r.mine)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SidebarPermits {
pub labels: bool,
pub assignees: bool,
pub milestone: bool,
pub reviewers: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Named {
pub id: NodeId,
pub name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Written {
Issue,
Pr,
IssueComment,
ReviewComment,
Review,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Edit {
Body {
of: Written,
id: NodeId,
body: String,
},
Delete { of: Written, id: NodeId },
Title { pr: bool, id: NodeId, title: String },
React {
subject: NodeId,
content: ReactionContent,
on: bool,
},
Labels {
subject: NodeId,
add: Vec<Named>,
remove: Vec<Named>,
},
Assignees {
subject: NodeId,
add: Vec<Named>,
remove: Vec<Named>,
},
Reviewers {
pr: NodeId,
users: Vec<Named>,
teams: Vec<Named>,
bots: Vec<Named>,
},
Milestone {
pr: bool,
id: NodeId,
milestone: Option<Named>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Sidebar {
pub labels: Capped<Named>,
pub assignees: Capped<Named>,
pub milestone: Option<Named>,
pub requested: Requested,
pub author: Option<String>,
pub all_labels: Capped<Named>,
pub users: Capped<Named>,
pub milestones: Capped<Named>,
pub teams: Option<Result<Capped<Named>, String>>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Requested {
pub users: Vec<Named>,
pub teams: Vec<Named>,
pub bots: Vec<Named>,
pub left_out: u64,
}
fn ids(list: &[Named]) -> Vec<cynic::Id> {
list.iter().map(|n| n.id.gql()).collect()
}
impl GitHub {
pub async fn edit(&self, edit: &Edit) -> Result<(), ApiError> {
match edit {
Edit::Body { of, id, body } => {
let vars = BodyVariables {
id: id.gql(),
body: body.clone(),
};
match of {
Written::Issue => self.run::<UpdateIssueBody, _>(vars).await,
Written::Pr => self.run::<UpdatePrBody, _>(vars).await,
Written::IssueComment => self.run::<UpdateIssueComment, _>(vars).await,
Written::ReviewComment => self.run::<UpdateReviewComment, _>(vars).await,
Written::Review => self.run::<UpdateReview, _>(vars).await,
}
}
Edit::Delete { of, id } => {
let vars = EditIdVariables { id: id.gql() };
match of {
Written::ReviewComment => self.run::<DeleteReviewComment, _>(vars).await,
Written::Review => self.run::<DeleteReview, _>(vars).await,
_ => self.run::<DeleteIssueComment, _>(vars).await,
}
}
Edit::Title { pr, id, title } => {
let vars = TitleVariables {
id: id.gql(),
title: title.clone(),
};
if *pr {
self.run::<UpdatePrTitle, _>(vars).await
} else {
self.run::<UpdateIssueTitle, _>(vars).await
}
}
Edit::React {
subject,
content,
on,
} => {
let vars = ReactVariables {
subject: subject.gql(),
content: *content,
};
if *on {
self.run::<AddReaction, _>(vars).await
} else {
self.run::<RemoveReaction, _>(vars).await
}
}
Edit::Labels {
subject,
add,
remove,
} => {
self.add_remove::<AddLabels, RemoveLabels>(subject, add, remove)
.await
}
Edit::Assignees {
subject,
add,
remove,
} => {
self.add_remove::<AddAssignees, RemoveAssignees>(subject, add, remove)
.await
}
Edit::Reviewers {
pr,
users,
teams,
bots,
} => {
self.run::<RequestReviews, _>(ReviewersVariables {
pr: pr.gql(),
users: Some(ids(users)),
teams: Some(ids(teams)),
bots: Some(ids(bots)),
})
.await
}
Edit::Milestone { pr, id, milestone } => {
let vars = MilestoneVariables {
id: id.gql(),
milestone: milestone.as_ref().map(|m| m.id.gql()),
};
if *pr {
self.run::<SetPrMilestone, _>(vars).await
} else {
self.run::<SetIssueMilestone, _>(vars).await
}
}
}
}
async fn add_remove<A, R>(
&self,
subject: &NodeId,
add: &[Named],
remove: &[Named],
) -> Result<(), ApiError>
where
A: cynic::MutationBuilder<AddVariables> + serde::de::DeserializeOwned,
R: cynic::MutationBuilder<RemoveVariables> + serde::de::DeserializeOwned,
{
if !add.is_empty() {
let ids = Some(ids(add));
self.run::<A, _>(AddVariables {
subject: subject.gql(),
ids,
})
.await?;
}
if !remove.is_empty() {
let ids = ids(remove);
self.run::<R, _>(RemoveVariables {
subject: subject.gql(),
ids,
})
.await?;
}
Ok(())
}
pub async fn sidebar(&self, repo: &RepoId, number: u64) -> Result<Sidebar, ApiError> {
use cynic::QueryBuilder;
let subject = format!("{repo}#{number}");
let op = SidebarQuery::build(NumberVariables {
owner: repo.owner.clone(),
name: repo.name.clone(),
number: i32::try_from(number).map_err(|_| ApiError::NotFound(subject.clone()))?,
});
let (mut sidebar, org) = self
.find(op, subject, |q| q.repository?.into_sidebar())
.await?;
if let Some(org) = org {
let op = TeamsQuery::build(TeamsVariables { org: org.clone() });
let teams = self.find(op, &org, |q| {
let t = q.organization?.teams;
Some(Capped::from_nodes(t.total_count, t.nodes, |t| {
named(t.id, t.combined_slug)
}))
});
sidebar.teams = Some(teams.await.map_err(|err| err.to_string()));
}
Ok(sidebar)
}
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "ReactionGroup")]
pub struct WireReactionGroup {
pub content: ReactionContent,
pub viewer_has_reacted: bool,
pub reactors: ReactorCount,
}
fragments! {
total_count: i32;
ReactorCount = "ReactorConnection",
}
pub(crate) fn reactions(groups: Option<Vec<WireReactionGroup>>) -> Vec<Reaction> {
(groups.into_iter().flatten())
.map(|g| Reaction {
content: g.content,
count: count(g.reactors.total_count),
mine: g.viewer_has_reacted,
})
.filter(|r| r.count > 0 || r.mine)
.collect()
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "NumberVariables")]
pub struct SidebarQuery {
#[arguments(owner: $owner, name: $name)]
pub repository: Option<SidebarRepo>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Repository", variables = "NumberVariables")]
pub struct SidebarRepo {
#[arguments(number: $number)]
pub issue_or_pull_request: Option<SidebarOf>,
#[arguments(first: 100)]
pub labels: Option<LabelNames>,
#[arguments(first: 100)]
pub assignable_users: UserNames,
#[arguments(first: 100, states: [OPEN])]
pub milestones: Option<MilestoneNames>,
pub owner: SidebarOwner,
}
#[derive(cynic::InlineFragments, Debug)]
#[cynic(graphql_type = "IssueOrPullRequest")]
pub enum SidebarOf {
Issue(SidebarIssue),
PullRequest(SidebarPr),
#[cynic(fallback)]
Other,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Issue")]
pub struct SidebarIssue {
#[arguments(first: 100)]
pub labels: Option<LabelNames>,
#[arguments(first: 100)]
pub assignees: UserNames,
pub milestone: Option<MilestoneNamed>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "PullRequest")]
pub struct SidebarPr {
#[arguments(first: 100)]
pub labels: Option<LabelNames>,
#[arguments(first: 100)]
pub assignees: UserNames,
pub milestone: Option<MilestoneNamed>,
#[arguments(first: 100)]
pub review_requests: Option<RequestNames>,
pub author: Option<crate::api::queries::Actor>,
}
#[derive(cynic::InlineFragments, Debug)]
#[cynic(graphql_type = "RepositoryOwner")]
pub enum SidebarOwner {
Organization(OrgLogin),
#[cynic(fallback)]
Other,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Organization")]
pub struct OrgLogin {
pub login: String,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct TeamsVariables {
pub org: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "TeamsVariables")]
pub struct TeamsQuery {
#[arguments(login: $org)]
pub organization: Option<OrgTeams>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Organization")]
pub struct OrgTeams {
#[arguments(first: 100)]
pub teams: TeamNames,
}
fragments! {
counted:
LabelNames = "LabelConnection" => LabelNamed,
UserNames = "UserConnection" => UserNamed,
MilestoneNames = "MilestoneConnection" => MilestoneNamed,
TeamNames = "TeamConnection" => TeamNamed,
RequestNames = "ReviewRequestConnection" => RequestNamed,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Label")]
pub struct LabelNamed {
pub id: cynic::Id,
pub name: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "User")]
pub struct UserNamed {
pub id: cynic::Id,
pub login: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Bot")]
pub struct BotNamed {
pub id: cynic::Id,
pub login: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Milestone")]
pub struct MilestoneNamed {
pub id: cynic::Id,
pub title: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Team")]
pub struct TeamNamed {
pub id: cynic::Id,
pub combined_slug: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "ReviewRequest")]
pub struct RequestNamed {
pub requested_reviewer: Option<RequestedNamed>,
}
#[derive(cynic::InlineFragments, Debug)]
#[cynic(graphql_type = "RequestedReviewer")]
pub enum RequestedNamed {
User(UserNamed),
Bot(BotNamed),
Team(TeamNamed),
#[cynic(fallback)]
Other,
}
fn named(id: cynic::Id, name: String) -> Named {
Named {
id: id.into(),
name,
}
}
fn label_names(list: Option<LabelNames>) -> Capped<Named> {
list.map_or_else(Capped::default, |l| {
Capped::from_nodes(l.total_count, l.nodes, |l| named(l.id, l.name))
})
}
fn user_names(list: UserNames) -> Capped<Named> {
Capped::from_nodes(list.total_count, list.nodes, |u| named(u.id, u.login))
}
impl SidebarRepo {
fn into_sidebar(self) -> Option<(Sidebar, Option<String>)> {
let (labels, assignees, milestone, requests, author) = match self.issue_or_pull_request? {
SidebarOf::Issue(i) => (i.labels, i.assignees, i.milestone, None, None),
SidebarOf::PullRequest(p) => (
p.labels,
p.assignees,
p.milestone,
p.review_requests,
Some(crate::api::model::author(p.author)),
),
SidebarOf::Other => return None,
};
let mut requested = Requested::default();
if let Some(r) = requests {
let mut shown = 0;
for asked in nodes(r.nodes).filter_map(|r| r.requested_reviewer) {
shown += 1;
match asked {
RequestedNamed::User(u) => requested.users.push(named(u.id, u.login)),
RequestedNamed::Bot(b) => requested.bots.push(named(b.id, b.login)),
RequestedNamed::Team(t) => requested.teams.push(named(t.id, t.combined_slug)),
RequestedNamed::Other => shown -= 1,
}
}
requested.left_out = count(r.total_count).saturating_sub(shown);
}
let org = match self.owner {
SidebarOwner::Organization(o) if author.is_some() => Some(o.login),
_ => None,
};
let sidebar = Sidebar {
labels: label_names(labels),
assignees: user_names(assignees),
milestone: milestone.map(|m| named(m.id, m.title)),
requested,
author,
all_labels: label_names(self.labels),
users: user_names(self.assignable_users),
milestones: self.milestones.map_or_else(Capped::default, |m| {
Capped::from_nodes(m.total_count, m.nodes, |m| named(m.id, m.title))
}),
teams: None,
};
Some((sidebar, org))
}
}
fragments! {
client_mutation_id: Option<String>;
UpdateIssuePayload = "UpdateIssuePayload",
UpdatePrPayload = "UpdatePullRequestPayload",
UpdateIssueCommentPayload = "UpdateIssueCommentPayload",
UpdateReviewCommentPayload = "UpdatePullRequestReviewCommentPayload",
DeleteIssueCommentPayload = "DeleteIssueCommentPayload",
DeleteReviewCommentPayload = "DeletePullRequestReviewCommentPayload",
UpdateReviewPayload = "UpdatePullRequestReviewPayload",
DeleteReviewPayload = "DeletePullRequestReviewPayload",
AddReactionPayload = "AddReactionPayload",
RemoveReactionPayload = "RemoveReactionPayload",
AddLabelsPayload = "AddLabelsToLabelablePayload",
RemoveLabelsPayload = "RemoveLabelsFromLabelablePayload",
AddAssigneesPayload = "AddAssigneesToAssignablePayload",
RemoveAssigneesPayload = "RemoveAssigneesFromAssignablePayload",
RequestReviewsPayload = "RequestReviewsPayload",
}
#[derive(cynic::QueryVariables, Debug)]
pub struct EditIdVariables {
pub id: cynic::Id,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct BodyVariables {
pub id: cynic::Id,
pub body: String,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct TitleVariables {
pub id: cynic::Id,
pub title: String,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct ReactVariables {
pub subject: cynic::Id,
pub content: ReactionContent,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct AddVariables {
pub subject: cynic::Id,
pub ids: Option<Vec<cynic::Id>>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct RemoveVariables {
pub subject: cynic::Id,
pub ids: Vec<cynic::Id>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct ReviewersVariables {
pub pr: cynic::Id,
pub users: Option<Vec<cynic::Id>>,
pub teams: Option<Vec<cynic::Id>>,
pub bots: Option<Vec<cynic::Id>>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct MilestoneVariables {
pub id: cynic::Id,
pub milestone: Option<cynic::Id>,
}
mutations! {
UpdateIssueBody("BodyVariables") update_issue: UpdateIssuePayload { id: $id, body: $body }
UpdatePrBody("BodyVariables") update_pull_request: UpdatePrPayload { pullRequestId: $id, body: $body }
UpdateIssueComment("BodyVariables") update_issue_comment: UpdateIssueCommentPayload { id: $id, body: $body }
UpdateReviewComment("BodyVariables") update_pull_request_review_comment: UpdateReviewCommentPayload { pullRequestReviewCommentId: $id, body: $body }
DeleteIssueComment("EditIdVariables") delete_issue_comment: DeleteIssueCommentPayload { id: $id }
DeleteReviewComment("EditIdVariables") delete_pull_request_review_comment: DeleteReviewCommentPayload { id: $id }
UpdateReview("BodyVariables") update_pull_request_review: UpdateReviewPayload { pullRequestReviewId: $id, body: $body }
DeleteReview("EditIdVariables") delete_pull_request_review: DeleteReviewPayload { pullRequestReviewId: $id }
UpdateIssueTitle("TitleVariables") update_issue: UpdateIssuePayload { id: $id, title: $title }
UpdatePrTitle("TitleVariables") update_pull_request: UpdatePrPayload { pullRequestId: $id, title: $title }
AddReaction("ReactVariables") add_reaction: AddReactionPayload { subjectId: $subject, content: $content }
RemoveReaction("ReactVariables") remove_reaction: RemoveReactionPayload { subjectId: $subject, content: $content }
AddLabels("AddVariables") add_labels_to_labelable: AddLabelsPayload { labelableId: $subject, labelIds: $ids }
RemoveLabels("RemoveVariables") remove_labels_from_labelable: RemoveLabelsPayload { labelableId: $subject, labelIds: $ids }
AddAssignees("AddVariables") add_assignees_to_assignable: AddAssigneesPayload { assignableId: $subject, assigneeIds: $ids }
RemoveAssignees("RemoveVariables") remove_assignees_from_assignable: RemoveAssigneesPayload { assignableId: $subject, assigneeIds: $ids }
RequestReviews("ReviewersVariables") request_reviews: RequestReviewsPayload { pullRequestId: $pr, userIds: $users, teamIds: $teams, botIds: $bots }
SetIssueMilestone("MilestoneVariables") update_issue: UpdateIssuePayload { id: $id, milestoneId: $milestone }
SetPrMilestone("MilestoneVariables") update_pull_request: UpdatePrPayload { pullRequestId: $id, milestoneId: $milestone }
}