diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9b7ac73 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "shiny-robots" +version = "0.1.0" +authors = ["Sam Rijs "] + +[dependencies] +futures = "0.1.18" +hyper = "0.11.15" +hyper-tls = "0.1.2" +serde = "1.0.27" +serde_derive = "1.0.27" +serde_json = "1.0.9" +slog = "2.1.1" +slog-json = "2.2.0" +tokio-core = "0.1.12" +tokio-service = "0.1.0" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d526855 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,74 @@ +#![feature(ascii_ctype)] +#![feature(conservative_impl_trait)] + +extern crate futures; +extern crate hyper; +extern crate hyper_tls; +#[macro_use] extern crate serde_derive; +extern crate serde; +extern crate serde_json; +#[macro_use] extern crate slog; +extern crate slog_json; +extern crate tokio_core; +extern crate tokio_service; + +mod models; +mod robots; +mod serve; + +use std::net::SocketAddr; +use std::sync::Mutex; + +use futures::{Future, Stream}; +use hyper::Client; +use hyper::server::Http; +use hyper_tls::HttpsConnector; +use slog::Drain; +use tokio_core::reactor::Core; + +use self::serve::Serve; + +fn main() { + let logger = slog::Logger::root( + Mutex::new(slog_json::Json::default(std::io::stderr())).map(slog::Fuse), + o!("version" => env!("CARGO_PKG_VERSION")) + ); + + let mut core = Core::new() + .expect("failed to create event loop"); + + let handle = core.handle(); + + let connector = HttpsConnector::new(4, &handle) + .expect("failed to create https connector"); + + let client = Client::configure() + .connector(connector) + .build(&core.handle()); + + let addr = "0.0.0.0:8080".parse::() + .expect("failed to parse socket addr"); + + let http = Http::new(); + + let serve_logger = logger.clone(); + let serve = http.serve_addr_handle(&addr, &handle, move || { + Ok(Serve { + client: client.clone(), + logger: serve_logger.clone() + }) + }).expect("failed to bind server"); + + let serving = serve.for_each(move |conn| { + let conn_logger = logger.clone(); + handle.spawn(conn.then(move |res| { + if let Err(err) = res { + info!(conn_logger, "server connection error: {}", err) + } + Ok(()) + })); + Ok(()) + }); + + core.run(serving).expect("server failed"); +} diff --git a/src/models/crates.rs b/src/models/crates.rs new file mode 100644 index 0000000..0dd0de4 --- /dev/null +++ b/src/models/crates.rs @@ -0,0 +1,28 @@ +use std::str::FromStr; + +pub struct CrateName(String); + +#[derive(Debug)] +pub struct CrateNameValidationError; + +impl AsRef for CrateName { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl FromStr for CrateName { + type Err = CrateNameValidationError; + + fn from_str(input: &str) -> Result { + let is_valid = input.chars().all(|c| { + c.is_ascii_alphanumeric() || c == '_' || c == '-' + }); + + if !is_valid { + Err(CrateNameValidationError) + } else { + Ok(CrateName(input.to_string())) + } + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..30e78d1 --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1 @@ +pub mod crates; diff --git a/src/robots/crates.rs b/src/robots/crates.rs new file mode 100644 index 0000000..3b6f452 --- /dev/null +++ b/src/robots/crates.rs @@ -0,0 +1,52 @@ +use futures::{Future, Stream, IntoFuture, future}; +use hyper::{Error as HyperError, Method, Request, Response, StatusCode}; +use hyper::error::UriError; +use tokio_service::Service; +use serde_json; + +use ::models::crates::CrateName; + +const CRATES_API_BASE_URI: &'static str = "https://crates.io/api/v1"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct CratesVersion { + num: String, + yanked: bool +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct QueryCratesVersionsResponse { + versions: Vec +} + +#[derive(Debug)] +pub enum QueryCratesVersionsError { + Uri(UriError), + Status(StatusCode), + Transport(HyperError), + Decode(serde_json::Error) +} + +pub fn query_crates_versions(service: S, crate_name: &CrateName) -> + impl Future + where S: Service +{ + let uri_future = format!("{}/crates/{}/versions", CRATES_API_BASE_URI, crate_name.as_ref()) + .parse().into_future().map_err(QueryCratesVersionsError::Uri); + + uri_future.and_then(move |uri| { + let request = Request::new(Method::Get, uri); + + service.call(request).map_err(QueryCratesVersionsError::Transport).and_then(|response| { + let status = response.status(); + if !status.is_success() { + future::Either::A(future::err(QueryCratesVersionsError::Status(status))) + } else { + let body_future = response.body().concat2().map_err(QueryCratesVersionsError::Transport); + let decode_future = body_future + .and_then(|body| serde_json::from_slice(&body).map_err(QueryCratesVersionsError::Decode)); + future::Either::B(decode_future) + } + }) + }) +} diff --git a/src/robots/github.rs b/src/robots/github.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/robots/mod.rs b/src/robots/mod.rs new file mode 100644 index 0000000..30e78d1 --- /dev/null +++ b/src/robots/mod.rs @@ -0,0 +1 @@ +pub mod crates; diff --git a/src/serve.rs b/src/serve.rs new file mode 100644 index 0000000..c827d23 --- /dev/null +++ b/src/serve.rs @@ -0,0 +1,42 @@ +use futures::{Future, future}; +use hyper::{Client, Error as HyperError, Request, Response, StatusCode}; +use hyper::client::HttpConnector; +use hyper_tls::HttpsConnector; +use slog::Logger; +use tokio_service::Service; + +use ::robots::crates::query_crates_versions; + +pub struct Serve { + pub client: Client>, + pub logger: Logger +} + +impl Service for Serve { + type Request = Request; + type Response = Response; + type Error = HyperError; + type Future = Box>; + + fn call(&self, req: Request) -> Self::Future { + let crate_name = "hyper".parse().unwrap(); + + let future = query_crates_versions(self.client.clone(), &crate_name).then(|result| { + match result { + Err(err) => { + let mut response = Response::new(); + response.set_status(StatusCode::InternalServerError); + response.set_body(format!("{:?}", err)); + future::Either::A(future::ok(response)) + }, + Ok(crates_response) => { + let mut response = Response::new(); + response.set_body(format!("{:?}", crates_response)); + future::Either::B(future::ok(response)) + } + } + }); + + Box::new(future) + } +}