Major refactoring.

Here's the idea: under the `Rocket` namespace should live things critical to
writing simple Rocket apps: Request, Response, Error, etc. Nothing should be
nested more than one level deep. Only items required for more complex things
(implementing uncommon traits, etc.) should be nested one level deep.

This commit is the first attempt at realizing this.
This commit is contained in:
Sergio Benitez 2016-04-01 16:54:53 -07:00
parent 6ba67ae79a
commit 50bc0d6999
17 changed files with 250 additions and 177 deletions

View File

@ -7,7 +7,7 @@ mod files;
use rocket::Rocket; use rocket::Rocket;
use rocket::response::Redirect; use rocket::response::Redirect;
use rocket::error::Error; use rocket::Error;
#[route(GET, path = "/user/<username>")] #[route(GET, path = "/user/<username>")]
fn user_page(username: &str) -> String { fn user_page(username: &str) -> String {

View File

@ -0,0 +1,8 @@
[package]
name = "redirect"
version = "0.0.1"
authors = ["Sergio Benitez <sb@sergio.bz>"]
[dependencies]
rocket = { path = "../../lib" }
rocket_macros = { path = "../../macros" }

View File

@ -0,0 +1,20 @@
#![feature(plugin)]
#![plugin(rocket_macros)]
extern crate rocket;
use rocket::Rocket;
use rocket::response::Redirect;
#[route(GET, path = "/")]
fn root() -> Redirect {
Redirect::to("/login")
}
#[route(GET, path = "/login")]
fn login() -> &'static str {
"Hi! Please log in before continuing."
}
fn main() {
Rocket::new("localhost", 8000).mount_and_launch("/", routes![root, login]);
}

View File

@ -4,5 +4,9 @@ version = "0.0.1"
authors = ["Sergio Benitez <sb@sergio.bz>"] authors = ["Sergio Benitez <sb@sergio.bz>"]
[dependencies] [dependencies]
hyper = "*"
term-painter = "*" term-painter = "*"
hyper = "*"
# [dependencies.hyper]
# git = "https://github.com/hyperium/hyper.git"
# branch = "mio"

View File

@ -4,125 +4,21 @@
extern crate term_painter; extern crate term_painter;
extern crate hyper; extern crate hyper;
pub mod method; mod method;
pub mod error; mod error;
pub mod response; mod param;
pub mod request; mod router;
pub mod param; mod rocket;
pub mod router; mod route;
pub mod request;
pub mod response;
pub use method::Method;
pub use error::Error;
pub use request::Request; pub use request::Request;
pub use method::Method;
pub use response::{Response, Responder};
pub use error::Error;
pub use param::FromParam; pub use param::FromParam;
pub use router::Router; pub use router::Router;
pub use response::{Response, HypResponse, Responder, HypFresh}; pub use route::{Route, Handler};
pub use rocket::Rocket;
use std::fmt;
use std::io::Read;
use term_painter::ToStyle;
use term_painter::Color::*;
use hyper::uri::RequestUri;
use hyper::server::Handler as HypHandler;
use hyper::server::Request as HypRequest;
use hyper::Server;
pub type Handler<'a> = fn(Request) -> Response<'a>;
// TODO: Figure out if using 'static for Handler is a good idea.
// TODO: Merge this `Route` and route::Route, somewhow.
pub struct Route {
pub method: Method,
pub path: &'static str,
pub handler: Handler<'static>
}
impl Route {
pub fn new(method: Method, path: &'static str, handler: Handler<'static>)
-> Route {
Route {
method: method,
path: path,
handler: handler
}
}
}
impl<'a> fmt::Display for Route {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} {:?}", Green.paint(&self.method), Blue.paint(&self.path))
}
}
pub struct Rocket {
address: &'static str,
port: isize,
router: Router
}
impl HypHandler for Rocket {
fn handle<'a, 'k>(&'a self, mut req: HypRequest<'a, 'k>,
res: HypResponse<'a, HypFresh>) {
println!("{} {:?} {:?}", White.paint("Incoming:"),
Green.paint(&req.method), Blue.paint(&req.uri));
let mut buf = vec![];
req.read_to_end(&mut buf); // FIXME: Simple DOS attack here.
if let RequestUri::AbsolutePath(uri_string) = req.uri {
if let Some(method) = Method::from_hyp(req.method) {
let uri_str = uri_string.as_str();
let route = self.router.route(method, uri_str);
let mut response = route.map_or(Response::not_found(), |route| {
let params = route.get_params(uri_str);
let request = Request::new(params, uri_str);
(route.handler)(request)
});
println!("{}", Green.paint("\t=> Dispatched request."));
return response.respond(res);
}
println!("{}", Yellow.paint("\t=> Debug: Method::from_hyp failed!"));
}
println!("{}", Red.paint("\t=> Dispatch failed. Returning 404."));
Response::not_found().respond(res);
}
}
impl Rocket {
pub fn new(address: &'static str, port: isize) -> Rocket {
Rocket {
address: address,
port: port,
router: Router::new()
}
}
pub fn mount(&mut self, base: &'static str, routes: &[&Route]) -> &mut Self {
println!("🛰 {} '{}':", Magenta.paint("Mounting"), Blue.paint(base));
for route in routes {
println!("\t* {}", route);
self.router.add_route(route.method, base, route.path, route.handler);
}
self
}
pub fn mount_and_launch(mut self, base: &'static str, routes: &[&Route]) {
self.mount(base, routes);
self.launch();
}
pub fn launch(self) {
if self.router.has_collisions() {
println!("{}", Yellow.paint("Warning: route collisions detected!"));
}
let full_addr = format!("{}:{}", self.address, self.port);
println!("🚀 {} {}...", White.paint("Rocket has launched from"),
White.bold().paint(&full_addr));
let _ = Server::http(full_addr.as_str()).unwrap().handle(self);
}
}

View File

@ -1,8 +1,9 @@
use super::*;
use self::Method::*; use self::Method::*;
use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use std::fmt::{self, Display}; use hyper::method::Method as HyperMethod;
use error::Error;
use hyper::method::Method as HypMethod;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum Method { pub enum Method {
@ -18,17 +19,17 @@ pub enum Method {
} }
impl Method { impl Method {
pub fn from_hyp(method: HypMethod) -> Option<Method> { pub fn from_hyp(method: HyperMethod) -> Option<Method> {
match method { match method {
HypMethod::Get => Some(Get), HyperMethod::Get => Some(Get),
HypMethod::Put => Some(Put), HyperMethod::Put => Some(Put),
HypMethod::Post => Some(Post), HyperMethod::Post => Some(Post),
HypMethod::Delete => Some(Delete), HyperMethod::Delete => Some(Delete),
HypMethod::Options => Some(Options), HyperMethod::Options => Some(Options),
HypMethod::Head => Some(Head), HyperMethod::Head => Some(Head),
HypMethod::Trace => Some(Trace), HyperMethod::Trace => Some(Trace),
HypMethod::Connect => Some(Connect), HyperMethod::Connect => Some(Connect),
HypMethod::Patch => Some(Patch), HyperMethod::Patch => Some(Patch),
_ => None _ => None
} }
} }
@ -53,7 +54,7 @@ impl FromStr for Method {
} }
} }
impl Display for Method { impl fmt::Display for Method {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.write_str(match *self { fmt.write_str(match *self {
Get => "GET", Get => "GET",

View File

@ -1,6 +1,8 @@
use error::Error; use error::Error;
use param::FromParam; use param::FromParam;
pub use hyper::server::Request as HyperRequest;
pub struct Request<'a> { pub struct Request<'a> {
params: Vec<&'a str>, params: Vec<&'a str>,
uri: &'a str, uri: &'a str,

View File

@ -10,7 +10,7 @@ impl Empty {
} }
impl Responder for Empty { impl Responder for Empty {
fn respond<'a>(&mut self, mut res: HypResponse<'a, HypFresh>) { fn respond<'a>(&mut self, mut res: HyperResponse<'a, HyperFresh>) {
res.headers_mut().set(header::ContentLength(0)); res.headers_mut().set(header::ContentLength(0));
*(res.status_mut()) = self.0; *(res.status_mut()) = self.0;

View File

@ -2,15 +2,15 @@ mod empty;
mod responder; mod responder;
mod redirect; mod redirect;
pub use hyper::server::Response as HyperResponse;
pub use hyper::net::Fresh as HyperFresh;
pub use hyper::status::StatusCode;
pub use hyper::header;
pub use self::responder::Responder; pub use self::responder::Responder;
pub use self::empty::Empty; pub use self::empty::Empty;
pub use self::redirect::Redirect; pub use self::redirect::Redirect;
pub use hyper::server::Response as HypResponse;
pub use hyper::net::Fresh as HypFresh;
pub use hyper::status::StatusCode;
pub use hyper::header;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
pub struct Response<'a>(Box<Responder + 'a>); pub struct Response<'a>(Box<Responder + 'a>);

View File

@ -22,7 +22,7 @@ impl Redirect {
} }
impl<'a> Responder for Redirect { impl<'a> Responder for Redirect {
fn respond<'b>(&mut self, mut res: HypResponse<'b, HypFresh>) { fn respond<'b>(&mut self, mut res: HyperResponse<'b, HyperFresh>) {
res.headers_mut().set(header::ContentLength(0)); res.headers_mut().set(header::ContentLength(0));
res.headers_mut().set(header::Location(self.1.clone())); res.headers_mut().set(header::Location(self.1.clone()));
*(res.status_mut()) = self.0; *(res.status_mut()) = self.0;

View File

@ -4,24 +4,24 @@ use std::fs::File;
use std::fmt; use std::fmt;
pub trait Responder { pub trait Responder {
fn respond<'a>(&mut self, mut res: HypResponse<'a, HypFresh>); fn respond<'a>(&mut self, mut res: HyperResponse<'a, HyperFresh>);
} }
impl<'a> Responder for &'a str { impl<'a> Responder for &'a str {
fn respond<'b>(&mut self, res: HypResponse<'b, HypFresh>) { fn respond<'b>(&mut self, res: HyperResponse<'b, HyperFresh>) {
res.send(self.as_bytes()).unwrap(); res.send(self.as_bytes()).unwrap();
} }
} }
impl Responder for String { impl Responder for String {
fn respond<'b>(&mut self, res: HypResponse<'b, HypFresh>) { fn respond<'b>(&mut self, res: HyperResponse<'b, HyperFresh>) {
res.send(self.as_bytes()).unwrap(); res.send(self.as_bytes()).unwrap();
} }
} }
// FIXME: Should we set a content-type here? Safari needs text/html to render. // FIXME: Should we set a content-type here? Safari needs text/html to render.
impl Responder for File { impl Responder for File {
fn respond<'b>(&mut self, mut res: HypResponse<'b, HypFresh>) { fn respond<'b>(&mut self, mut res: HyperResponse<'b, HyperFresh>) {
let size = self.metadata().unwrap().len(); let size = self.metadata().unwrap().len();
res.headers_mut().set(header::ContentLength(size)); res.headers_mut().set(header::ContentLength(size));
@ -36,7 +36,7 @@ impl Responder for File {
} }
impl<T: Responder> Responder for Option<T> { impl<T: Responder> Responder for Option<T> {
fn respond<'b>(&mut self, res: HypResponse<'b, HypFresh>) { fn respond<'b>(&mut self, res: HyperResponse<'b, HyperFresh>) {
if self.is_none() { if self.is_none() {
println!("Option is none."); println!("Option is none.");
// TODO: Should this be a 404 or 500? // TODO: Should this be a 404 or 500?
@ -49,7 +49,7 @@ impl<T: Responder> Responder for Option<T> {
impl<T: Responder, E: fmt::Debug> Responder for Result<T, E> { impl<T: Responder, E: fmt::Debug> Responder for Result<T, E> {
// prepend with `default` when using impl specialization // prepend with `default` when using impl specialization
default fn respond<'b>(&mut self, res: HypResponse<'b, HypFresh>) { default fn respond<'b>(&mut self, res: HyperResponse<'b, HyperFresh>) {
if self.is_err() { if self.is_err() {
println!("Error: {:?}", self.as_ref().err().unwrap()); println!("Error: {:?}", self.as_ref().err().unwrap());
// TODO: Should this be a 404 or 500? // TODO: Should this be a 404 or 500?
@ -61,7 +61,7 @@ impl<T: Responder, E: fmt::Debug> Responder for Result<T, E> {
} }
impl<T: Responder, E: Responder + fmt::Debug> Responder for Result<T, E> { impl<T: Responder, E: Responder + fmt::Debug> Responder for Result<T, E> {
fn respond<'b>(&mut self, res: HypResponse<'b, HypFresh>) { fn respond<'b>(&mut self, res: HyperResponse<'b, HyperFresh>) {
match self { match self {
&mut Ok(ref mut responder) => responder.respond(res), &mut Ok(ref mut responder) => responder.respond(res),
&mut Err(ref mut responder) => responder.respond(res) &mut Err(ref mut responder) => responder.respond(res)

84
lib/src/rocket.rs Normal file
View File

@ -0,0 +1,84 @@
use super::*;
use response::{HyperResponse, HyperFresh};
use request::HyperRequest;
use std::io::Read;
use term_painter::Color::*;
use term_painter::ToStyle;
use hyper::uri::RequestUri as HyperRequestUri;
use hyper::server::Server as HyperServer;
use hyper::server::Handler as HyperHandler;
pub struct Rocket {
address: &'static str,
port: isize,
router: Router
}
impl HyperHandler for Rocket {
fn handle<'a, 'k>(&'a self, mut req: HyperRequest<'a, 'k>,
res: HyperResponse<'a, HyperFresh>) {
println!("{} {:?} {:?}", White.paint("Incoming:"),
Green.paint(&req.method), Blue.paint(&req.uri));
let mut buf = vec![];
req.read_to_end(&mut buf); // FIXME: Simple DOS attack here.
if let HyperRequestUri::AbsolutePath(uri_string) = req.uri {
if let Some(method) = Method::from_hyp(req.method) {
let uri_str = uri_string.as_str();
let route = self.router.route(method, uri_str);
let mut response = route.map_or(Response::not_found(), |route| {
let params = route.get_params(uri_str);
let request = Request::new(params, uri_str);
(route.handler)(request)
});
println!("{}", Green.paint("\t=> Dispatched request."));
return response.respond(res);
}
println!("{}", Yellow.paint("\t=> Debug: Method::from_hyp failed!"));
}
println!("{}", Red.paint("\t=> Dispatch failed. Returning 404."));
Response::not_found().respond(res);
}
}
impl Rocket {
pub fn new(address: &'static str, port: isize) -> Rocket {
Rocket {
address: address,
port: port,
router: Router::new()
}
}
pub fn mount(&mut self, base: &'static str, routes: &[&Route]) -> &mut Self {
println!("🛰 {} '{}':", Magenta.paint("Mounting"), Blue.paint(base));
for route in routes {
println!("\t* {}", route);
self.router.add_route(route.method, base, route.path, route.handler);
}
self
}
pub fn mount_and_launch(mut self, base: &'static str, routes: &[&Route]) {
self.mount(base, routes);
self.launch();
}
pub fn launch(self) {
if self.router.has_collisions() {
println!("{}", Yellow.paint("Warning: route collisions detected!"));
}
let full_addr = format!("{}:{}", self.address, self.port);
println!("🚀 {} {}...", White.paint("Rocket has launched from"),
White.bold().paint(&full_addr));
let _ = HyperServer::http(full_addr.as_str()).unwrap().handle(self);
}
}

35
lib/src/route.rs Normal file
View File

@ -0,0 +1,35 @@
use request::Request;
use response::Response;
use method::Method;
use std::fmt;
use term_painter::Color::*;
use term_painter::ToStyle;
pub type Handler<'a> = fn(Request) -> Response<'a>;
// TODO: Figure out if using 'static for Handler is a good idea.
// TODO: Merge this `Route` and route::Route, somewhow.
pub struct Route {
pub method: Method,
pub path: &'static str,
pub handler: Handler<'static>
}
impl Route {
pub fn new(method: Method, path: &'static str, handler: Handler<'static>)
-> Route {
Route {
method: method,
path: path,
handler: handler
}
}
}
impl<'a> fmt::Display for Route {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} {:?}", Green.paint(&self.method), Blue.paint(&self.path))
}
}

View File

@ -1,4 +1,5 @@
use std::path::Component; use std::path::Component;
use std::path::Path;
pub trait Collider<T: ?Sized = Self> { pub trait Collider<T: ?Sized = Self> {
fn collides_with(&self, other: &T) -> bool; fn collides_with(&self, other: &T) -> bool;
@ -44,6 +45,31 @@ macro_rules! comp_to_str {
) )
} }
impl Collider for Path {
// TODO: It's expensive to compute the number of components: O(n) per path
// where n == number of chars.
//
// Idea: Create a `CachedPath` type that caches the number of components
// similar to the way `Route` does it.
fn collides_with(&self, b: &Path) -> bool {
if self.components().count() != b.components().count() {
return false;
}
let mut a_components = self.components();
let mut b_components = b.components();
while let Some(ref c1) = a_components.next() {
if let Some(ref c2) = b_components.next() {
if !c1.collides_with(c2) {
return false
}
}
}
true
}
}
impl<'a> Collider for Component<'a> { impl<'a> Collider for Component<'a> {
fn collides_with(&self, other: &Component<'a>) -> bool { fn collides_with(&self, other: &Component<'a>) -> bool {
let (a, b) = (comp_to_str!(self), comp_to_str!(other)); let (a, b) = (comp_to_str!(self), comp_to_str!(other));
@ -51,6 +77,12 @@ impl<'a> Collider for Component<'a> {
} }
} }
impl<'a> Collider<str> for &'a str {
fn collides_with(&self, other: &str) -> bool {
Path::new(self).collides_with(Path::new(other))
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use router::Collider; use router::Collider;
@ -84,6 +116,10 @@ mod tests {
route_a.collides_with(b) route_a.collides_with(b)
} }
fn s_s_collide(a: &'static str, b: &'static str) -> bool {
a.collides_with(b)
}
#[test] #[test]
fn simple_collisions() { fn simple_collisions() {
assert!(collide("a", "a")); assert!(collide("a", "a"));
@ -177,7 +213,19 @@ mod tests {
assert!(!r_s_collide("/a<a>/<b>", "/b/<b>")); assert!(!r_s_collide("/a<a>/<b>", "/b/<b>"));
} }
#[test]
fn test_str_collisions() { fn test_str_collisions() {
// FIXME: Write these. assert!(!s_s_collide("/a", "/b"));
assert!(!s_s_collide("/a/b", "/a"));
assert!(!s_s_collide("/a/b", "/a/c"));
assert!(!s_s_collide("/a/hello", "/a/c"));
assert!(!s_s_collide("/hello", "/a/c"));
assert!(!s_s_collide("/hello/there", "/hello/there/guy"));
assert!(!s_s_collide("/b<a>/there", "/hi/there"));
assert!(!s_s_collide("/<a>/<b>c", "/hi/person"));
assert!(!s_s_collide("/<a>/<b>cd", "/hi/<a>e"));
assert!(!s_s_collide("/a<a>/<b>", "/b<b>/<a>"));
assert!(!s_s_collide("/a/<b>", "/b/<b>"));
assert!(!s_s_collide("/a<a>/<b>", "/b/<b>"));
} }
} }

View File

@ -9,7 +9,7 @@ use self::route::Route;
use std::collections::hash_map::HashMap; use std::collections::hash_map::HashMap;
use std::path::Path; use std::path::Path;
use method::Method; use method::Method;
use Handler; use route::Handler;
type Selector = (Method, usize); type Selector = (Method, usize);

View File

@ -5,7 +5,7 @@ use std::fmt;
use method::Method; use method::Method;
use super::Collider; // :D use super::Collider; // :D
use std::path::Component; use std::path::Component;
use Handler; use route::Handler;
// TODO: Add ranking to routes. Give static routes higher rank by default. // TODO: Add ranking to routes. Give static routes higher rank by default.
// FIXME: Take in the handler! Or maybe keep that in `Router`? // FIXME: Take in the handler! Or maybe keep that in `Router`?
@ -89,31 +89,6 @@ impl fmt::Display for Route {
} }
} }
impl Collider for Path {
// TODO: It's expensive to compute the number of components: O(n) per path
// where n == number of chars.
//
// Idea: Create a `CachedPath` type that caches the number of components
// similar to the way `Route` does it.
fn collides_with(&self, b: &Path) -> bool {
if self.components().count() != b.components().count() {
return false;
}
let mut a_components = self.components();
let mut b_components = b.components();
while let Some(ref c1) = a_components.next() {
if let Some(ref c2) = b_components.next() {
if !c1.collides_with(c2) {
return false
}
}
}
true
}
}
impl Collider for Route { impl Collider for Route {
fn collides_with(&self, b: &Route) -> bool { fn collides_with(&self, b: &Route) -> bool {
if self.n_components != b.n_components || self.method != b.method { if self.n_components != b.n_components || self.method != b.method {

View File

@ -6,7 +6,7 @@ use std::collections::HashSet;
use syntax::ext::quote::rt::ToTokens; use syntax::ext::quote::rt::ToTokens;
use syntax::codemap::{Span, BytePos, DUMMY_SP, Spanned}; use syntax::codemap::{Span, BytePos, DUMMY_SP, Spanned};
use syntax::ast::{self, Ident, TokenTree, PatKind, Stmt}; use syntax::ast::{Ident, TokenTree, PatKind, Stmt};
use syntax::ast::{Item, Expr, ItemKind, MetaItem, MetaItemKind, FnDecl, Ty}; use syntax::ast::{Item, Expr, ItemKind, MetaItem, MetaItemKind, FnDecl, Ty};
use syntax::ext::base::{Annotatable, ExtCtxt}; use syntax::ext::base::{Annotatable, ExtCtxt};
use syntax::ptr::P; use syntax::ptr::P;