use reqwest::Method; use serde::{Deserialize, Serialize}; use crate::query; pub(crate) mod auth; pub(crate) mod issues; #[cfg(debug_assertions)] mod mock; pub(crate) mod repo; pub(crate) mod user; #[derive(Clone)] pub struct QueryContext { pub(crate) http: reqwest::Client, pub(crate) auth: Option, pub(crate) github: GithubCredentials, #[cfg(debug_assertions)] pub(crate) should_use_fixtures: bool, } #[derive(Clone, Serialize, Deserialize)] pub(crate) struct AuthTokens { pub(crate) access_token: String, } #[derive(Clone)] pub(crate) struct GithubCredentials { pub(crate) base_url: &'static str, pub(crate) client_id: &'static str, } #[derive(Debug)] pub(crate) enum Error { Unauthenticated, #[cfg(debug_assertions)] MissingMockFixture(String), Github(GithubError), MalformedResponse(String), HttpError(reqwest::Error), GraphQLError(Vec), } #[derive(Debug, Deserialize)] pub(crate) struct GithubError { pub error: String, pub error_description: Option, pub error_uri: Option, } impl QueryContext { fn auth(&self) -> Result<&AuthTokens, Error> { self.auth.as_ref().ok_or(Error::Unauthenticated) } fn github_request( &self, method: reqwest::Method, url: &str, ) -> Result { let auth = self.auth()?; Ok(self .http .request(method, format!("{}{}", self.github.base_url, url)) .header("Accept", "application/vnd.github+json") .header("X-GitHub-Api-Version", "2026-03-10") .header("User-Agent", "kennethnym") .bearer_auth(&auth.access_token)) } fn github_graphql_request( &self, request: &graphql_client::QueryBody, ) -> Result where V: serde::Serialize, { Ok(self.github_request(Method::POST, "/graphql")?.json(request)) } } #[cfg(debug_assertions)] pub(crate) fn use_github_fixtures() -> bool { mock::is_enabled() } #[cfg(not(debug_assertions))] pub(crate) fn use_github_fixtures() -> bool { false } impl query::Context for QueryContext {} impl From for Error { fn from(value: reqwest::Error) -> Self { Self::HttpError(value) } } impl From for Error { fn from(value: serde_json::Error) -> Self { Self::MalformedResponse(value.to_string()) } } async fn parse_response(res: reqwest::Response) -> Result where T: serde::de::DeserializeOwned, { let status = res.status().clone(); let data = res.bytes().await?; println!("[query] RES {:?} {:?}", status, str::from_utf8(&data)); if status.is_success() { serde_json::from_slice::(&data).map_err(|e| e.into()) } else { serde_json::from_slice::(&data) .map_err(|e| e.into()) .and_then(|e| { println!( "[api parse error] invalid json, received: {:?}", str::from_utf8(&data), ); Err(Error::Github(e)) }) } } async fn parse_graphql_response( res: reqwest::Response, ) -> Result<(graphql_client::Response, T), Error> where T: serde::de::DeserializeOwned, { let mut body: graphql_client::Response = res.json().await?; match body.data.take() { | None => Err(Error::GraphQLError(body.errors.unwrap_or_default())), | Some(data) => Ok((body, data)), } }