#[deny(missing_docs)]
#[macro_use]
extern crate derive_builder;
extern crate futures;
#[macro_use]
extern crate hyper;
#[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;
#[macro_use]
extern crate error_chain;
#[cfg(feature = "tls")]
extern crate hyper_tls;
#[cfg(feature = "tls")]
use hyper_tls::HttpsConnector;
use futures::{Future as StdFuture, IntoFuture, Stream as StdStream, stream};
use futures::future::FutureResult;
use std::borrow::Cow;
use hyper::{Client as HyperClient, Method, Request, StatusCode, Uri};
use hyper::client::{Connect, HttpConnector};
use hyper::header::{Accept, Authorization, ContentType, UserAgent};
use serde::de::DeserializeOwned;
use serde::ser::Serialize;
use std::fmt;
use tokio_core::reactor::Core;
use url::percent_encoding::{PATH_SEGMENT_ENCODE_SET, utf8_percent_encode};
pub mod env;
use env::Env;
pub mod builds;
use builds::Builds;
pub mod jobs;
use jobs::Jobs;
pub mod repos;
use repos::Repos;
pub mod error;
use error::*;
pub use error::{Error, Result};
header! {
#[doc(hidden)]
(TravisApiVersion, "Travis-Api-Version") => [String]
}
const OSS_HOST: &str = "https://api.travis-ci.org";
const PRO_HOST: &str = "https://api.travis-ci.com";
#[derive(Debug, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum State {
Received,
Created,
Started,
Canceled,
Passed,
Failed,
Errored,
}
impl fmt::Display for State {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match *self {
State::Received => "received",
State::Created => "created",
State::Started => "started",
State::Canceled => "canceled",
State::Passed => "passed",
State::Failed => "failed",
State::Errored => "errored",
}
)
}
}
#[derive(Debug, Deserialize, Clone)]
struct Pagination {
count: usize,
first: Page,
next: Option<Page>,
}
#[derive(Debug, Deserialize, Clone)]
struct Page {
#[serde(rename = "@href")]
href: String,
}
#[derive(Clone, Debug)]
pub enum Credential {
Token(String),
Github(String),
}
#[derive(Debug, Serialize)]
struct GithubToken {
github_token: String,
}
#[derive(Debug, Deserialize)]
struct AccessToken {
pub access_token: String,
}
#[derive(Debug, Deserialize, Clone)]
pub struct Branch {
pub name: String,
}
#[derive(Debug, Deserialize, Clone)]
pub struct Owner {
pub id: usize,
pub login: String,
}
pub type Future<T> = Box<StdFuture<Item = T, Error = Error>>;
pub type Stream<T> = Box<stream::Stream<Item = T, Error = Error>>;
pub(crate) fn escape(raw: &str) -> String {
utf8_percent_encode(raw, PATH_SEGMENT_ENCODE_SET).to_string()
}
#[derive(Clone, Debug)]
pub struct Client<C>
where
C: Clone + Connect,
{
http: HyperClient<C>,
credential: Option<Credential>,
host: String,
}
#[cfg(feature = "tls")]
impl Client<HttpsConnector<HttpConnector>> {
pub fn oss(
credential: Option<Credential>,
core: &mut Core,
) -> Result<Self> {
let connector = HttpsConnector::new(4, &core.handle()).unwrap();
let http = HyperClient::configure()
.connector(connector)
.keep_alive(true)
.build(&core.handle());
Client::custom(OSS_HOST, http, credential, core)
}
pub fn pro(
credential: Option<Credential>,
core: &mut Core,
) -> Result<Self> {
let connector = HttpsConnector::new(4, &core.handle()).unwrap();
let http = HyperClient::configure()
.connector(connector)
.keep_alive(true)
.build(&core.handle());
Client::custom(PRO_HOST, http, credential, core)
}
}
impl<C> Client<C>
where
C: Clone + Connect,
{
pub fn custom<H>(
host: H,
http: HyperClient<C>,
credential: Option<Credential>,
core: &mut Core,
) -> Result<Self>
where
H: Into<String>,
{
match credential {
Some(Credential::Github(gh)) => {
let host = host.into();
let http_client = http.clone();
let uri = format!("{host}/auth/github", host = host)
.parse()
.map_err(Error::from)
.into_future();
let response = uri.and_then(move |uri| {
let mut req = Request::new(Method::Post, uri);
{
let mut headers = req.headers_mut();
headers.set(UserAgent::new(
format!("Travis/{}", env!("CARGO_PKG_VERSION")),
));
headers.set(Accept(vec![
"application/vnd.travis-ci.2+json"
.parse()
.unwrap(),
]));
headers.set(ContentType::json());
}
req.set_body(
serde_json::to_vec(
&GithubToken { github_token: gh.to_owned() },
).unwrap(),
);
http_client.request(req).map_err(Error::from)
});
let parse = response.and_then(move |response| {
let status = response.status();
let body = response.body().concat2().map_err(Error::from);
body.and_then(move |body| if status.is_success() {
debug!(
"body {}",
::std::str::from_utf8(&body).unwrap()
);
serde_json::from_slice::<AccessToken>(&body).map_err(
|error| {
ErrorKind::Codec(error).into()
},
)
} else {
if StatusCode::Forbidden == status {
return Err(
ErrorKind::Fault {
code: status,
error: String::from_utf8_lossy(&body)
.into_owned()
.clone(),
}.into(),
);
}
debug!(
"{} err {}",
status,
::std::str::from_utf8(&body).unwrap()
);
match serde_json::from_slice::<ClientError>(&body) {
Ok(error) => Err(
ErrorKind::Fault {
code: status,
error: error.error_message,
}.into(),
),
Err(error) => Err(ErrorKind::Codec(error).into()),
}
})
});
let client = parse.map(move |access| {
Self {
http,
credential: Some(Credential::Token(
access.access_token.to_owned(),
)),
host: host.into(),
}
});
core.run(client)
}
_ => Ok(Self {
http,
credential,
host: host.into(),
}),
}
}
pub fn repos(&self) -> Repos<C> {
Repos { travis: self.clone() }
}
pub fn env<'a, R>(&self, slug: R) -> Env<C>
where
R: Into<Cow<'a, str>>,
{
Env {
travis: &self,
slug: escape(slug.into().as_ref()),
}
}
pub fn builds<'a, R>(&self, slug: R) -> Builds<C>
where
R: Into<Cow<'a, str>>,
{
Builds {
travis: self.clone(),
slug: escape(slug.into().as_ref()),
}
}
pub fn jobs(&self, build_id: usize) -> Jobs<C> {
Jobs {
travis: &self,
build_id: build_id,
}
}
pub(crate) fn patch<T, B>(
&self,
uri: FutureResult<Uri, Error>,
body: B,
) -> Future<T>
where
T: DeserializeOwned + 'static,
B: Serialize,
{
self.request::<T>(
Method::Patch,
Some(serde_json::to_vec(&body).unwrap()),
uri,
)
}
pub(crate) fn post<T, B>(
&self,
uri: FutureResult<Uri, Error>,
body: B,
) -> Future<T>
where
T: DeserializeOwned + 'static,
B: Serialize,
{
self.request::<T>(
Method::Post,
Some(serde_json::to_vec(&body).unwrap()),
uri,
)
}
pub(crate) fn get<T>(&self, uri: FutureResult<Uri, Error>) -> Future<T>
where
T: DeserializeOwned + 'static,
{
self.request::<T>(Method::Get, None, uri)
}
pub(crate) fn delete(&self, uri: FutureResult<Uri, Error>) -> Future<()> {
Box::new(self.request::<()>(Method::Delete, None, uri).then(
|result| {
match result {
Err(Error(ErrorKind::Codec(_), _)) => Ok(()),
otherwise => otherwise,
}
},
))
}
pub(crate) fn request<T>(
&self,
method: Method,
body: Option<Vec<u8>>,
uri: FutureResult<Uri, Error>,
) -> Future<T>
where
T: DeserializeOwned + 'static,
{
let http_client = self.http.clone();
let credential = self.credential.clone();
let response = uri.and_then(move |uri| {
let mut req = Request::new(method, uri);
{
let mut headers = req.headers_mut();
headers.set(UserAgent::new(
format!("Travis/{}", env!("CARGO_PKG_VERSION")),
));
headers.set(TravisApiVersion("3".into()));
headers.set(ContentType::json());
if let Some(Credential::Token(ref token)) = credential {
headers.set(Authorization(format!("token {}", token)))
}
}
for b in body {
req.set_body(b);
}
http_client.request(req).map_err(Error::from)
});
let result = response.and_then(|response| {
let status = response.status();
let body = response.body().concat2().map_err(Error::from);
body.and_then(move |body| if status.is_success() {
debug!("body {}", ::std::str::from_utf8(&body).unwrap());
serde_json::from_slice::<T>(&body).map_err(|error| {
ErrorKind::Codec(error).into()
})
} else {
debug!(
"{} err {}",
status,
::std::str::from_utf8(&body).unwrap()
);
match serde_json::from_slice::<ClientError>(&body) {
Ok(error) => Err(
ErrorKind::Fault {
code: status,
error: error.error_message,
}.into(),
),
Err(error) => Err(ErrorKind::Codec(error).into()),
}
})
});
Box::new(result)
}
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {}
}