#![cfg_attr(test, recursion_limit = "512")]
mod api;
mod auth;
mod error;
mod from_response;
mod page;
pub mod etag;
pub mod models;
pub mod params;
use std::sync::Arc;
use once_cell::sync::Lazy;
use reqwest::Url;
use serde::Serialize;
use snafu::*;
use auth::Auth;
pub use self::{
api::{
actions, activity, current, gitignore, issues, licenses, markdown, orgs, pulls, repos,
search, teams,
},
error::{Error, GitHubError},
from_response::FromResponse,
page::Page,
};
pub type Result<T, E = error::Error> = std::result::Result<T, E>;
const GITHUB_BASE_URL: &str = "https://api.github.com";
static STATIC_INSTANCE: Lazy<arc_swap::ArcSwap<Octocrab>> =
Lazy::new(|| arc_swap::ArcSwap::from_pointee(Octocrab::default()));
pub fn format_preview(preview: impl AsRef<str>) -> String {
format!("application/vnd.github.{}-preview", preview.as_ref())
}
pub fn format_media_type(media_type: impl AsRef<str>) -> String {
let media_type = media_type.as_ref();
let json_suffix = match media_type {
"raw" | "text" | "html" | "full" => "+json",
_ => "",
};
format!("application/vnd.github.v3.{}{}", media_type, json_suffix)
}
pub async fn map_github_error(response: reqwest::Response) -> Result<reqwest::Response> {
if response.status().is_success() {
Ok(response)
} else {
Err(error::Error::GitHub {
source: response
.json::<error::GitHubError>()
.await
.context(error::Http)?,
backtrace: Backtrace::generate(),
})
}
}
pub fn initialise(builder: OctocrabBuilder) -> Result<Arc<Octocrab>> {
Ok(STATIC_INSTANCE.swap(Arc::from(builder.build()?)))
}
pub fn instance() -> Arc<Octocrab> {
STATIC_INSTANCE.load().clone()
}
#[derive(Default)]
pub struct OctocrabBuilder {
auth: Auth,
previews: Vec<&'static str>,
base_url: Option<Url>,
}
impl OctocrabBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn add_preview(mut self, preview: &'static str) -> Self {
self.previews.push(preview);
self
}
pub fn personal_token(mut self, token: String) -> Self {
self.auth = Auth::PersonalToken(token);
self
}
pub fn base_url(mut self, base_url: impl reqwest::IntoUrl) -> Result<Self> {
self.base_url = Some(base_url.into_url().context(crate::error::Http)?);
Ok(self)
}
pub fn build(self) -> Result<Octocrab> {
let mut hmap = reqwest::header::HeaderMap::new();
for preview in &self.previews {
hmap.append(
reqwest::header::ACCEPT,
crate::format_preview(&preview).parse().unwrap(),
);
}
if let Auth::PersonalToken(token) = self.auth {
hmap.append(
reqwest::header::AUTHORIZATION,
format!("Bearer {}", token).parse().unwrap(),
);
}
let client = reqwest::Client::builder()
.user_agent("octocrab")
.default_headers(hmap)
.build()
.context(crate::error::Http)?;
Ok(Octocrab {
client,
base_url: self
.base_url
.unwrap_or_else(|| Url::parse(GITHUB_BASE_URL).unwrap()),
})
}
}
#[derive(Debug, Clone)]
pub struct Octocrab {
client: reqwest::Client,
pub base_url: Url,
}
impl Default for Octocrab {
fn default() -> Self {
Self {
base_url: Url::parse(GITHUB_BASE_URL).unwrap(),
client: reqwest::ClientBuilder::new()
.user_agent("octocrab")
.build()
.unwrap(),
}
}
}
impl Octocrab {
pub fn builder() -> OctocrabBuilder {
OctocrabBuilder::default()
}
}
impl Octocrab {
pub fn actions(&self) -> actions::ActionsHandler {
actions::ActionsHandler::new(self)
}
pub fn current(&self) -> current::CurrentAuthHandler {
current::CurrentAuthHandler::new(self)
}
pub fn activity(&self) -> activity::ActivityHandler {
activity::ActivityHandler::new(self)
}
pub fn gitignore(&self) -> gitignore::GitignoreHandler {
gitignore::GitignoreHandler::new(self)
}
pub fn issues(
&self,
owner: impl Into<String>,
repo: impl Into<String>,
) -> issues::IssueHandler {
issues::IssueHandler::new(self, owner.into(), repo.into())
}
pub fn licenses(&self) -> licenses::LicenseHandler {
licenses::LicenseHandler::new(self)
}
pub fn markdown(&self) -> markdown::MarkdownHandler {
markdown::MarkdownHandler::new(self)
}
pub fn orgs(&self, owner: impl Into<String>) -> orgs::OrgHandler {
orgs::OrgHandler::new(self, owner.into())
}
pub fn pulls(
&self,
owner: impl Into<String>,
repo: impl Into<String>,
) -> pulls::PullRequestHandler {
pulls::PullRequestHandler::new(self, owner.into(), repo.into())
}
pub fn repos(&self, owner: impl Into<String>, repo: impl Into<String>) -> repos::RepoHandler {
repos::RepoHandler::new(self, owner.into(), repo.into())
}
pub fn search(&self) -> search::SearchHandler {
search::SearchHandler::new(self)
}
pub fn teams(&self, owner: impl Into<String>) -> teams::TeamHandler {
teams::TeamHandler::new(self, owner.into())
}
}
impl Octocrab {
pub async fn graphql<R: crate::FromResponse>(
&self,
body: &(impl serde::Serialize + ?Sized),
) -> crate::Result<R> {
self.post(
"/graphql",
Some(&serde_json::json!({
"query": body,
})),
)
.await
}
}
impl Octocrab {
pub async fn post<P: Serialize + ?Sized, R: FromResponse>(
&self,
route: impl AsRef<str>,
body: Option<&P>,
) -> Result<R> {
let response = self._post(self.absolute_url(route)?, body).await?;
R::from_response(crate::map_github_error(response).await?).await
}
pub async fn _post<P: Serialize + ?Sized>(
&self,
url: impl reqwest::IntoUrl,
body: Option<&P>,
) -> Result<reqwest::Response> {
let mut request = self.client.post(url);
if let Some(body) = body {
request = request.json(body);
}
self.execute(request).await
}
pub async fn get<R, A, P>(&self, route: A, parameters: Option<&P>) -> Result<R>
where
A: AsRef<str>,
P: Serialize + ?Sized,
R: FromResponse,
{
let response = self._get(self.absolute_url(route)?, parameters).await?;
R::from_response(crate::map_github_error(response).await?).await
}
pub async fn _get<P: Serialize + ?Sized>(
&self,
url: impl reqwest::IntoUrl,
parameters: Option<&P>,
) -> Result<reqwest::Response> {
let mut request = self.client.get(url);
if let Some(parameters) = parameters {
request = request.query(parameters);
}
self.execute(request).await
}
pub async fn patch<R, A, B>(&self, route: A, body: Option<&B>) -> Result<R>
where
A: AsRef<str>,
B: Serialize + ?Sized,
R: FromResponse,
{
let response = self._patch(self.absolute_url(route)?, body).await?;
R::from_response(crate::map_github_error(response).await?).await
}
pub async fn _patch<B: Serialize + ?Sized>(
&self,
url: impl reqwest::IntoUrl,
parameters: Option<&B>,
) -> Result<reqwest::Response> {
let mut request = self.client.patch(url);
if let Some(parameters) = parameters {
request = request.json(parameters);
}
self.execute(request).await
}
pub async fn put<R, A, B>(&self, route: A, body: Option<&B>) -> Result<R>
where
A: AsRef<str>,
B: Serialize + ?Sized,
R: FromResponse,
{
let response = self._put(self.absolute_url(route)?, body).await?;
R::from_response(crate::map_github_error(response).await?).await
}
pub async fn _put<B: Serialize + ?Sized>(
&self,
url: impl reqwest::IntoUrl,
body: Option<&B>,
) -> Result<reqwest::Response> {
let mut request = self.client.put(url);
if let Some(body) = body {
request = request.json(body);
}
self.execute(request).await
}
pub async fn delete<R, A, P>(&self, route: A, parameters: Option<&P>) -> Result<R>
where
A: AsRef<str>,
P: Serialize + ?Sized,
R: FromResponse,
{
let response = self._delete(self.absolute_url(route)?, parameters).await?;
R::from_response(crate::map_github_error(response).await?).await
}
pub async fn _delete<P: Serialize + ?Sized>(
&self,
url: impl reqwest::IntoUrl,
parameters: Option<&P>,
) -> Result<reqwest::Response> {
let mut request = self.client.delete(url);
if let Some(parameters) = parameters {
request = request.query(parameters);
}
self.execute(request).await
}
pub fn request_builder(
&self,
url: impl reqwest::IntoUrl,
method: reqwest::Method,
) -> reqwest::RequestBuilder {
self.client.request(method, url)
}
pub async fn execute(&self, request: reqwest::RequestBuilder) -> Result<reqwest::Response> {
request.send().await.context(error::Http)
}
}
impl Octocrab {
pub fn absolute_url(&self, url: impl AsRef<str>) -> Result<Url> {
Ok(self
.base_url
.join(url.as_ref())
.context(crate::error::Url)?)
}
pub async fn get_page<R: serde::de::DeserializeOwned>(
&self,
url: &Option<Url>,
) -> crate::Result<Option<Page<R>>> {
match url {
Some(url) => self.get(url, None::<&()>).await.map(Some),
None => Ok(None),
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn absolute_url_escapes() {
assert_eq!(
crate::instance()
.absolute_url("/help wanted")
.unwrap()
.as_str(),
String::from(crate::GITHUB_BASE_URL) + "/help%20wanted"
);
}
}