use futures::{Future as StdFuture, Stream as StdStream};
use hyper::{self, Method, StatusCode};
use hyper::client::{Connect, HttpConnector, Request};
use hyper::header::UserAgent;
use log::Level::*;
use regex::{Regex, RegexBuilder};
use serde::de::DeserializeOwned;
use serde_json;
use tokio_core::reactor::Handle;
use super::api::{LeagueRules, Leagues, PvpMatches, Stashes};
use super::error::Error;
pub type Future<T> = Box<StdFuture<Item = T, Error = Error>>;
pub const DEFAULT_API_ROOT: &str = "http://www.pathofexile.com/api";
#[derive(Clone, Debug)]
pub struct Client<C>
where C: Clone + Connect
{
http: hyper::Client<C>,
api_root: String,
user_agent: String,
}
impl Client<HttpConnector> {
pub fn new<A>(user_agent: A, handle: &Handle) -> Self
where A: Into<String>
{
Self::with_api_root(DEFAULT_API_ROOT, user_agent, handle)
}
pub fn with_api_root<R, A>(api_root: R, user_agent: A, handle: &Handle) -> Self
where R: AsRef<str>, A: Into<String>
{
let http = hyper::Client::configure()
.keep_alive(true)
.build(handle);
Self::with_http(http, api_root, user_agent)
}
}
impl<C: Clone + Connect> Client<C> {
pub fn with_http<R, A>(http: hyper::Client<C>, api_root: R, user_agent: A) -> Self
where R: AsRef<str>, A: Into<String>
{
Client {
http,
api_root: api_root.as_ref().trim_right_matches("/").to_owned(),
user_agent: user_agent.into(),
}
}
}
impl<C: Clone + Connect> Client<C> {
#[inline]
pub fn stashes(&self) -> Stashes<C> {
Stashes::new(self.clone())
}
#[inline]
pub fn leagues(&self) -> Leagues<C> {
Leagues::new(self.clone())
}
#[inline]
pub fn league_rules(&self) -> LeagueRules<C> {
LeagueRules::new(self.clone())
}
#[inline]
pub fn pvp_matches(&self) -> PvpMatches<C> {
PvpMatches::new(self.clone())
}
}
impl<C: Clone + Connect> Client<C> {
pub(crate) fn get<U, Out>(&self, url: U) -> Future<Out>
where U: AsRef<str>,
Out: DeserializeOwned + 'static
{
self.request(Method::Get, url)
}
fn request<U, Out>(&self, method: Method, url: U) -> Future<Out>
where U: AsRef<str>,
Out: DeserializeOwned + 'static
{
let url = format!("{}/{}",
self.api_root, url.as_ref().trim_left_matches("/"));
let mut request = Request::new(method.clone(), url.parse().unwrap());
request.headers_mut().set(UserAgent::new(self.user_agent.clone()));
trace!("{} {}", method, url);
let this = self.clone();
Box::new(
this.http.request(request).from_err().and_then(move |resp| {
let status = resp.status();
debug!("HTTP {}{} for {} {}",
status.as_u16(),
status.canonical_reason()
.map(|r| format!(" ({})", r)).unwrap_or_else(String::new),
method, url);
resp.body().concat2().from_err().and_then(move |body| {
if log_enabled!(Debug) {
const MAX_LEN: usize = 2048;
let body_text = String::from_utf8_lossy(&body);
let omitted = body_text.len() - MAX_LEN;
if omitted > 0 {
debug!("Response payload: {}... (and {} more bytes)",
&body_text[..MAX_LEN], omitted);
} else {
debug!("Response payload: {}", body_text);
}
}
if status.is_success() {
return serde_json::from_slice::<Out>(&body).map_err(Error::Json);
}
let body_text = String::from_utf8_lossy(&body);
lazy_static! {
static ref ANGLE_HTML_RE: Regex = RegexBuilder::new("<html")
.case_insensitive(true)
.build().unwrap();
}
let is_maintenance = status == StatusCode::ServiceUnavailable && ANGLE_HTML_RE.is_match(&*body_text)
&& body_text.contains("maintenance");
if is_maintenance {
Err(Error::UnderMaintenance)
} else {
Err(Error::Server(status))
}
})
})
)
}
}