diff --git a/src/api/issue.rs b/src/api/issue.rs index 14aa177..cf4a07f 100644 --- a/src/api/issue.rs +++ b/src/api/issue.rs @@ -10,11 +10,8 @@ use serde::{Deserialize, Serialize}; use crate::{ api, api::DateTimeUtc, - database::{self, Database, TryFromKeyValue}, - outbound::{ - issues::{IssueState, PullRequestState}, - GitHubIssue, - }, + database::{self, Database, GitHubIssue, TryFromKeyValue}, + outbound::issues::{IssueState, PullRequestState}, }; scalar!(IssueState); @@ -241,7 +238,7 @@ impl IssueQuery { #[cfg(test)] mod tests { - use crate::{api::TestSchema, outbound::GitHubIssue}; + use crate::{api::TestSchema, database::GitHubIssue}; fn create_issues(n: usize) -> Vec { (1..=n) diff --git a/src/api/issue_stat.rs b/src/api/issue_stat.rs index c9c33ed..3026e55 100644 --- a/src/api/issue_stat.rs +++ b/src/api/issue_stat.rs @@ -77,10 +77,10 @@ impl IssueStatQuery { mod tests { use jiff::Timestamp; - use crate::{api::TestSchema, outbound::GitHubIssue}; + use crate::{api::TestSchema, database::GitHubIssue}; fn create_issues(n: usize) -> Vec { - (0..n) + (1..=n) .map(|i| GitHubIssue { number: i.try_into().unwrap(), ..Default::default() diff --git a/src/database.rs b/src/database.rs index 1d28b39..eee6796 100644 --- a/src/database.rs +++ b/src/database.rs @@ -6,13 +6,12 @@ use regex::Regex; use serde::Serialize; pub mod discussion; +pub mod issue; pub(crate) use discussion::DiscussionDbSchema; +pub(crate) use issue::GitHubIssue; -use crate::{ - api::{issue::Issue, pull_request::PullRequest}, - outbound::{GitHubIssue, GitHubPullRequestNode}, -}; +use crate::{api::pull_request::PullRequest, outbound::GitHubPullRequestNode}; const GLOBAL_PARTITION_NAME: &str = "global"; const ISSUE_PARTITION_NAME: &str = "issues"; @@ -86,19 +85,6 @@ impl Database { bail!("Failed to get db value"); } - pub(crate) fn insert_issues( - &self, - resp: Vec, - owner: &str, - name: &str, - ) -> Result<()> { - for item in resp { - let keystr: String = format!("{owner}/{name}#{}", item.number); - Database::insert(&keystr, item, &self.issue_partition)?; - } - Ok(()) - } - pub(crate) fn insert_pull_requests( &self, resp: Vec, @@ -112,15 +98,6 @@ impl Database { Ok(()) } - pub(crate) fn issues(&self, start: Option<&[u8]>, end: Option<&[u8]>) -> Iter { - let start = start.unwrap_or(b"\x00"); - if let Some(end) = end { - Iter::new(self.issue_partition.range(start..end)) - } else { - Iter::new(self.issue_partition.range(start..)) - } - } - pub(crate) fn pull_requests( &self, start: Option<&[u8]>, diff --git a/src/database/issue.rs b/src/database/issue.rs new file mode 100644 index 0000000..7fa1a39 --- /dev/null +++ b/src/database/issue.rs @@ -0,0 +1,397 @@ +use anyhow::{Context, Error, Result}; +use jiff::Timestamp; +use serde::{Deserialize, Serialize}; + +use super::{Database, Iter}; +use crate::api::issue::Issue; +use crate::outbound::issues::{ + IssueState, IssuesRepositoryIssuesNodes, IssuesRepositoryIssuesNodesAssignees, + IssuesRepositoryIssuesNodesAuthor, IssuesRepositoryIssuesNodesAuthor::User as IssueAuthor, + IssuesRepositoryIssuesNodesClosedByPullRequestsReferences, + IssuesRepositoryIssuesNodesClosedByPullRequestsReferencesEdgesNode, + IssuesRepositoryIssuesNodesClosedByPullRequestsReferencesEdgesNodeAuthor::User as PullRequestRefAuthor, + IssuesRepositoryIssuesNodesComments, IssuesRepositoryIssuesNodesCommentsNodes, + IssuesRepositoryIssuesNodesCommentsNodesAuthor::User as IssueCommentsAuthor, + IssuesRepositoryIssuesNodesLabels, IssuesRepositoryIssuesNodesParent, + IssuesRepositoryIssuesNodesProjectItems, IssuesRepositoryIssuesNodesProjectItemsNodes, + IssuesRepositoryIssuesNodesProjectItemsNodesTodoInitiationOption as TodoInitOption, + IssuesRepositoryIssuesNodesProjectItemsNodesTodoPendingDays as TodoPendingDays, + IssuesRepositoryIssuesNodesProjectItemsNodesTodoPriority as TodoPriority, + IssuesRepositoryIssuesNodesProjectItemsNodesTodoSize as TodoSize, + IssuesRepositoryIssuesNodesProjectItemsNodesTodoStatus as TodoStatus, + IssuesRepositoryIssuesNodesSubIssues, IssuesRepositoryIssuesNodesSubIssuesNodes, + IssuesRepositoryIssuesNodesSubIssuesNodesAuthor::User as SubIssueAuthor, PullRequestState, +}; + +impl Database { + pub(crate) fn insert_issues( + &self, + resp: Vec, + owner: &str, + name: &str, + ) -> Result<()> { + for item in resp { + let keystr: String = format!("{owner}/{name}#{}", item.number); + Database::insert(&keystr, item, &self.issue_partition)?; + } + Ok(()) + } + + pub(crate) fn issues(&self, start: Option<&[u8]>, end: Option<&[u8]>) -> Iter { + let start = start.unwrap_or(b"\x00"); + if let Some(end) = end { + Iter::new(self.issue_partition.range(start..end)) + } else { + Iter::new(self.issue_partition.range(start..)) + } + } +} + +#[derive(Debug, Default, Deserialize, Serialize, PartialEq)] +pub(crate) struct GitHubIssue { + pub(crate) id: String, + pub(crate) number: i32, + pub(crate) title: String, + pub(crate) author: String, + pub(crate) body: String, + pub(crate) state: IssueState, + pub(crate) assignees: Vec, + pub(crate) labels: Vec, + pub(crate) comments: GitHubIssueCommentConnection, + pub(crate) project_items: GitHubProjectV2ItemConnection, + pub(crate) sub_issues: GitHubSubIssueConnection, + pub(crate) parent: Option, + pub(crate) url: String, + pub(crate) closed_by_pull_requests: Vec, + pub(crate) created_at: Timestamp, + pub(crate) updated_at: Timestamp, + pub(crate) closed_at: Option, +} + +#[derive(Debug, Default, Deserialize, Serialize, PartialEq)] +pub(crate) struct GitHubIssueCommentConnection { + pub(crate) total_count: i32, + pub(crate) nodes: Vec, +} + +#[derive(Debug, Deserialize, Serialize, PartialEq)] +pub(crate) struct GitHubIssueComment { + pub(crate) id: String, + pub(crate) author: String, + pub(crate) body: String, + pub(crate) created_at: Timestamp, + pub(crate) updated_at: Timestamp, + pub(crate) repository_name: String, + pub(crate) url: String, +} + +#[derive(Debug, Default, Deserialize, Serialize, PartialEq)] +pub(crate) struct GitHubProjectV2ItemConnection { + pub(crate) total_count: i32, + pub(crate) nodes: Vec, +} + +#[derive(Debug, Deserialize, Serialize, PartialEq)] +pub(crate) struct GitHubProjectV2Item { + pub(crate) id: String, + pub(crate) todo_status: Option, + pub(crate) todo_priority: Option, + pub(crate) todo_size: Option, + pub(crate) todo_initiation_option: Option, + pub(crate) todo_pending_days: Option, +} + +#[derive(Debug, Default, Deserialize, Serialize, PartialEq)] +pub(crate) struct GitHubSubIssueConnection { + pub(crate) total_count: i32, + pub(crate) nodes: Vec, +} + +#[derive(Debug, Deserialize, Serialize, PartialEq)] +pub(crate) struct GitHubSubIssue { + pub(crate) id: String, + pub(crate) number: i32, + pub(crate) title: String, + pub(crate) state: IssueState, + pub(crate) author: String, + pub(crate) assignees: Vec, + pub(crate) created_at: Timestamp, + pub(crate) updated_at: Timestamp, + pub(crate) closed_at: Option, +} + +#[derive(Debug, Deserialize, Serialize, PartialEq)] +pub(crate) struct GitHubParentIssue { + pub(crate) id: String, + pub(crate) number: i32, + pub(crate) title: String, +} + +#[derive(Debug, Deserialize, Serialize, PartialEq)] +pub(crate) struct GitHubPullRequestRef { + pub(crate) number: i32, + pub(crate) state: PullRequestState, + pub(crate) author: String, + pub(crate) created_at: Timestamp, + pub(crate) updated_at: Timestamp, + pub(crate) closed_at: Option, + pub(crate) url: String, +} + +/// Convert one single *Issue* of GitHub GraphQL API to our internal data structure (`GitHubIssue`) +impl TryFrom for GitHubIssue { + type Error = Error; + + fn try_from(issue: IssuesRepositoryIssuesNodes) -> Result { + let number: i32 = issue.number.try_into()?; + let author = String::from(issue.author.context("Failed to fetch author of issue.")?); + let comments = issue.comments.try_into()?; + let project_items = issue.project_items.try_into()?; + let sub_issues = issue.sub_issues.try_into()?; + let parent = issue.parent.and_then(|node| node.try_into().ok()); + let closed_by_pull_requests = issue + .closed_by_pull_requests_references + .and_then(|pr| pr.try_into().ok()) + .unwrap_or_default(); + + Ok(Self { + id: issue.id, + number, + title: issue.title, + author, + body: issue.body, + state: issue.state, + assignees: issue.assignees.into(), + labels: issue.labels.map(Vec::::from).unwrap_or_default(), + comments, + project_items, + sub_issues, + parent, + url: issue.url, + closed_by_pull_requests, + created_at: issue.created_at, + updated_at: issue.updated_at, + closed_at: issue.closed_at, + }) + } +} + +impl From for String { + fn from(author: IssuesRepositoryIssuesNodesAuthor) -> Self { + match author { + IssueAuthor(user) => user.login, + _ => String::new(), + } + } +} + +impl From for Vec { + fn from(assignees: IssuesRepositoryIssuesNodesAssignees) -> Self { + assignees + .nodes + .unwrap_or_default() + .into_iter() + .flatten() + .map(|user| user.login) + .collect() + } +} + +impl From for Vec { + fn from(labels: IssuesRepositoryIssuesNodesLabels) -> Self { + labels + .nodes + .unwrap_or_default() + .into_iter() + .flatten() + .map(|label| label.name) + .collect() + } +} + +impl TryFrom for GitHubIssueCommentConnection { + type Error = Error; + + fn try_from(comments: IssuesRepositoryIssuesNodesComments) -> Result { + let total_count = comments.total_count.try_into()?; + + Ok(Self { + total_count, + nodes: comments + .nodes + .unwrap_or_default() + .into_iter() + .flatten() + .map(GitHubIssueComment::from) + .collect(), + }) + } +} + +impl From for GitHubIssueComment { + fn from(comment: IssuesRepositoryIssuesNodesCommentsNodes) -> Self { + Self { + author: match comment.author { + Some(IssueCommentsAuthor(u)) => u.login, + _ => String::new(), + }, + body: comment.body, + created_at: comment.created_at, + id: comment.id, + repository_name: comment.repository.name, + updated_at: comment.updated_at, + url: comment.url, + } + } +} + +impl TryFrom for GitHubProjectV2ItemConnection { + type Error = Error; + + fn try_from(project_items: IssuesRepositoryIssuesNodesProjectItems) -> Result { + let total_count = project_items.total_count.try_into()?; + + Ok(Self { + total_count, + nodes: project_items + .nodes + .unwrap_or_default() + .into_iter() + .flatten() + .map(GitHubProjectV2Item::from) + .collect(), + }) + } +} + +impl From for GitHubProjectV2Item { + fn from(node: IssuesRepositoryIssuesNodesProjectItemsNodes) -> Self { + Self { + id: node.id, + todo_status: node.todo_status.and_then(|status| match status { + TodoStatus::ProjectV2ItemFieldSingleSelectValue(inner) => inner.name, + _ => None, + }), + todo_priority: node.todo_priority.and_then(|priority| match priority { + TodoPriority::ProjectV2ItemFieldSingleSelectValue(inner) => inner.name, + _ => None, + }), + todo_size: node.todo_size.and_then(|size| match size { + TodoSize::ProjectV2ItemFieldSingleSelectValue(inner) => inner.name, + _ => None, + }), + todo_initiation_option: node.todo_initiation_option.and_then(|init| match init { + TodoInitOption::ProjectV2ItemFieldSingleSelectValue(inner) => inner.name, + _ => None, + }), + todo_pending_days: node.todo_pending_days.and_then(|days| match days { + TodoPendingDays::ProjectV2ItemFieldNumberValue(inner) => inner.number, + _ => None, + }), + } + } +} + +impl TryFrom for GitHubSubIssueConnection { + type Error = Error; + + fn try_from(sub_issues: IssuesRepositoryIssuesNodesSubIssues) -> Result { + let total_count = sub_issues.total_count.try_into()?; + let nodes = sub_issues + .nodes + .unwrap_or_default() + .into_iter() + .flatten() + .map(GitHubSubIssue::try_from) + .collect::>>()?; + + Ok(Self { total_count, nodes }) + } +} + +impl TryFrom for GitHubSubIssue { + type Error = Error; + + fn try_from(sub_issue: IssuesRepositoryIssuesNodesSubIssuesNodes) -> Result { + let number = sub_issue.number.try_into()?; + + Ok(Self { + id: sub_issue.id, + number, + title: sub_issue.title, + state: sub_issue.state, + created_at: sub_issue.created_at, + updated_at: sub_issue.updated_at, + closed_at: sub_issue.closed_at, + author: match sub_issue.author { + Some(SubIssueAuthor(u)) => u.login, + _ => String::new(), + }, + assignees: sub_issue + .assignees + .nodes + .unwrap_or_default() + .into_iter() + .flatten() + .map(|n| n.login) + .collect(), + }) + } +} + +impl TryFrom + for Vec +{ + type Error = Error; + + fn try_from( + closing_prs: IssuesRepositoryIssuesNodesClosedByPullRequestsReferences, + ) -> Result { + closing_prs + .edges + .unwrap_or_default() + .into_iter() + .flatten() + .filter_map(|edge| edge.node.map(GitHubPullRequestRef::try_from)) + .collect::>>() + } +} + +impl TryFrom + for GitHubPullRequestRef +{ + type Error = Error; + + fn try_from( + node: IssuesRepositoryIssuesNodesClosedByPullRequestsReferencesEdgesNode, + ) -> std::result::Result { + let number = node.number.try_into()?; + + Ok(Self { + number, + state: node.state, + created_at: node.created_at, + updated_at: node.updated_at, + closed_at: node.closed_at, + author: match node.author { + Some(PullRequestRefAuthor(u)) => u.login, + _ => String::new(), + }, + url: node.url, + }) + } +} + +impl TryFrom for GitHubParentIssue { + type Error = Error; + + fn try_from(parent: IssuesRepositoryIssuesNodesParent) -> Result { + let number = parent.number.try_into()?; + + Ok(Self { + id: parent.id, + number, + title: parent.title, + }) + } +} diff --git a/src/outbound.rs b/src/outbound.rs index 3ee2744..74aec08 100644 --- a/src/outbound.rs +++ b/src/outbound.rs @@ -1,6 +1,6 @@ use std::{sync::Arc, time::Duration}; -use anyhow::{bail, Context, Result}; +use anyhow::{bail, Context, Error, Result}; use graphql_client::{GraphQLQuery, QueryBody, Response as GraphQlResponse}; use jiff::Timestamp; use reqwest::{Client, RequestBuilder, Response}; @@ -9,22 +9,12 @@ use tokio::time; use tracing::error; pub use self::pull_requests::{PullRequestReviewState, PullRequestState as PRPullRequestState}; +use crate::database::issue::GitHubProjectV2Item; use crate::database::DiscussionDbSchema; use crate::{ - database::Database, + database::{issue::GitHubIssue, Database}, outbound::{ - issues::{ - IssueState, IssuesRepositoryIssuesNodesAuthor::User as IssueAuthor, - IssuesRepositoryIssuesNodesClosedByPullRequestsReferencesEdgesNodeAuthor::User as PullRequestRefAuthor, - IssuesRepositoryIssuesNodesCommentsNodesAuthor::User as IssueCommentsAuthor, - IssuesRepositoryIssuesNodesProjectItemsNodesTodoInitiationOption as TodoInitOption, - IssuesRepositoryIssuesNodesProjectItemsNodesTodoPendingDays as TodoPendingDays, - IssuesRepositoryIssuesNodesProjectItemsNodesTodoPriority as TodoPriority, - IssuesRepositoryIssuesNodesProjectItemsNodesTodoSize as TodoSize, - IssuesRepositoryIssuesNodesProjectItemsNodesTodoStatus as TodoStatus, - IssuesRepositoryIssuesNodesSubIssuesNodesAuthor::User as SubIssueAuthor, - PullRequestState, - }, + issues::IssueState, pull_requests::{ PullRequestReviewDecision, PullRequestsRepositoryPullRequestsNodesAuthor::User as PullRequestAuthorUser, @@ -49,7 +39,7 @@ type URI = String; #[graphql( schema_path = "src/outbound/graphql/schema.graphql", query_path = "src/outbound/graphql/issues.graphql", - response_derives = "Debug, Clone" + response_derives = "Debug, Clone, PartialEq" )] pub(crate) struct Issues; @@ -76,44 +66,6 @@ impl Default for IssueState { } } -#[derive(Debug, Default, Deserialize, Serialize)] -pub(super) struct GitHubIssue { - pub(super) id: String, - pub(super) number: i32, - pub(super) title: String, - pub(super) author: String, - pub(super) body: String, - pub(super) state: IssueState, - pub(super) assignees: Vec, - pub(super) labels: Vec, - pub(super) comments: GitHubIssueCommentConnection, - pub(super) project_items: GitHubProjectV2ItemConnection, - pub(super) sub_issues: GitHubSubIssueConnection, - pub(super) parent: Option, - pub(super) url: String, - pub(super) closed_by_pull_requests: Vec, - pub(super) created_at: Timestamp, - pub(super) updated_at: Timestamp, - pub(super) closed_at: Option, -} - -#[derive(Debug, Default, Deserialize, Serialize)] -pub(crate) struct GitHubIssueCommentConnection { - pub(crate) total_count: i32, - pub(crate) nodes: Vec, -} - -#[derive(Debug, Deserialize, Serialize)] -pub(crate) struct GitHubIssueComment { - pub(crate) id: String, - pub(crate) author: String, - pub(crate) body: String, - pub(crate) created_at: Timestamp, - pub(crate) updated_at: Timestamp, - pub(crate) repository_name: String, - pub(crate) url: String, -} - #[derive(Debug, Deserialize, Serialize)] pub(crate) struct GitHubPRComment { pub(crate) author: String, @@ -136,53 +88,6 @@ pub(crate) struct GitHubProjectV2ItemConnection { pub(crate) nodes: Vec, } -#[derive(Debug, Deserialize, Serialize)] -pub(crate) struct GitHubProjectV2Item { - pub(crate) id: String, - pub(crate) todo_status: Option, - pub(crate) todo_priority: Option, - pub(crate) todo_size: Option, - pub(crate) todo_initiation_option: Option, - pub(crate) todo_pending_days: Option, -} - -#[derive(Debug, Default, Deserialize, Serialize)] -pub(crate) struct GitHubSubIssueConnection { - pub(crate) total_count: i32, - pub(crate) nodes: Vec, -} - -#[derive(Debug, Deserialize, Serialize)] -pub(crate) struct GitHubSubIssue { - pub(crate) id: String, - pub(crate) number: i32, - pub(crate) title: String, - pub(crate) state: IssueState, - pub(crate) author: String, - pub(crate) assignees: Vec, - pub(crate) created_at: Timestamp, - pub(crate) updated_at: Timestamp, - pub(crate) closed_at: Option, -} - -#[derive(Debug, Deserialize, Serialize)] -pub(crate) struct GitHubParentIssue { - pub(crate) id: String, - pub(crate) number: i32, - pub(crate) title: String, -} - -#[derive(Debug, Deserialize, Serialize)] -pub(crate) struct GitHubPullRequestRef { - pub(crate) number: i32, - pub(crate) state: PullRequestState, - pub(crate) author: String, - pub(crate) created_at: Timestamp, - pub(crate) updated_at: Timestamp, - pub(crate) closed_at: Option, - pub(crate) url: String, -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub(super) struct CommitInner { pub(super) additions: i32, @@ -334,7 +239,6 @@ pub(super) async fn fetch_periodically( } } -#[allow(clippy::too_many_lines)] async fn send_github_issue_query( owner: &str, name: &str, @@ -342,8 +246,7 @@ async fn send_github_issue_query( token: &str, ) -> Result>> { let mut total_issue = Vec::new(); - let mut end_cur: Option = None; - let mut issues: Vec = Vec::new(); + let mut end_cursor: Option = None; loop { let var = issues::Variables { owner: owner.to_string(), @@ -351,195 +254,21 @@ async fn send_github_issue_query( first: Some(GITHUB_FETCH_SIZE), last: None, before: None, - after: end_cur.take(), + after: end_cursor.take(), since: Some(since.parse::()?), }; let resp_body: GraphQlResponse = send_query::(token, var).await?.json().await?; - if let Some(data) = resp_body.data { - if let Some(repository) = data.repository { - let nodes = repository.issues.nodes.unwrap_or_default(); - for issue in nodes.into_iter().flatten() { - let author = match issue.author.context("Missing issue author")? { - IssueAuthor(u) => u.login, - _ => String::new(), - }; - issues.push(GitHubIssue { - id: issue.id, - number: issue.number.try_into().unwrap_or_default(), - title: issue.title, - author, - body: issue.body, - state: issue.state, - assignees: issue - .assignees - .nodes - .unwrap_or_default() - .into_iter() - .flatten() - .map(|n| n.login) - .collect(), - labels: issue - .labels - .and_then(|l| l.nodes) - .unwrap_or_default() - .into_iter() - .flatten() - .map(|node| node.name) - .collect(), - comments: GitHubIssueCommentConnection { - total_count: issue.comments.total_count.try_into().unwrap_or_default(), - nodes: issue - .comments - .nodes - .unwrap_or_default() - .into_iter() - .flatten() - .map(|comment| GitHubIssueComment { - author: match comment.author { - Some(IssueCommentsAuthor(u)) => u.login, - _ => String::new(), - }, - body: comment.body, - created_at: comment.created_at, - id: comment.id, - repository_name: comment.repository.name, - updated_at: comment.updated_at, - url: comment.url, - }) - .collect(), - }, - project_items: GitHubProjectV2ItemConnection { - total_count: issue - .project_items - .total_count - .try_into() - .unwrap_or_default(), - nodes: issue - .project_items - .nodes - .unwrap_or_default() - .into_iter() - .flatten() - .map(|node| GitHubProjectV2Item { - id: node.id, - todo_status: node.todo_status.and_then(|status| match status { - TodoStatus::ProjectV2ItemFieldSingleSelectValue(inner) => { - inner.name - } - _ => None, - }), - todo_priority: node.todo_priority.and_then(|priority| { - match priority { - TodoPriority::ProjectV2ItemFieldSingleSelectValue( - inner, - ) => inner.name, - _ => None, - } - }), - todo_size: node.todo_size.and_then(|size| match size { - TodoSize::ProjectV2ItemFieldSingleSelectValue(inner) => { - inner.name - } - _ => None, - }), - todo_initiation_option: node.todo_initiation_option.and_then( - |init| match init { - TodoInitOption::ProjectV2ItemFieldSingleSelectValue( - inner, - ) => inner.name, - _ => None, - }, - ), - todo_pending_days: node.todo_pending_days.and_then(|days| { - match days { - TodoPendingDays::ProjectV2ItemFieldNumberValue( - inner, - ) => inner.number, - _ => None, - } - }), - }) - .collect(), - }, - sub_issues: GitHubSubIssueConnection { - total_count: issue - .sub_issues - .total_count - .try_into() - .unwrap_or_default(), - nodes: issue - .sub_issues - .nodes - .unwrap_or_default() - .into_iter() - .flatten() - .map(|si| GitHubSubIssue { - id: si.id, - number: si.number.try_into().unwrap_or_default(), - title: si.title, - state: si.state, - created_at: si.created_at, - updated_at: si.updated_at, - closed_at: si.closed_at, - author: match si.author { - Some(SubIssueAuthor(u)) => u.login, - _ => String::new(), - }, - assignees: si - .assignees - .nodes - .unwrap_or_default() - .into_iter() - .flatten() - .map(|n| n.login) - .collect(), - }) - .collect(), - }, - parent: issue.parent.map(|parent| GitHubParentIssue { - id: parent.id, - number: parent.number.try_into().unwrap_or_default(), - title: parent.title, - }), - url: issue.url, - closed_by_pull_requests: issue - .closed_by_pull_requests_references - .map(|r| r.edges) - .unwrap_or_default() - .into_iter() - .flatten() - .flatten() - .filter_map(|edge| { - edge.node.map(|node| GitHubPullRequestRef { - number: node.number.try_into().unwrap_or_default(), - state: node.state, - created_at: node.created_at, - updated_at: node.updated_at, - closed_at: node.closed_at, - author: match node.author { - Some(PullRequestRefAuthor(u)) => u.login, - _ => String::new(), - }, - url: node.url, - }) - }) - .collect(), - created_at: issue.created_at, - updated_at: issue.updated_at, - closed_at: issue.closed_at, - }); - } - if !repository.issues.page_info.has_next_page { - total_issue.push(issues); - break; - } - end_cur = repository.issues.page_info.end_cursor; - continue; - } - bail!("Failed to parse response data"); + + let issue_resp = GitHubIssueResponse::try_from(resp_body)?; + + if !issue_resp.has_next_page { + total_issue.push(issue_resp.issues); + break; } + end_cursor = issue_resp.end_cursor; } + Ok(total_issue) } #[allow(clippy::too_many_lines)] @@ -683,8 +412,6 @@ async fn send_github_pr_query( }) .unwrap_or_default(), }, - - commits: GitHubCommitConnection { total_count: pr .commits @@ -727,8 +454,6 @@ async fn send_github_pr_query( .collect() }), } - - }); } if !repository.pull_requests.page_info.has_next_page { @@ -810,3 +535,38 @@ where { Ok(request(&T::build_query(var), token)?.send().await?) } + +struct GitHubIssueResponse { + issues: Vec, + has_next_page: bool, + end_cursor: Option, +} + +impl TryFrom> for GitHubIssueResponse { + type Error = Error; + + fn try_from(value: GraphQlResponse) -> Result { + let repo = value + .data + .context("You might send wrong request to GitHub.")? + .repository + .context("No repository was found. Check your request to GitHub.")?; + let nodes = repo + .issues + .nodes + .context("Repository exists, but there is no issue for the repository.")?; + let issues = nodes + .into_iter() + .flatten() + .map(GitHubIssue::try_from) + .collect::>>()?; + let has_next_page = repo.issues.page_info.has_next_page; + let end_cursor = repo.issues.page_info.end_cursor; + + Ok(Self { + issues, + has_next_page, + end_cursor, + }) + } +}