#![deny(missing_docs, non_camel_case_types)]
#![cfg_attr(feature = "clippy", feature(plugin))]
#![cfg_attr(feature = "clippy", plugin(clippy))]
#[cfg(feature = "hyper")]
extern crate hyper;
extern crate futures;
#[macro_use]
extern crate log;
extern crate rand;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
#[cfg(test)]
extern crate tokio_core;
extern crate url;
extern crate url_serde;
mod error;
#[cfg(test)]
mod util;
pub use self::error::{Error, HttpRequestError, Result};
pub use self::model::XkcdResponse;
pub mod comics;
pub mod model;
pub mod random;
use futures::Future;
use serde::Deserialize;
use std::fmt::Debug;
fn parse_xkcd_response<'de, T>(response: &'de str) -> Result<T>
where T: Debug + Deserialize<'de>,
{
let parsed_response = serde_json::from_str(response)?;
trace!("Parsed response: {:?}", parsed_response);
Ok(parsed_response)
}
pub trait XkcdRequestSender {
fn send<'a>(&'a self, method: &str) -> Box<'a + Future<Item = String, Error = HttpRequestError>>;
}
#[cfg(feature = "hyper")]
mod hyper_support {
use error::HttpRequestError;
use futures::{Future, Stream};
use futures::future::result;
use hyper::{self, StatusCode, Uri};
use hyper::client::Connect;
use std::str::{self, FromStr};
use super::XkcdRequestSender;
use url::Url;
impl<C> XkcdRequestSender for hyper::Client<C>
where C: Connect,
{
fn send<'a>(&'a self, method: &str) -> Box<'a + Future<Item = String, Error = HttpRequestError>> {
let url_string = format!("https://xkcd.com/{}", method);
let url = url_string.parse::<Url>().expect("Unable to parse URL");
let uri = Uri::from_str(url.as_ref()).map_err(hyper::Error::from);
let res = result(uri)
.and_then(move |uri| {
trace!("Sending query to URI: {}", uri);
self.get(uri)
})
.map_err(From::from)
.and_then(|res| {
trace!("Response status: {}", res.status());
if let StatusCode::NotFound = res.status() {
Err(HttpRequestError::not_found(url))
} else {
Ok(res)
}
})
.and_then(|res| res.body().concat2().map_err(From::from))
.and_then(|body| {
str::from_utf8(&body)
.map_err(From::from)
.map(|string| string.to_string())
});
Box::new(res)
}
}
impl From<hyper::Error> for HttpRequestError {
fn from(error: hyper::Error) -> HttpRequestError {
match error {
hyper::Error::Io(e) => HttpRequestError::Io(e),
e => HttpRequestError::Other(Box::new(e)),
}
}
}
}
#[cfg(feature = "hyper")]
pub use hyper_support::*;
#[cfg(test)]
mod test_helpers {
use futures::Future;
use futures::future::result;
use super::{HttpRequestError, XkcdRequestSender};
pub struct MockXkcdRequestSender {
response: String,
}
impl MockXkcdRequestSender {
pub fn respond_with<S: Into<String>>(response: S) -> Self {
MockXkcdRequestSender { response: response.into() }
}
}
impl XkcdRequestSender for MockXkcdRequestSender {
fn send(&self, _: &str) -> Box<Future<Item = String, Error = HttpRequestError>> {
Box::new(result(Ok(self.response.clone())))
}
}
}
#[cfg(test)]
mod tests {
use model::XkcdResponse;
use super::parse_xkcd_response;
use url::Url;
use util::read_sample_data_from_path;
#[test]
fn test_parse_xkcd_response() {
let result = read_sample_data_from_path("tests/sample-data/example.json");
let response = parse_xkcd_response::<XkcdResponse>(result.as_str()).unwrap();
assert_eq!(response,
XkcdResponse {
month: 9,
num: 1572,
link: "http://goo.gl/forms/pj0OhX6wfO".to_owned(),
year: 2015,
news: "".to_owned(),
safe_title: "xkcd Survey".to_owned(),
transcript: "Introducing the XKCD SURVEY! A search for weird correlations.\n\nNOTE: This survey is anonymous, but all responses will be posted publicly so people can play with the data.\n\nClick here to take the survey.\n\nhttp:\n\ngoo.gl\nforms\nlzZr7P9Qlm\n\nOr click here, or here. The whole comic is a link because I still haven't gotten the hang of HTML imagemaps.\n\n{{Title text: The xkcd Survey: Big Data for a Big Planet}}".to_owned(),
alt: "The xkcd Survey: Big Data for a Big Planet".to_owned(),
img: "http://imgs.xkcd.com/comics/xkcd_survey.png".to_owned().parse::<Url>().unwrap(),
title: "xkcd Survey".to_owned(),
day: 1,
}
);
}
}