#![cfg_attr(test, recursion_limit = "512")]
mod api;
mod error;
mod from_response;
mod page;
pub mod auth;
pub mod etag;
pub mod models;
pub mod params;
use std::sync::Arc;
use once_cell::sync::Lazy;
use reqwest::{header::HeaderName, Url};
use serde::Serialize;
use snafu::*;
use auth::Auth;
pub use self::{
api::{
actions, activity, apps, current, events, gists, gitignore, issues, licenses, markdown,
orgs, pulls, repos, search, teams, workflows,
},
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>,
extra_headers: Vec<(HeaderName, String)>,
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 add_header(mut self, key: HeaderName, value: String) -> Self {
self.extra_headers.push((key, value));
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(),
);
}
for (key, value) in self.extra_headers.into_iter() {
hmap.append(key, value.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 apps(&self) -> apps::AppsRequestHandler {
apps::AppsRequestHandler::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())
}
pub fn workflows(
&self,
owner: impl Into<String>,
repo: impl Into<String>,
) -> workflows::WorkflowsHandler {
workflows::WorkflowsHandler::new(self, owner.into(), repo.into())
}
pub fn events(&self) -> events::EventsBuilder {
events::EventsBuilder::new(self)
}
pub fn gists(&self) -> gists::GistsHandler {
gists::GistsHandler::new(self)
}
}
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"
);
}
#[test]
fn absolute_url_for_subdir() {
assert_eq!(
crate::OctocrabBuilder::new()
.base_url("https://git.example.com/api/v3/")
.unwrap()
.build()
.unwrap()
.absolute_url("/my/api")
.unwrap()
.as_str(),
String::from("https://git.example.com/my/api")
);
}
#[test]
fn relative_url() {
assert_eq!(
crate::instance().absolute_url("my/api").unwrap().as_str(),
String::from(crate::GITHUB_BASE_URL) + "/my/api"
);
}
#[test]
fn relative_url_for_subdir() {
assert_eq!(
crate::OctocrabBuilder::new()
.base_url("https://git.example.com/api/v3/")
.unwrap()
.build()
.unwrap()
.absolute_url("my/api")
.unwrap()
.as_str(),
String::from("https://git.example.com/api/v3/my/api")
);
}
#[tokio::test]
async fn extra_headers() {
use reqwest::header::HeaderName;
use wiremock::{matchers, Mock, MockServer, ResponseTemplate};
let response = ResponseTemplate::new(304).append_header("etag", "\"abcd\"");
let mock_server = MockServer::start().await;
Mock::given(matchers::method("GET"))
.and(matchers::path_regex(".*"))
.and(matchers::header("x-test1", "hello"))
.and(matchers::header("x-test2", "goodbye"))
.respond_with(response)
.expect(1)
.mount(&mock_server)
.await;
crate::OctocrabBuilder::new()
.base_url(mock_server.uri())
.unwrap()
.add_header(HeaderName::from_static("x-test1"), "hello".to_string())
.add_header(HeaderName::from_static("x-test2"), "goodbye".to_string())
.build()
.unwrap()
.repos("XAMPPRocky", "octocrab")
.events()
.send()
.await
.unwrap();
}
}