#![warn(missing_docs)]
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate log;
#[macro_use]
extern crate hyper;
#[macro_use]
extern crate serde_derive;
extern crate serde;
extern crate serde_json;
extern crate url;
use serde::de::Deserialize;
pub mod branches;
pub mod git;
pub mod users;
pub mod comments;
pub mod review_comments;
pub mod pull_commits;
pub mod keys;
pub mod gists;
pub mod deployments;
pub mod errors;
pub mod hooks;
pub mod issues;
pub mod labels;
pub mod releases;
pub mod repositories;
pub mod statuses;
pub mod pulls;
pub mod search;
pub mod teams;
pub mod organizations;
pub use errors::{Error, ErrorKind, Result};
use gists::{Gists, UserGists};
use search::Search;
use hyper::Client;
use hyper::client::RequestBuilder;
use hyper::method::Method;
use hyper::header::{qitem, Accept, Authorization, ContentLength, UserAgent};
use hyper::mime::Mime;
use hyper::status::StatusCode;
use repositories::{Repository, Repositories, UserRepositories, OrganizationRepositories};
use organizations::{Organization, Organizations, UserOrganizations};
use std::fmt;
use std::io::Read;
use url::Url;
use std::collections::HashMap;
header! { (Link, "Link") => [String] }
const DEFAULT_HOST: &'static str = "https://api.github.com";
#[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()
}
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum SortDirection {
Asc,
Desc,
}
impl fmt::Display for SortDirection {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f,
"{}",
match *self {
SortDirection::Asc => "asc",
SortDirection::Desc => "desc",
})
}
}
impl Default for SortDirection {
fn default() -> SortDirection {
SortDirection::Asc
}
}
#[derive(Debug, PartialEq)]
pub enum Credentials {
None,
Token(String),
Client(String, String),
}
impl Default for Credentials {
fn default() -> Credentials {
Credentials::None
}
}
pub struct Github {
host: String,
agent: String,
client: Client,
credentials: Credentials,
}
impl Github {
pub fn new<A>(agent: A, client: Client, credentials: Credentials) -> Github
where A: Into<String>
{
Github::host(DEFAULT_HOST, agent, client, credentials)
}
pub fn host<H, A>(host: H, agent: A, client: Client, credentials: Credentials) -> Github
where H: Into<String>,
A: Into<String>
{
Github {
host: host.into(),
agent: agent.into(),
client: client,
credentials: credentials,
}
}
pub fn repo<O, R>(&self, owner: O, repo: R) -> Repository
where O: Into<String>,
R: Into<String>
{
Repository::new(self, owner, repo)
}
pub fn user_repos<S>(&self, owner: S) -> UserRepositories
where S: Into<String>
{
UserRepositories::new(self, owner)
}
pub fn repos(&self) -> Repositories {
Repositories::new(self)
}
pub fn org<O>(&self, org: O) -> Organization
where O: Into<String>
{
Organization::new(self, org)
}
pub fn orgs(&self) -> Organizations {
Organizations::new(self)
}
pub fn user_orgs<U>(&self, user: U) -> UserOrganizations
where U: Into<String>
{
UserOrganizations::new(self, user)
}
pub fn user_gists<O>(&self, owner: O) -> UserGists
where O: Into<String>
{
UserGists::new(self, owner)
}
pub fn gists(&self) -> Gists {
Gists::new(self)
}
pub fn search(&self) -> Search {
Search::new(self)
}
pub fn org_repos<O>(&self, org: O) -> OrganizationRepositories
where O: Into<String>
{
OrganizationRepositories::new(self, org)
}
fn authenticate(&self, method: Method, url: String) -> RequestBuilder {
match self.credentials {
Credentials::Token(ref token) => {
self.client
.request(method, &url)
.header(Authorization(format!("token {}", token)))
}
Credentials::Client(ref id, ref secret) => {
let mut parsed = Url::parse(&url).unwrap();
parsed
.query_pairs_mut()
.append_pair("client_id", id)
.append_pair("client_secret", secret);
self.client.request(method, parsed)
}
Credentials::None => self.client.request(method, &url),
}
}
fn iter<'a, D, I>(&'a self, uri: String, into_items: fn(D) -> Vec<I>) -> Result<Iter<'a, D, I>>
where D: Deserialize
{
self.iter_media(uri, into_items, MediaType::Json)
}
fn iter_media<'a, D, I>(&'a self,
uri: String,
into_items: fn(D) -> Vec<I>,
media_type: MediaType)
-> Result<Iter<'a, D, I>>
where D: Deserialize
{
Iter::new(self, self.host.clone() + &uri, into_items, media_type)
}
fn request<D>(&self,
method: Method,
uri: String,
body: Option<&[u8]>,
media_type: MediaType)
-> Result<(Option<Links>, D)>
where D: Deserialize
{
let builder = self.authenticate(method, uri)
.header(UserAgent(self.agent.to_owned()))
.header(Accept(vec![qitem(From::from(media_type))]));
let mut res = (match body {
Some(ref bod) => builder.body(*bod).send(),
_ => builder.send(),
})?;
let mut body = match res.headers.clone().get::<ContentLength>() {
Some(&ContentLength(len)) => String::with_capacity(len as usize),
_ => String::new(),
};
res.read_to_string(&mut body)?;
let links = res.headers
.get::<Link>()
.map(|&Link(ref value)| Links::new(value.to_owned()));
debug!("rec response {:#?} {:#?} {}", res.status, res.headers, body);
match res.status {
StatusCode::Conflict |
StatusCode::BadRequest |
StatusCode::UnprocessableEntity |
StatusCode::Unauthorized |
StatusCode::NotFound |
StatusCode::Forbidden => {
Err(ErrorKind::Fault {
code: res.status,
error: serde_json::from_str::<errors::ClientError>(&body)?,
}
.into())
}
_ => Ok((links, serde_json::from_str::<D>(&body)?)),
}
}
fn request_entity<D>(&self,
method: Method,
uri: String,
body: Option<&[u8]>,
media_type: MediaType)
-> Result<D>
where D: Deserialize
{
self.request(method, uri, body, media_type)
.map(|(_, entity)| entity)
}
fn get<D>(&self, uri: &str) -> Result<D>
where D: Deserialize
{
self.get_media(uri, MediaType::Json)
}
fn get_media<D>(&self, uri: &str, media: MediaType) -> Result<D>
where D: Deserialize
{
self.request_entity(Method::Get, self.host.clone() + uri, None, media)
}
fn delete(&self, uri: &str) -> Result<()> {
match self.request_entity::<()>(Method::Delete,
self.host.clone() + uri,
None,
MediaType::Json) {
Err(Error(ErrorKind::Codec(_), _)) => Ok(()),
otherwise => otherwise,
}
}
fn post<D>(&self, uri: &str, message: &[u8]) -> Result<D>
where D: Deserialize
{
self.request_entity(Method::Post,
self.host.clone() + uri,
Some(message),
MediaType::Json)
}
fn patch_nothing(&self, uri: &str) -> Result<()> {
match self.request_entity::<()>(Method::Patch,
self.host.clone() + uri,
None,
MediaType::Json) {
Err(Error(ErrorKind::Codec(_), _)) => Ok(()),
otherwise => otherwise,
}
}
fn patch_media<D>(&self, uri: &str, message: &[u8], media: MediaType) -> Result<D>
where D: Deserialize
{
self.request_entity(Method::Patch, self.host.clone() + uri, Some(message), media)
}
fn patch<D>(&self, uri: &str, message: &[u8]) -> Result<D>
where D: Deserialize
{
self.patch_media(uri, message, MediaType::Json)
}
fn put<D>(&self, uri: &str, message: &[u8]) -> Result<D>
where D: Deserialize
{
self.request_entity(Method::Put,
self.host.clone() + uri,
Some(message),
MediaType::Json)
}
}
pub struct Iter<'a, D, I> {
github: &'a Github,
next_link: Option<String>,
into_items: fn(D) -> Vec<I>,
items: Vec<I>,
media_type: MediaType,
}
impl<'a, D, I> Iter<'a, D, I>
where D: Deserialize
{
pub fn new(github: &'a Github,
uri: String,
into_items: fn(D) -> Vec<I>,
media_type: MediaType)
-> Result<Iter<'a, D, I>> {
let (links, payload) = github.request::<D>(Method::Get, uri, None, media_type)?;
let mut items = into_items(payload);
items.reverse(); Ok(Iter {
github: github,
next_link: links.and_then(|l| l.next()),
into_items: into_items,
items: items,
media_type: media_type,
})
}
fn set_next(&mut self, next: Option<String>) {
self.next_link = next;
}
}
impl<'a, D, I> Iterator for Iter<'a, D, I>
where D: Deserialize
{
type Item = I;
fn next(&mut self) -> Option<I> {
self.items
.pop()
.or_else(|| {
self.next_link
.clone()
.and_then(|ref next_link| {
self.github
.request::<D>(Method::Get, next_link.to_owned(), None, self.media_type)
.ok()
.and_then(|(links, payload)| {
let mut next_items = (self.into_items)(payload);
next_items.reverse(); self.set_next(links.and_then(|l| l.next()));
self.items = next_items;
self.next()
})
})
})
}
}
#[derive(Debug)]
pub struct Links {
values: HashMap<String, String>,
}
impl Links {
pub fn new<V>(value: V) -> Links
where V: Into<String>
{
let values = value
.into()
.split(",")
.map(|link| {
let parts = link.split(";").collect::<Vec<_>>();
(parts[1]
.to_owned()
.replace(" rel=\"", "")
.replace("\"", ""),
parts[0]
.to_owned()
.replace("<", "")
.replace(">", "")
.replace(" ", ""))
})
.fold(HashMap::new(), |mut acc, (rel, link)| {
acc.insert(rel, link);
acc
});
Links { values: values }
}
pub fn next(&self) -> Option<String> {
self.values.get("next").map(|s| s.to_owned())
}
pub fn prev(&self) -> Option<String> {
self.values.get("prev").map(|s| s.to_owned())
}
pub fn last(&self) -> Option<String> {
self.values.get("last").map(|s| s.to_owned())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_links() {
let links = Links::new(r#"<linknext>; rel="next", <linklast>; rel="last""#);
assert_eq!(links.next(), Some("linknext".to_owned()));
assert_eq!(links.last(), Some("linklast".to_owned()));
}
#[test]
fn default_sort_direction() {
let default: SortDirection = Default::default();
assert_eq!(default, SortDirection::Asc)
}
#[test]
fn default_credentials() {
let default: Credentials = Default::default();
assert_eq!(default, Credentials::None)
}
}