diff --git a/src/github/api/mod.rs b/src/github/api/mod.rs index f3b6d48..6dbe3a1 100644 --- a/src/github/api/mod.rs +++ b/src/github/api/mod.rs @@ -92,7 +92,46 @@ impl HttpClient { } } + /// Send a request to the GitHub API and return the response. fn graphql(&self, query: &str, variables: V, org: &str) -> anyhow::Result + where + R: serde::de::DeserializeOwned, + V: serde::Serialize, + { + let res = self.send_graphql_req(query, variables, org)?; + + if let Some(error) = res.errors.first() { + bail!("graphql error: {}", error.message); + } + + read_graphql_data(res) + } + + /// Send a request to the GitHub API and return the response. + /// If the request contains the error type `NOT_FOUND`, this method returns `Ok(None)`. + fn graphql_opt(&self, query: &str, variables: V, org: &str) -> anyhow::Result> + where + R: serde::de::DeserializeOwned, + V: serde::Serialize, + { + let res = self.send_graphql_req(query, variables, org)?; + + if let Some(error) = res.errors.first() { + if error.type_ == Some(GraphErrorType::NotFound) { + return Ok(None); + } + bail!("graphql error: {}", error.message); + } + + read_graphql_data(res) + } + + fn send_graphql_req( + &self, + query: &str, + variables: V, + org: &str, + ) -> anyhow::Result> where R: serde::de::DeserializeOwned, V: serde::Serialize, @@ -105,19 +144,13 @@ impl HttpClient { let resp = self .req(Method::POST, &GitHubUrl::new("graphql", org))? .json(&Request { query, variables }) - .send()? + .send() + .context("failed to send graphql request")? .custom_error_for_status()?; - let res: GraphResult = resp.json_annotated().with_context(|| { + resp.json_annotated().with_context(|| { format!("Failed to decode response body on graphql request with query '{query}'") - })?; - if let Some(error) = res.errors.first() { - bail!("graphql error: {}", error.message); - } else if let Some(data) = res.data { - Ok(data) - } else { - bail!("missing graphql data"); - } + }) } fn rest_paginated(&self, method: &Method, url: &GitHubUrl, mut f: F) -> anyhow::Result<()> @@ -159,6 +192,17 @@ impl HttpClient { } } +fn read_graphql_data(res: GraphResult) -> anyhow::Result +where + R: serde::de::DeserializeOwned, +{ + if let Some(data) = res.data { + Ok(data) + } else { + bail!("missing graphql data"); + } +} + fn allow_not_found(resp: Response, method: Method, url: &str) -> Result<(), anyhow::Error> { match resp.status() { StatusCode::NOT_FOUND => { @@ -180,9 +224,19 @@ struct GraphResult { #[derive(Debug, serde::Deserialize)] struct GraphError { + #[serde(rename = "type")] + type_: Option, message: String, } +#[derive(Debug, serde::Deserialize, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +enum GraphErrorType { + NotFound, + #[serde(other)] + Other, +} + #[derive(serde::Deserialize)] struct GraphNodes { nodes: Vec>, diff --git a/src/github/api/read.rs b/src/github/api/read.rs index 19737a2..fd82880 100644 --- a/src/github/api/read.rs +++ b/src/github/api/read.rs @@ -2,6 +2,7 @@ use crate::github::api::{ BranchProtection, GraphNode, GraphNodes, GraphPageInfo, HttpClient, Login, Repo, RepoTeam, RepoUser, Team, TeamMember, TeamRole, team_node_id, url::GitHubUrl, user_node_id, }; +use anyhow::Context as _; use reqwest::Method; use std::collections::{HashMap, HashSet}; @@ -271,16 +272,19 @@ impl GithubRead for GitHubApiRead { is_archived: bool, } - let result: Wrapper = self.client.graphql( - QUERY, - Params { - owner: org, - name: repo, - }, - org, - )?; + let result: Option = self + .client + .graphql_opt( + QUERY, + Params { + owner: org, + name: repo, + }, + org, + ) + .with_context(|| format!("failed to retrieve repo `{org}/{repo}`"))?; - let repo = result.repository.map(|repo_response| Repo { + let repo = result.and_then(|r| r.repository).map(|repo_response| Repo { node_id: repo_response.id, name: repo.to_string(), description: repo_response.description.unwrap_or_default(), diff --git a/src/utils.rs b/src/utils.rs index 71708d9..6804638 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -13,8 +13,8 @@ impl ResponseExt for Response { match self.error_for_status_ref() { Ok(_) => Ok(self), Err(err) => { - let body = self.text()?; - Err(err).context(format!("Body: {:?}", body)) + let body = self.text().context("failed to read response body")?; + Err(err).context(format!("Body: {body:?}")) } } }