query crate versions

This commit is contained in:
Sam Rijs 2018-01-26 14:37:46 +11:00
parent 95228976e0
commit f82e3d0ef6
8 changed files with 214 additions and 0 deletions

16
Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "shiny-robots"
version = "0.1.0"
authors = ["Sam Rijs <srijs@airpost.net>"]
[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"

74
src/main.rs Normal file
View file

@ -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::<SocketAddr>()
.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");
}

28
src/models/crates.rs Normal file
View file

@ -0,0 +1,28 @@
use std::str::FromStr;
pub struct CrateName(String);
#[derive(Debug)]
pub struct CrateNameValidationError;
impl AsRef<str> for CrateName {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl FromStr for CrateName {
type Err = CrateNameValidationError;
fn from_str(input: &str) -> Result<CrateName, CrateNameValidationError> {
let is_valid = input.chars().all(|c| {
c.is_ascii_alphanumeric() || c == '_' || c == '-'
});
if !is_valid {
Err(CrateNameValidationError)
} else {
Ok(CrateName(input.to_string()))
}
}
}

1
src/models/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod crates;

52
src/robots/crates.rs Normal file
View file

@ -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<CratesVersion>
}
#[derive(Debug)]
pub enum QueryCratesVersionsError {
Uri(UriError),
Status(StatusCode),
Transport(HyperError),
Decode(serde_json::Error)
}
pub fn query_crates_versions<S>(service: S, crate_name: &CrateName) ->
impl Future<Item=QueryCratesVersionsResponse, Error=QueryCratesVersionsError>
where S: Service<Request=Request, Response=Response, Error=HyperError>
{
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)
}
})
})
}

0
src/robots/github.rs Normal file
View file

1
src/robots/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod crates;

42
src/serve.rs Normal file
View file

@ -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<HttpsConnector<HttpConnector>>,
pub logger: Logger
}
impl Service for Serve {
type Request = Request;
type Response = Response;
type Error = HyperError;
type Future = Box<Future<Item=Response, Error=HyperError>>;
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)
}
}