#![allow(missing_docs)]
#[macro_use]
extern crate error_chain;
extern crate futures;
#[macro_use]
extern crate hyper;
#[cfg(feature = "tls")]
extern crate hyper_tls;
#[macro_use]
extern crate log;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
extern crate tokio_core;
extern crate url;
use std::fmt;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use futures::{future, stream, Future as StdFuture, IntoFuture, Stream as StdStream};
use hyper::client::{Connect, HttpConnector, Request};
use hyper::header::{qitem, Accept, Authorization, Link, Location, RelationType, UserAgent};
use hyper::mime::Mime;
use hyper::{Client, Method, StatusCode};
#[cfg(feature = "tls")]
use hyper_tls::HttpsConnector;
use serde::de::DeserializeOwned;
use tokio_core::reactor::Handle;
use url::Url;
#[macro_use]
mod macros; pub mod activity;
pub mod branches;
pub mod comments;
pub mod deployments;
pub mod errors;
pub mod gists;
pub mod git;
pub mod hooks;
pub mod issues;
pub mod keys;
pub mod labels;
pub mod notifications;
pub mod organizations;
pub mod pull_commits;
pub mod pulls;
pub mod rate_limit;
pub mod releases;
pub mod repositories;
pub mod review_comments;
pub mod search;
pub mod stars;
pub mod statuses;
pub mod teams;
pub mod traffic;
pub mod users;
pub use errors::{Error, ErrorKind, Result};
use activity::Activity;
use gists::{Gists, UserGists};
use organizations::{Organization, Organizations, UserOrganizations};
use rate_limit::RateLimit;
use repositories::{OrganizationRepositories, Repositories, Repository, UserRepositories};
use search::Search;
use users::Users;
const DEFAULT_HOST: &str = "https://api.github.com";
pub type Future<T> = Box<StdFuture<Item = T, Error = Error>>;
pub type Stream<T> = Box<StdStream<Item = T, Error = Error>>;
header! {
#[doc(hidden)]
(XGithubRequestId, "X-GitHub-Request-Id") => [String]
}
header! {
#[doc(hidden)]
(XRateLimitLimit, "X-RateLimit-Limit") => [u16]
}
header! {
#[doc(hidden)]
(XRateLimitRemaining, "X-RateLimit-Remaining") => [u32]
}
header! {
#[doc(hidden)]
(XRateLimitReset, "X-RateLimit-Reset") => [u32]
}
#[derive(Clone, Copy)]
pub enum MediaType {
Json,
Preview(&'static str),
}
impl Default for MediaType {
fn default() -> MediaType {
MediaType::Json
}
}
impl From<MediaType> for Mime {
fn from(media: MediaType) -> Mime {
match media {
MediaType::Json => "application/vnd.github.v3+json".parse().unwrap(),
MediaType::Preview(codename) => {
format!("application/vnd.github.{}-preview+json", codename)
.parse()
.unwrap_or_else(|_| panic!("could not parse media type for preview {}", codename))
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SortDirection {
Asc,
Desc,
}
impl fmt::Display for SortDirection {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
SortDirection::Asc => "asc",
SortDirection::Desc => "desc",
}.fmt(f)
}
}
impl Default for SortDirection {
fn default() -> SortDirection {
SortDirection::Asc
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum Credentials {
Token(String),
Client(String, String),
}
#[derive(Clone, Debug)]
pub struct Github<C>
where
C: Clone + Connect,
{
host: String,
agent: String,
client: Client<C>,
credentials: Option<Credentials>,
}
#[cfg(feature = "tls")]
impl Github<HttpsConnector<HttpConnector>> {
pub fn new<A, C>(agent: A, credentials: C, handle: &Handle) -> Self
where
A: Into<String>,
C: Into<Option<Credentials>>,
{
Self::host(DEFAULT_HOST, agent, credentials, handle)
}
pub fn host<H, A, C>(host: H, agent: A, credentials: C, handle: &Handle) -> Self
where
H: Into<String>,
A: Into<String>,
C: Into<Option<Credentials>>,
{
let connector = HttpsConnector::new(4, handle).unwrap();
let http = Client::configure()
.connector(connector)
.keep_alive(true)
.build(handle);
Self::custom(host, agent, credentials, http)
}
}
impl<C> Github<C>
where
C: Clone + Connect,
{
pub fn custom<H, A, CR>(host: H, agent: A, credentials: CR, http: Client<C>) -> Self
where
H: Into<String>,
A: Into<String>,
CR: Into<Option<Credentials>>,
{
Self {
host: host.into(),
agent: agent.into(),
client: http,
credentials: credentials.into(),
}
}
pub fn rate_limit(&self) -> RateLimit<C> {
RateLimit::new(self.clone())
}
pub fn activity(&self) -> Activity<C> {
Activity::new(self.clone())
}
pub fn repo<O, R>(&self, owner: O, repo: R) -> Repository<C>
where
O: Into<String>,
R: Into<String>,
{
Repository::new(self.clone(), owner, repo)
}
pub fn user_repos<S>(&self, owner: S) -> UserRepositories<C>
where
S: Into<String>,
{
UserRepositories::new(self.clone(), owner)
}
pub fn repos(&self) -> Repositories<C> {
Repositories::new(self.clone())
}
pub fn org<O>(&self, org: O) -> Organization<C>
where
O: Into<String>,
{
Organization::new(self.clone(), org)
}
pub fn orgs(&self) -> Organizations<C> {
Organizations::new(self.clone())
}
pub fn users(&self) -> Users<C> {
Users::new(self.clone())
}
pub fn user_orgs<U>(&self, user: U) -> UserOrganizations<C>
where
U: Into<String>,
{
UserOrganizations::new(self.clone(), user)
}
pub fn user_gists<O>(&self, owner: O) -> UserGists<C>
where
O: Into<String>,
{
UserGists::new(self.clone(), owner)
}
pub fn gists(&self) -> Gists<C> {
Gists::new(self.clone())
}
pub fn search(&self) -> Search<C> {
Search::new(self.clone())
}
pub fn org_repos<O>(&self, org: O) -> OrganizationRepositories<C>
where
O: Into<String>,
{
OrganizationRepositories::new(self.clone(), org)
}
fn request<Out>(
&self,
method: Method,
uri: &str,
body: Option<Vec<u8>>,
media_type: MediaType,
) -> Future<(Option<Link>, Out)>
where
Out: DeserializeOwned + 'static,
{
let url = if let Some(Credentials::Client(ref id, ref secret)) = self.credentials {
let mut parsed = Url::parse(&uri).unwrap();
parsed
.query_pairs_mut()
.append_pair("client_id", id)
.append_pair("client_secret", secret);
parsed.to_string().parse().into_future()
} else {
uri.parse().into_future()
};
let instance = self.clone();
let body2 = body.clone();
let method2 = method.clone();
let response = url.map_err(Error::from).and_then(move |url| {
let mut req = Request::new(method2, url);
{
let headers = req.headers_mut();
headers.set(UserAgent::new(instance.agent.clone()));
headers.set(Accept(vec![qitem(From::from(media_type))]));
if let Some(Credentials::Token(ref token)) = instance.credentials {
headers.set(Authorization(format!("token {}", token)))
}
}
if let Some(body) = body2 {
req.set_body(body)
}
instance.client.request(req).map_err(Error::from)
});
let instance2 = self.clone();
Box::new(response.and_then(move |response| {
if let Some(value) = response.headers().get::<XGithubRequestId>() {
debug!("x-github-request-id: {}", value)
}
if let Some(value) = response.headers().get::<XRateLimitLimit>() {
debug!("x-rate-limit-limit: {}", value.0)
}
let remaining = response
.headers()
.get::<XRateLimitRemaining>()
.map(|val| val.0);
let reset = response.headers().get::<XRateLimitReset>().map(|val| val.0);
if let Some(value) = remaining {
debug!("x-rate-limit-remaining: {}", value)
}
if let Some(value) = reset {
debug!("x-rate-limit-reset: {}", value)
}
let status = response.status();
if StatusCode::MovedPermanently == status || StatusCode::TemporaryRedirect == status {
if let Some(location) = response.headers().get::<Location>() {
debug!("redirect location {:?}", location);
return instance2.request(method, &location.to_string(), body, media_type);
}
}
let link = response.headers().get::<Link>().cloned();
Box::new(response.body().concat2().map_err(Error::from).and_then(
move |response_body| {
if status.is_success() {
debug!(
"response payload {}",
String::from_utf8_lossy(&response_body)
);
serde_json::from_slice::<Out>(&response_body)
.map(|out| (link, out))
.map_err(|error| ErrorKind::Codec(error).into())
} else {
let error = match (remaining, reset) {
(Some(remaining), Some(reset)) if remaining == 0 => {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
ErrorKind::RateLimit {
reset: Duration::from_secs(u64::from(reset) - now),
}
}
_ => ErrorKind::Fault {
code: status,
error: serde_json::from_slice(&response_body)?,
},
};
Err(error.into())
}
},
))
}))
}
fn request_entity<D>(
&self,
method: Method,
uri: &str,
body: Option<Vec<u8>>,
media_type: MediaType,
) -> Future<D>
where
D: DeserializeOwned + 'static,
{
Box::new(
self.request(method, uri, body, media_type)
.map(|(_, entity)| entity),
)
}
fn get<D>(&self, uri: &str) -> Future<D>
where
D: DeserializeOwned + 'static,
{
self.get_media(uri, MediaType::Json)
}
fn get_media<D>(&self, uri: &str, media: MediaType) -> Future<D>
where
D: DeserializeOwned + 'static,
{
self.request_entity(Method::Get, &(self.host.clone() + uri), None, media)
}
fn get_pages<D>(&self, uri: &str) -> Future<(Option<Link>, D)>
where
D: DeserializeOwned + 'static,
{
self.request(Method::Get, &(self.host.clone() + uri), None, MediaType::Json)
}
fn delete(&self, uri: &str) -> Future<()> {
Box::new(self.request_entity::<()>(
Method::Delete,
&(self.host.clone() + uri),
None,
MediaType::Json,
).or_else(|err| match err {
Error(ErrorKind::Codec(_), _) => Ok(()),
otherwise => Err(otherwise),
}))
}
fn post<D>(&self, uri: &str, message: Vec<u8>) -> Future<D>
where
D: DeserializeOwned + 'static,
{
self.request_entity(
Method::Post,
&(self.host.clone() + uri),
Some(message),
MediaType::Json,
)
}
fn patch_no_response(&self, uri: &str, message: Vec<u8>) -> Future<()> {
Box::new(self.patch(uri, message).or_else(|err| match err {
Error(ErrorKind::Codec(_), _) => Ok(()),
err => Err(err),
}))
}
fn patch_media<D>(&self, uri: &str, message: Vec<u8>, media: MediaType) -> Future<D>
where
D: DeserializeOwned + 'static,
{
self.request_entity(Method::Patch, &(self.host.clone() + uri), Some(message), media)
}
fn patch<D>(&self, uri: &str, message: Vec<u8>) -> Future<D>
where
D: DeserializeOwned + 'static,
{
self.patch_media(uri, message, MediaType::Json)
}
fn put_no_response(&self, uri: &str, message: Vec<u8>) -> Future<()> {
Box::new(self.put(uri, message).or_else(|err| match err {
Error(ErrorKind::Codec(_), _) => Ok(()),
err => Err(err),
}))
}
fn put<D>(&self, uri: &str, message: Vec<u8>) -> Future<D>
where
D: DeserializeOwned + 'static,
{
self.request_entity(
Method::Put,
&(self.host.clone() + uri),
Some(message),
MediaType::Json,
)
}
}
fn next_link(l: &Link) -> Option<String> {
l.values()
.into_iter()
.find(|v| v.rel().unwrap_or(&[]).get(0) == Some(&RelationType::Next))
.map(|v| v.link().to_owned())
}
fn unfold<C, D, I>(
github: Github<C>,
first: Future<(Option<Link>, D)>,
into_items: fn(D) -> Vec<I>,
) -> Stream<I>
where
D: DeserializeOwned + 'static,
I: 'static,
C: Clone + Connect,
{
Box::new(
first
.map(move |(link, payload)| {
let mut items = into_items(payload);
items.reverse();
stream::unfold::<_, _, Future<(I, (Option<Link>, Vec<I>))>, _>(
(link, items),
move |(link, mut items)| match items.pop() {
Some(item) => Some(Box::new(future::ok((item, (link, items))))),
_ => link.and_then(|l| next_link(&l)).map(|url| {
let url = Url::parse(&url).unwrap();
let uri = [url.path(), url.query().unwrap_or_default()].join("?");
Box::new(github.get_pages(uri.as_ref()).map(move |(link, payload)| {
let mut items = into_items(payload);
items.reverse();
(items.remove(0), (link, items))
})) as Future<(I, (Option<Link>, Vec<I>))>
}),
},
)
})
.into_stream()
.flatten(),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_sort_direction() {
let default: SortDirection = Default::default();
assert_eq!(default, SortDirection::Asc)
}
}