2026-05-11 00:32:12 +08:00
|
|
|
use reqwest::Method;
|
2026-04-26 16:06:49 +01:00
|
|
|
use serde::{Deserialize, Serialize};
|
2026-04-24 19:22:25 +01:00
|
|
|
|
2026-04-20 22:54:31 +01:00
|
|
|
use crate::query;
|
|
|
|
|
|
2026-04-21 20:30:41 +01:00
|
|
|
pub(crate) mod auth;
|
2026-05-06 01:42:38 +08:00
|
|
|
pub(crate) mod issues;
|
|
|
|
|
#[cfg(debug_assertions)]
|
|
|
|
|
mod mock;
|
2026-04-20 22:54:31 +01:00
|
|
|
pub(crate) mod repo;
|
2026-04-21 11:50:04 +01:00
|
|
|
pub(crate) mod user;
|
2026-04-20 22:54:31 +01:00
|
|
|
|
2026-04-21 11:50:04 +01:00
|
|
|
#[derive(Clone)]
|
2026-04-20 22:54:31 +01:00
|
|
|
pub struct QueryContext {
|
|
|
|
|
pub(crate) http: reqwest::Client,
|
2026-04-24 19:22:25 +01:00
|
|
|
pub(crate) auth: Option<AuthTokens>,
|
2026-04-21 20:30:41 +01:00
|
|
|
pub(crate) github: GithubCredentials,
|
2026-05-06 01:42:38 +08:00
|
|
|
|
|
|
|
|
#[cfg(debug_assertions)]
|
|
|
|
|
pub(crate) should_use_fixtures: bool,
|
2026-04-20 22:54:31 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-26 16:06:49 +01:00
|
|
|
#[derive(Clone, Serialize, Deserialize)]
|
2026-04-24 19:22:25 +01:00
|
|
|
pub(crate) struct AuthTokens {
|
|
|
|
|
pub(crate) access_token: String,
|
2026-04-21 20:30:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub(crate) struct GithubCredentials {
|
2026-04-24 19:22:25 +01:00
|
|
|
pub(crate) base_url: &'static str,
|
2026-04-21 20:30:41 +01:00
|
|
|
pub(crate) client_id: &'static str,
|
2026-04-21 11:50:04 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-24 19:22:25 +01:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub(crate) enum Error {
|
2026-04-21 11:50:04 +01:00
|
|
|
Unauthenticated,
|
2026-05-06 01:42:38 +08:00
|
|
|
#[cfg(debug_assertions)]
|
|
|
|
|
MissingMockFixture(String),
|
2026-04-24 19:22:25 +01:00
|
|
|
Github(GithubError),
|
2026-05-11 00:32:12 +08:00
|
|
|
MalformedResponse(String),
|
2026-04-21 20:30:41 +01:00
|
|
|
HttpError(reqwest::Error),
|
2026-05-11 00:32:12 +08:00
|
|
|
GraphQLError(Vec<graphql_client::Error>),
|
2026-04-21 11:50:04 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-24 19:22:25 +01:00
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub(crate) struct GithubError {
|
|
|
|
|
pub error: String,
|
|
|
|
|
pub error_description: Option<String>,
|
|
|
|
|
pub error_uri: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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<reqwest::RequestBuilder, Error> {
|
|
|
|
|
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")
|
2026-04-25 00:49:50 +01:00
|
|
|
.header("User-Agent", "kennethnym")
|
2026-04-24 19:22:25 +01:00
|
|
|
.bearer_auth(&auth.access_token))
|
|
|
|
|
}
|
2026-05-11 00:32:12 +08:00
|
|
|
|
|
|
|
|
fn github_graphql_request<V>(
|
|
|
|
|
&self,
|
|
|
|
|
request: &graphql_client::QueryBody<V>,
|
|
|
|
|
) -> Result<reqwest::RequestBuilder, Error>
|
|
|
|
|
where
|
|
|
|
|
V: serde::Serialize,
|
|
|
|
|
{
|
|
|
|
|
Ok(self.github_request(Method::POST, "/graphql")?.json(request))
|
|
|
|
|
}
|
2026-04-24 19:22:25 +01:00
|
|
|
}
|
|
|
|
|
|
2026-05-06 01:42:38 +08:00
|
|
|
#[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
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 11:50:04 +01:00
|
|
|
impl query::Context for QueryContext {}
|
2026-04-21 20:30:41 +01:00
|
|
|
|
|
|
|
|
impl From<reqwest::Error> for Error {
|
|
|
|
|
fn from(value: reqwest::Error) -> Self {
|
|
|
|
|
Self::HttpError(value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<serde_json::Error> for Error {
|
|
|
|
|
fn from(value: serde_json::Error) -> Self {
|
2026-05-11 00:32:12 +08:00
|
|
|
Self::MalformedResponse(value.to_string())
|
2026-04-21 20:30:41 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-24 19:22:25 +01:00
|
|
|
|
|
|
|
|
async fn parse_response<T>(res: reqwest::Response) -> Result<T, Error>
|
|
|
|
|
where
|
|
|
|
|
T: serde::de::DeserializeOwned,
|
|
|
|
|
{
|
|
|
|
|
let status = res.status().clone();
|
|
|
|
|
let data = res.bytes().await?;
|
|
|
|
|
|
2026-04-25 00:49:50 +01:00
|
|
|
println!("[query] RES {:?} {:?}", status, str::from_utf8(&data));
|
|
|
|
|
|
2026-04-24 19:22:25 +01:00
|
|
|
if status.is_success() {
|
|
|
|
|
serde_json::from_slice::<T>(&data).map_err(|e| e.into())
|
|
|
|
|
} else {
|
|
|
|
|
serde_json::from_slice::<GithubError>(&data)
|
|
|
|
|
.map_err(|e| e.into())
|
2026-04-25 00:49:50 +01:00
|
|
|
.and_then(|e| {
|
|
|
|
|
println!(
|
|
|
|
|
"[api parse error] invalid json, received: {:?}",
|
|
|
|
|
str::from_utf8(&data),
|
|
|
|
|
);
|
|
|
|
|
Err(Error::Github(e))
|
|
|
|
|
})
|
2026-04-24 19:22:25 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-08 02:23:28 +08:00
|
|
|
|
|
|
|
|
async fn parse_graphql_response<T>(
|
|
|
|
|
res: reqwest::Response,
|
2026-05-11 00:32:12 +08:00
|
|
|
) -> Result<(graphql_client::Response<T>, T), Error>
|
2026-05-08 02:23:28 +08:00
|
|
|
where
|
|
|
|
|
T: serde::de::DeserializeOwned,
|
|
|
|
|
{
|
2026-05-11 00:32:12 +08:00
|
|
|
let mut body: graphql_client::Response<T> = res.json().await?;
|
|
|
|
|
match body.data.take() {
|
2026-05-13 20:02:26 +08:00
|
|
|
| None => Err(Error::GraphQLError(body.errors.unwrap_or_default())),
|
|
|
|
|
| Some(data) => Ok((body, data)),
|
2026-05-11 00:32:12 +08:00
|
|
|
}
|
2026-05-08 02:23:28 +08:00
|
|
|
}
|