2026-04-24 19:22:25 +01:00
|
|
|
use std::fmt::format;
|
|
|
|
|
|
|
|
|
|
use reqwest::{Response, dns::Resolving};
|
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-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-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-04-24 19:22:25 +01:00
|
|
|
Github(GithubError),
|
2026-04-21 20:30:41 +01:00
|
|
|
MalformedResponse(serde_json::Error),
|
|
|
|
|
HttpError(reqwest::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-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 {
|
|
|
|
|
Self::MalformedResponse(value)
|
|
|
|
|
}
|
|
|
|
|
}
|
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
|
|
|
}
|
|
|
|
|
}
|