This commit is a squash of the following commits:

* Add content-type responsers for JSON, HTML, and plain text.
  * Use content-type responders in content_type example.
  * Conditionally create Request `from` HypRequest.
  * Clean-up dispatching and handling in main rocket.
  * Change Level enum to Logging Level and reexport.
  * Allow users to set logging level before launch.
  * Fix content_type example error handling.
  * Percent decode params when user requests `String`.
This commit is contained in:
Sergio Benitez 2016-08-26 18:37:28 -07:00
parent 90d8621adf
commit a1ad05e879
13 changed files with 139 additions and 91 deletions

View File

@ -4,7 +4,8 @@
extern crate rocket; extern crate rocket;
extern crate serde_json; extern crate serde_json;
use rocket::{Rocket, Request, ContentType, Error}; use rocket::{Rocket, Request, Error};
use rocket::response::JSON;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct Person { struct Person {
@ -13,24 +14,24 @@ struct Person {
} }
#[GET(path = "/<name>/<age>", content = "application/json")] #[GET(path = "/<name>/<age>", content = "application/json")]
fn hello(name: String, age: i8) -> String { fn hello(name: String, age: i8) -> JSON<String> {
let person = Person { let person = Person {
name: name, name: name,
age: age, age: age,
}; };
serde_json::to_string(&person).unwrap() JSON(serde_json::to_string(&person).unwrap())
} }
#[error(code = "404")] #[error(code = "404")]
fn not_found<'r>(error: Error, request: &'r Request<'r>) -> String { fn not_found<'r>(error: Error, request: &'r Request<'r>) -> String {
match error { match error {
Error::NoRoute if !request.content_type().is_json() => { Error::BadMethod if !request.content_type().is_json() => {
format!("<p>This server only supports JSON requests, not '{}'.</p>", format!("<p>This server only supports JSON requests, not '{}'.</p>",
request.content_type()) request.content_type())
} }
Error::NoRoute => { Error::NoRoute => {
format!("<p>Sorry, this server but '{}' is not a valid path!</p> format!("<p>Sorry, '{}' is not a valid path!</p>
<p>Try visiting /hello/&lt;name&gt;/&lt;age&gt; instead.</p>", <p>Try visiting /hello/&lt;name&gt;/&lt;age&gt; instead.</p>",
request.uri()) request.uri())
} }
@ -40,7 +41,6 @@ fn not_found<'r>(error: Error, request: &'r Request<'r>) -> String {
fn main() { fn main() {
let mut rocket = Rocket::new("0.0.0.0", 8000); let mut rocket = Rocket::new("0.0.0.0", 8000);
rocket.mount("/hello", routes![hello]); rocket.mount("/hello", routes![hello]).catch(errors![not_found]);
rocket.catch(errors![not_found]);
rocket.launch(); rocket.launch();
} }

View File

@ -1,9 +1,9 @@
pub use hyper::mime::{Mime, TopLevel, SubLevel}; pub use response::mime::{Mime, TopLevel, SubLevel};
use response::mime::{Param};
use std::str::FromStr; use std::str::FromStr;
use std::borrow::Borrow; use std::borrow::Borrow;
use std::fmt; use std::fmt;
use hyper::mime::{Param};
use self::TopLevel::{Text, Application}; use self::TopLevel::{Text, Application};
use self::SubLevel::{Json, Html}; use self::SubLevel::{Json, Html};

View File

@ -3,5 +3,6 @@ pub enum Error {
BadMethod, BadMethod,
BadParse, BadParse,
NoRoute, // FIXME: Add a chain of routes attempted. NoRoute, // FIXME: Add a chain of routes attempted.
Internal,
NoKey NoKey
} }

View File

@ -1,6 +1,6 @@
use std::str::FromStr; use std::str::FromStr;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr};
use url::{self}; use url;
use error::Error; use error::Error;

View File

@ -29,7 +29,7 @@ pub mod handler {
pub type ErrorHandler = for<'r> fn(error: Error, &'r Request<'r>) -> Response<'r>; pub type ErrorHandler = for<'r> fn(error: Error, &'r Request<'r>) -> Response<'r>;
} }
pub use logger::RocketLogger; pub use logger::{RocketLogger, LoggingLevel};
pub use content_type::ContentType; pub use content_type::ContentType;
pub use codegen::{StaticRouteInfo, StaticCatchInfo}; pub use codegen::{StaticRouteInfo, StaticCatchInfo};
pub use request::Request; pub use request::Request;

View File

@ -2,9 +2,10 @@ use log::{self, Log, LogLevel, LogRecord, LogMetadata};
use term_painter::Color::*; use term_painter::Color::*;
use term_painter::ToStyle; use term_painter::ToStyle;
pub struct RocketLogger(Level); pub struct RocketLogger(LoggingLevel);
pub enum Level { #[derive(PartialEq)]
pub enum LoggingLevel {
/// Only shows errors and warning. /// Only shows errors and warning.
Critical, Critical,
/// Shows everything except debug and trace information. /// Shows everything except debug and trace information.
@ -13,13 +14,13 @@ pub enum Level {
Debug, Debug,
} }
impl Level { impl LoggingLevel {
#[inline(always)] #[inline(always)]
fn max_log_level(&self) -> LogLevel { fn max_log_level(&self) -> LogLevel {
match *self { match *self {
Level::Critical => LogLevel::Warn, LoggingLevel::Critical => LogLevel::Warn,
Level::Normal => LogLevel::Info, LoggingLevel::Normal => LogLevel::Info,
Level::Debug => LogLevel::Trace LoggingLevel::Debug => LogLevel::Trace
} }
} }
} }
@ -55,7 +56,7 @@ impl Log for RocketLogger {
} }
// In Rocket, we abuse target with value "_" to indicate indentation. // In Rocket, we abuse target with value "_" to indicate indentation.
if record.target() == "_" { if record.target() == "_" && self.0 != LoggingLevel::Critical {
print!(" {} ", White.paint("=>")); print!(" {} ", White.paint("=>"));
} }
@ -83,7 +84,7 @@ impl Log for RocketLogger {
} }
} }
pub fn init(level: Level) { pub fn init(level: LoggingLevel) {
let result = log::set_logger(|max_log_level| { let result = log::set_logger(|max_log_level| {
max_log_level.set(level.max_log_level().to_log_level_filter()); max_log_level.set(level.max_log_level().to_log_level_filter());
Box::new(RocketLogger(level)) Box::new(RocketLogger(level))

View File

@ -1,5 +1,6 @@
use std::str::FromStr; use std::str::FromStr;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr};
use url;
use error::Error; use error::Error;
@ -13,6 +14,13 @@ impl<'a> FromParam<'a> for &'a str {
} }
} }
impl<'a> FromParam<'a> for String {
fn from_param(p: &'a str) -> Result<String, Error> {
let decoder = url::percent_encoding::percent_decode(p.as_bytes());
decoder.decode_utf8().map_err(|_| Error::BadParse).map(|s| s.into_owned())
}
}
macro_rules! impl_with_fromstr { macro_rules! impl_with_fromstr {
($($T:ident),+) => ($( ($($T:ident),+) => ($(
impl<'a> FromParam<'a> for $T { impl<'a> FromParam<'a> for $T {
@ -24,5 +32,5 @@ macro_rules! impl_with_fromstr {
} }
impl_with_fromstr!(f32, f64, isize, i8, i16, i32, i64, usize, u8, u16, u32, u64, impl_with_fromstr!(f32, f64, isize, i8, i16, i32, i64, usize, u8, u16, u32, u64,
bool, String, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, bool, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6,
SocketAddr); SocketAddr);

View File

@ -1,5 +1,9 @@
use std::io::{Read}; use std::io::{Read};
use std::cell::RefCell; use std::cell::RefCell;
use std::fmt;
use term_painter::Color::*;
use term_painter::ToStyle;
use error::Error; use error::Error;
use param::FromParam; use param::FromParam;
@ -16,10 +20,10 @@ use router::Route;
use request::{HyperHeaders, HyperRequest}; use request::{HyperHeaders, HyperRequest};
pub struct Request<'a> { pub struct Request<'a> {
pub params: RefCell<Option<Vec<&'a str>>>, // This also sucks.
pub method: Method, pub method: Method,
pub uri: URIBuf, // FIXME: Should be URI (without Hyper). pub uri: URIBuf, // FIXME: Should be URI (without Hyper).
pub data: Vec<u8>, // FIXME: Don't read this! (bad Hyper.) pub data: Vec<u8>, // FIXME: Don't read this! (bad Hyper.)
params: RefCell<Option<Vec<&'a str>>>, // This also sucks.
headers: HyperHeaders, // This sucks. headers: HyperHeaders, // This sucks.
} }
@ -33,7 +37,6 @@ impl<'a> Request<'a> {
} }
} }
#[cfg(test)]
pub fn mock(method: Method, uri: &str) -> Request { pub fn mock(method: Method, uri: &str) -> Request {
Request { Request {
params: RefCell::new(None), params: RefCell::new(None),
@ -44,7 +47,6 @@ impl<'a> Request<'a> {
} }
} }
// FIXME: Get rid of Hyper. // FIXME: Get rid of Hyper.
#[inline(always)] #[inline(always)]
pub fn headers(&self) -> &HyperHeaders { pub fn headers(&self) -> &HyperHeaders {
@ -70,27 +72,38 @@ impl<'a> Request<'a> {
self.headers.set::<header::ContentType>(hyper_ct) self.headers.set::<header::ContentType>(hyper_ct)
} }
} pub fn from_hyp<'h, 'k>(hyper_req: HyperRequest<'h, 'k>)
-> Result<Request<'a>, String> {
impl<'a, 'h, 'k> From<HyperRequest<'h, 'k>> for Request<'a> {
fn from(hyper_req: HyperRequest<'h, 'k>) -> Request<'a> {
let (_, h_method, h_headers, h_uri, _, mut h_body) = hyper_req.deconstruct(); let (_, h_method, h_headers, h_uri, _, mut h_body) = hyper_req.deconstruct();
let uri = match h_uri { let uri = match h_uri {
HyperRequestUri::AbsolutePath(s) => URIBuf::from(s), HyperRequestUri::AbsolutePath(s) => URIBuf::from(s),
_ => panic!("Can only accept absolute paths!") _ => return Err(format!("Bad URI: {}", h_uri))
};
let method = match Method::from_hyp(&h_method) {
Some(m) => m,
_ => return Err(format!("Bad method: {}", h_method))
}; };
// FIXME: GRRR. // FIXME: GRRR.
let mut data = vec![]; let mut data = vec![];
h_body.read_to_end(&mut data).unwrap(); h_body.read_to_end(&mut data).unwrap();
Request { let request = Request {
params: RefCell::new(None), params: RefCell::new(None),
method: Method::from_hyp(&h_method).unwrap(), method: method,
uri: uri, uri: uri,
data: data, data: data,
headers: h_headers, headers: h_headers,
} };
Ok(request)
}
}
impl<'r> fmt::Display for Request<'r> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} {}", Green.paint(&self.method), Blue.paint(&self.uri))
} }
} }

View File

@ -0,0 +1,19 @@
use response::{header, Responder, FreshHyperResponse, Outcome};
use response::mime::{Mime, TopLevel, SubLevel};
macro_rules! impl_data_type_responder {
($name:ident: $top:ident/$sub:ident) => (
pub struct $name<T: Responder>(pub T);
impl<T: Responder> Responder for $name<T> {
fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> Outcome<'b> {
let mime = Mime(TopLevel::$top, SubLevel::$sub, vec![]);
res.headers_mut().set(header::ContentType(mime));
self.0.respond(res)
}
})
}
impl_data_type_responder!(JSON: Application/Json);
impl_data_type_responder!(HTML: Text/Html);
impl_data_type_responder!(Plain: Text/Plain);

View File

@ -4,6 +4,7 @@ mod redirect;
mod with_status; mod with_status;
mod outcome; mod outcome;
mod cookied; mod cookied;
mod data_type;
pub use hyper::server::Response as HyperResponse; pub use hyper::server::Response as HyperResponse;
pub use hyper::net::Fresh as HyperFresh; pub use hyper::net::Fresh as HyperFresh;
@ -11,6 +12,7 @@ pub use hyper::status::StatusCode;
pub use hyper::header; pub use hyper::header;
pub use hyper::mime; pub use hyper::mime;
pub use self::data_type::*;
pub use self::responder::Responder; pub use self::responder::Responder;
pub use self::empty::{Empty, Forward}; pub use self::empty::{Empty, Forward};
pub use self::redirect::Redirect; pub use self::redirect::Redirect;

View File

@ -14,8 +14,11 @@ pub trait Responder {
impl<'a> Responder for &'a str { impl<'a> Responder for &'a str {
fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> Outcome<'b> { fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> Outcome<'b> {
let mime = Mime(TopLevel::Text, SubLevel::Html, vec![]); if res.headers().get::<header::ContentType>().is_none() {
res.headers_mut().set(header::ContentType(mime)); let mime = Mime(TopLevel::Text, SubLevel::Plain, vec![]);
res.headers_mut().set(header::ContentType(mime));
}
res.send(self.as_bytes()).unwrap(); res.send(self.as_bytes()).unwrap();
Outcome::Complete Outcome::Complete
} }
@ -23,8 +26,10 @@ impl<'a> Responder for &'a str {
impl Responder for String { impl Responder for String {
fn respond<'a>(&mut self, mut res: FreshHyperResponse<'a>) -> Outcome<'a> { fn respond<'a>(&mut self, mut res: FreshHyperResponse<'a>) -> Outcome<'a> {
let mime = Mime(TopLevel::Text, SubLevel::Html, vec![]); if res.headers().get::<header::ContentType>().is_none() {
res.headers_mut().set(header::ContentType(mime)); let mime = Mime(TopLevel::Text, SubLevel::Html, vec![]);
res.headers_mut().set(header::ContentType(mime));
}
res.send(self.as_bytes()).unwrap(); res.send(self.as_bytes()).unwrap();
Outcome::Complete Outcome::Complete
} }

View File

@ -3,14 +3,11 @@ use response::FreshHyperResponse;
use request::HyperRequest; use request::HyperRequest;
use catcher; use catcher;
use std::io::Read;
use std::collections::HashMap; use std::collections::HashMap;
use term_painter::Color::*; use term_painter::Color::*;
use term_painter::ToStyle; use term_painter::ToStyle;
use hyper::uri::RequestUri as HyperRequestUri;
use hyper::method::Method as HyperMethod;
use hyper::server::Server as HyperServer; use hyper::server::Server as HyperServer;
use hyper::server::Handler as HyperHandler; use hyper::server::Handler as HyperHandler;
@ -19,91 +16,80 @@ pub struct Rocket {
port: isize, port: isize,
router: Router, router: Router,
catchers: HashMap<u16, Catcher>, catchers: HashMap<u16, Catcher>,
} log_set: bool,
fn uri_is_absolute(uri: &HyperRequestUri) -> bool {
match *uri {
HyperRequestUri::AbsolutePath(_) => true,
_ => false
}
}
fn method_is_valid(method: &HyperMethod) -> bool {
Method::from_hyp(method).is_some()
} }
impl HyperHandler for Rocket { impl HyperHandler for Rocket {
fn handle<'h, 'k>(&self, req: HyperRequest<'h, 'k>, fn handle<'h, 'k>(&self, req: HyperRequest<'h, 'k>,
mut res: FreshHyperResponse<'h>) { mut res: FreshHyperResponse<'h>) {
info!("{:?} '{}':", Green.paint(&req.method), Blue.paint(&req.uri));
let finalize = |mut req: HyperRequest, _res: FreshHyperResponse| {
let mut buf = vec![];
// FIXME: Simple DOS attack here. Working around Hyper bug.
let _ = req.read_to_end(&mut buf);
};
if !uri_is_absolute(&req.uri) {
error_!("Internal failure. Bad URI.");
debug_!("Debug: {}", req.uri);
return finalize(req, res);
}
if !method_is_valid(&req.method) {
error_!("Internal failure. Bad method.");
debug_!("Method: {}", req.method);
return finalize(req, res);
}
res.headers_mut().set(response::header::Server("rocket".to_string())); res.headers_mut().set(response::header::Server("rocket".to_string()));
self.dispatch(req, res) self.dispatch(req, res)
} }
} }
impl Rocket { impl Rocket {
fn dispatch<'h, 'k>(&self, hyper_req: HyperRequest<'h, 'k>, fn dispatch<'h, 'k>(&self, hyp_req: HyperRequest<'h, 'k>,
res: FreshHyperResponse<'h>) { res: FreshHyperResponse<'h>) {
let req = Request::from(hyper_req); // Get a copy of the URI for later use.
let route = self.router.route(&req); let uri = hyp_req.uri.to_string();
if let Some(route) = route {
// Try to create a Rocket request from the hyper request.
let request = match Request::from_hyp(hyp_req) {
Ok(req) => req,
Err(reason) => {
let mock_request = Request::mock(Method::Get, uri.as_str());
return self.handle_internal_error(reason, &mock_request, res);
}
};
info!("{}:", request);
let route = self.router.route(&request);
if let Some(ref route) = route {
// Retrieve and set the requests parameters. // Retrieve and set the requests parameters.
req.set_params(&route); request.set_params(route);
// Here's the magic: dispatch the request to the handler. // Here's the magic: dispatch the request to the handler.
let outcome = (route.handler)(&req).respond(res); let outcome = (route.handler)(&request).respond(res);
info_!("{} {}", White.paint("Outcome:"), outcome); info_!("{} {}", White.paint("Outcome:"), outcome);
// // TODO: keep trying lower ranked routes before dispatching a not // TODO: keep trying lower ranked routes before dispatching a not
// // found error. // found error.
// outcome.map_forward(|res| { outcome.map_forward(|res| {
// error_!("No further matching routes."); error_!("No further matching routes.");
// // TODO: Have some way to know why this was failed forward. Use that // TODO: Have some way to know why this was failed forward. Use that
// // instead of always using an unchained error. // instead of always using an unchained error.
// self.handle_not_found(req, res); self.handle_not_found(&request, res);
// }); });
} else { } else {
error_!("No matching routes."); error_!("No matching routes.");
return self.handle_not_found(&req, res); self.handle_not_found(&request, res);
} }
} }
// A closure which we call when we know there is no route. // Call on internal server error.
fn handle_internal_error<'r>(&self, reason: String, request: &'r Request<'r>,
response: FreshHyperResponse) {
error!("Internal server error.");
debug!("{}", reason);
let catcher = self.catchers.get(&500).unwrap();
catcher.handle(Error::Internal, request).respond(response);
}
// Call when no route was found.
fn handle_not_found<'r>(&self, request: &'r Request<'r>, fn handle_not_found<'r>(&self, request: &'r Request<'r>,
response: FreshHyperResponse) { response: FreshHyperResponse) {
error_!("Dispatch failed. Returning 404."); error_!("{} dispatch failed: 404.", request);
let catcher = self.catchers.get(&404).unwrap(); let catcher = self.catchers.get(&404).unwrap();
catcher.handle(Error::NoRoute, request).respond(response); catcher.handle(Error::NoRoute, request).respond(response);
} }
pub fn new(address: &'static str, port: isize) -> Rocket { pub fn new(address: &'static str, port: isize) -> Rocket {
// FIXME: Allow user to override level/disable logging.
logger::init(logger::Level::Normal);
Rocket { Rocket {
address: address, address: address,
port: port, port: port,
router: Router::new(), router: Router::new(),
catchers: catcher::defaults::get(), catchers: catcher::defaults::get(),
log_set: false,
} }
} }
@ -138,11 +124,24 @@ impl Rocket {
self self
} }
pub fn launch(self) { pub fn log(&mut self, level: LoggingLevel) {
if self.log_set {
warn!("Log level already set! Not overriding.");
} else {
logger::init(level);
self.log_set = true;
}
}
pub fn launch(mut self) {
if self.router.has_collisions() { if self.router.has_collisions() {
warn!("Route collisions detected!"); warn!("Route collisions detected!");
} }
if !self.log_set {
self.log(LoggingLevel::Normal)
}
let full_addr = format!("{}:{}", self.address, self.port); let full_addr = format!("{}:{}", self.address, self.port);
info!("🚀 {} {}...", White.paint("Rocket has launched from"), info!("🚀 {} {}...", White.paint("Rocket has launched from"),
White.bold().paint(&full_addr)); White.bold().paint(&full_addr));

View File

@ -181,7 +181,7 @@ fn content_type_to_expr(ecx: &ExtCtxt, content_type: &ContentType) -> P<Expr> {
pub fn route_decorator(known_method: Option<Spanned<Method>>, ecx: &mut ExtCtxt, pub fn route_decorator(known_method: Option<Spanned<Method>>, ecx: &mut ExtCtxt,
sp: Span, meta_item: &MetaItem, annotated: &Annotatable, sp: Span, meta_item: &MetaItem, annotated: &Annotatable,
push: &mut FnMut(Annotatable)) { push: &mut FnMut(Annotatable)) {
::rocket::logger::init(::rocket::logger::Level::Debug); ::rocket::logger::init(::rocket::LoggingLevel::Debug);
// Get the encompassing item and function declaration for the annotated func. // Get the encompassing item and function declaration for the annotated func.
let parser = MetaItemParser::new(ecx, meta_item, annotated, &sp); let parser = MetaItemParser::new(ecx, meta_item, annotated, &sp);