diff --git a/examples/forms/Cargo.toml b/examples/forms/Cargo.toml new file mode 100644 index 00000000..5dbe518c --- /dev/null +++ b/examples/forms/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "forms" +version = "0.0.1" +authors = ["Sergio Benitez "] + +[dependencies] +rocket = { path = "../../lib" } +rocket_macros = { path = "../../macros" } diff --git a/examples/forms/src/main.rs b/examples/forms/src/main.rs new file mode 100644 index 00000000..5e71405c --- /dev/null +++ b/examples/forms/src/main.rs @@ -0,0 +1,39 @@ +#![feature(plugin)] +#![plugin(rocket_macros)] + +extern crate rocket; + +use rocket::Rocket; +use std::fs::File; +use std::io::Error as IOError; +use rocket::response::Redirect; + +#[route(GET, path = "/")] +fn index() -> File { + File::open("static/index.html").unwrap() +} + +#[route(GET, path = "/")] +fn files(file: &str) -> Result { + File::open(format!("static/{}", file)) +} + +#[route(GET, path = "/user/")] +fn user_page(username: &str) -> String { + format!("This is {}'s page.", username) +} + +// TODO: Actually look at form parameters. +#[route(POST, path = "/login")] +fn login() -> Result { + if true { + Ok(Redirect::other("/user/some_name")) + } else { + Err("Sorry, the username and password are invalid.") + } +} + +fn main() { + let rocket = Rocket::new("localhost", 8000); + rocket.mount_and_launch("/", routes![index, files, user_page, login]); +} diff --git a/examples/forms/static/index.html b/examples/forms/static/index.html new file mode 100644 index 00000000..0bd425f1 --- /dev/null +++ b/examples/forms/static/index.html @@ -0,0 +1,7 @@ +

Login

+ +
+ Username: + Password: + +
diff --git a/examples/optional_redirect/Cargo.toml b/examples/optional_redirect/Cargo.toml new file mode 100644 index 00000000..df6a2421 --- /dev/null +++ b/examples/optional_redirect/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "optional_result" +version = "0.0.1" +authors = ["Sergio Benitez "] + +[dependencies] +rocket = { path = "../../lib" } +rocket_macros = { path = "../../macros" } diff --git a/examples/optional_redirect/src/main.rs b/examples/optional_redirect/src/main.rs new file mode 100644 index 00000000..1636a77a --- /dev/null +++ b/examples/optional_redirect/src/main.rs @@ -0,0 +1,23 @@ +#![feature(plugin)] +#![plugin(rocket_macros)] +extern crate rocket; + +use rocket::Rocket; +use rocket::response::Redirect; + +#[route(GET, path = "/users/")] +fn user(name: &str) -> Result<&'static str, Redirect> { + match name { + "Sergio" => Ok("Hello, Sergio!"), + _ => Err(Redirect::to("/users/login")) + } +} + +#[route(GET, path = "/users/login")] +fn login() -> &'static str { + "Hi! That user doesn't exist. Maybe you need to log in?" +} + +fn main() { + Rocket::new("localhost", 8000).mount_and_launch("/", routes![user, login]); +} diff --git a/examples/optional_result/Cargo.toml b/examples/optional_result/Cargo.toml new file mode 100644 index 00000000..df6a2421 --- /dev/null +++ b/examples/optional_result/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "optional_result" +version = "0.0.1" +authors = ["Sergio Benitez "] + +[dependencies] +rocket = { path = "../../lib" } +rocket_macros = { path = "../../macros" } diff --git a/examples/optional_result/src/main.rs b/examples/optional_result/src/main.rs new file mode 100644 index 00000000..2b9ae840 --- /dev/null +++ b/examples/optional_result/src/main.rs @@ -0,0 +1,18 @@ +#![feature(plugin)] +#![plugin(rocket_macros)] + +extern crate rocket; +use rocket::Rocket; + +#[route(GET, path = "/users/")] +fn user(name: &str) -> Option<&'static str> { + if name == "Sergio" { + Some("Hello, Sergio!") + } else { + None + } +} + +fn main() { + Rocket::new("localhost", 8000).mount_and_launch("/", routes![user]); +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index f149f719..d7445779 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,4 +1,5 @@ #![feature(str_char)] +#![feature(specialization)] extern crate term_painter; extern crate hyper; @@ -12,10 +13,10 @@ pub mod router; pub use method::Method; pub use error::Error; -pub use response::{Response, HypResponse, HypFresh, Responder}; pub use request::Request; pub use param::FromParam; pub use router::Router; +pub use response::{Response, HypResponse, Responder, HypFresh}; use std::fmt; use term_painter::ToStyle; @@ -27,7 +28,8 @@ use hyper::Server; pub type Handler<'a> = fn(Request) -> Response<'a>; -// TODO: Figure out if having Handler<'static> there is a good idea. +// 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, @@ -35,7 +37,8 @@ pub struct Route { } impl Route { - pub fn new(method: Method, path: &'static str, handler: Handler<'static>) -> Route { + pub fn new(method: Method, path: &'static str, handler: Handler<'static>) + -> Route { Route { method: method, path: path, @@ -50,7 +53,6 @@ impl<'a> fmt::Display for Route { } } -#[allow(dead_code)] pub struct Rocket { address: &'static str, port: isize, @@ -60,9 +62,10 @@ pub struct Rocket { impl HypHandler for Rocket { fn handle<'a, 'k>(&'a self, req: HypRequest<'a, 'k>, res: HypResponse<'a, HypFresh>) { + println!("{} {:?} {:?}", White.paint("Incoming:"), + Green.paint(&req.method), Blue.paint(&req.uri)); if let RequestUri::AbsolutePath(uri_string) = req.uri { if let Some(method) = Method::from_hyp(req.method) { - println!("Request: {:?}", uri_string); 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| { @@ -71,11 +74,15 @@ impl HypHandler for Rocket { (route.handler)(request) }); - return response.body.respond(res); + println!("{}", Green.paint("\t=> Dispatched request.")); + return response.respond(res); } + + println!("{}", Yellow.paint("\t=> Debug: Method::from_hyp failed!")); } - Response::not_found().body.respond(res); + println!("{}", Red.paint("\t=> Dispatch failed. Returning 404.")); + Response::not_found().respond(res); } } diff --git a/lib/src/method.rs b/lib/src/method.rs index d3d1f41a..8e272015 100644 --- a/lib/src/method.rs +++ b/lib/src/method.rs @@ -21,6 +21,14 @@ impl Method { pub fn from_hyp(method: HypMethod) -> Option { match method { HypMethod::Get => Some(Get), + HypMethod::Put => Some(Put), + HypMethod::Post => Some(Post), + HypMethod::Delete => Some(Delete), + HypMethod::Options => Some(Options), + HypMethod::Head => Some(Head), + HypMethod::Trace => Some(Trace), + HypMethod::Connect => Some(Connect), + HypMethod::Patch => Some(Patch), _ => None } } diff --git a/lib/src/response.rs b/lib/src/response.rs deleted file mode 100644 index dc9c31dd..00000000 --- a/lib/src/response.rs +++ /dev/null @@ -1,133 +0,0 @@ -pub use hyper::server::Response as HypResponse; -pub use hyper::net::Fresh as HypFresh; - -use hyper::status::StatusCode; -use hyper::header; -use std::io::{Read, Write}; -use std::fs::File; -use std::fmt; - -pub struct Response<'a> { - pub body: Box -} - -impl<'a> Response<'a> { - pub fn new(body: T) -> Response<'a> { - Response { - body: Box::new(body) - } - } - - pub fn empty() -> Response<'a> { - Response { - body: Box::new(Empty::new(StatusCode::Ok)) - } - } - - pub fn not_found() -> Response<'a> { - Response { - body: Box::new(Empty::new(StatusCode::NotFound)) - } - } - - pub fn server_error() -> Response<'a> { - Response { - body: Box::new(Empty::new(StatusCode::InternalServerError)) - } - } -} - -pub trait Responder { - fn respond<'a>(&mut self, mut res: HypResponse<'a, HypFresh>); -} - -pub struct Empty { - status: StatusCode -} - -impl Empty { - fn new(status: StatusCode) -> Empty { - Empty { - status: status - } - } -} - -impl Responder for Empty { - fn respond<'a>(&mut self, mut res: HypResponse<'a, HypFresh>) { - res.headers_mut().set(header::ContentLength(0)); - *(res.status_mut()) = self.status; - - let mut stream = res.start().unwrap(); - stream.write_all(b"").unwrap(); - } -} - -impl<'a> Responder for &'a str { - fn respond<'b>(&mut self, res: HypResponse<'b, HypFresh>) { - res.send(self.as_bytes()).unwrap(); - } -} - -impl Responder for String { - fn respond<'b>(&mut self, res: HypResponse<'b, HypFresh>) { - res.send(self.as_bytes()).unwrap(); - } -} - -impl Responder for File { - fn respond<'b>(&mut self, mut res: HypResponse<'b, HypFresh>) { - let size = self.metadata().unwrap().len(); - - res.headers_mut().set(header::ContentLength(size)); - *(res.status_mut()) = StatusCode::Ok; - - let mut v = Vec::new(); - self.read_to_end(&mut v).unwrap(); - - let mut stream = res.start().unwrap(); - stream.write_all(&v).unwrap(); - } -} - -// Waiting for RFC #1210: impl specialization. It's not quite stable yet. -// impl Responder for Result { -// fn respond<'b>(&mut self, res: HypResponse<'b, HypFresh>) { -// if self.is_err() { -// println!("Response error: {}", self.as_ref().err().unwrap()); -// return; -// } - -// self.as_mut().unwrap().respond(res); -// } -// } - -impl Responder for Result { - // prepend with `default` when using impl specialization - fn respond<'b>(&mut self, res: HypResponse<'b, HypFresh>) { - if self.is_err() { - println!("Error: {:?}", self.as_ref().err().unwrap()); - return; - } - - self.as_mut().unwrap().respond(res); - } -} - -// TODO: Allow streamed responses. -// const CHUNK_SIZE: u32 = 4096; -// pub struct Stream(T); -// impl Responder for Stream { -// fn respond<'a>(&self, mut r: HypResponse<'a, HypFresh>) { -// r.headers_mut().set(header::TransferEncoding(vec![Encoding::Chunked])); -// *(r.status_mut()) = StatusCode::Ok; -// let mut stream = r.start(); - -// r.write() -// Response { -// status: StatusCode::Ok, -// headers: headers, -// body: Body::Stream(r) -// } -// } -// } diff --git a/lib/src/response/empty.rs b/lib/src/response/empty.rs new file mode 100644 index 00000000..2f70888f --- /dev/null +++ b/lib/src/response/empty.rs @@ -0,0 +1,20 @@ +use response::*; +use std::io::Write; + +pub struct Empty(StatusCode); + +impl Empty { + pub fn new(status: StatusCode) -> Empty { + Empty(status) + } +} + +impl Responder for Empty { + fn respond<'a>(&mut self, mut res: HypResponse<'a, HypFresh>) { + res.headers_mut().set(header::ContentLength(0)); + *(res.status_mut()) = self.0; + + let mut stream = res.start().unwrap(); + stream.write_all(b"").unwrap(); + } +} diff --git a/lib/src/response/mod.rs b/lib/src/response/mod.rs new file mode 100644 index 00000000..650f582e --- /dev/null +++ b/lib/src/response/mod.rs @@ -0,0 +1,48 @@ +mod empty; +mod responder; +mod redirect; + +pub use self::responder::Responder; +pub use self::empty::Empty; +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}; + +pub struct Response<'a>(Box); + +impl<'a> Response<'a> { + pub fn new(body: T) -> Response<'a> { + Response(Box::new(body)) + } + + pub fn empty() -> Response<'a> { + Response(Box::new(Empty::new(StatusCode::Ok))) + } + + pub fn not_found() -> Response<'a> { + Response(Box::new(Empty::new(StatusCode::NotFound))) + } + + pub fn server_error() -> Response<'a> { + Response(Box::new(Empty::new(StatusCode::InternalServerError))) + } +} + +impl<'a> Deref for Response<'a> { + type Target = Box; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a> DerefMut for Response<'a> { + fn deref_mut(&mut self) -> &mut Box { + &mut self.0 + } +} diff --git a/lib/src/response/redirect.rs b/lib/src/response/redirect.rs new file mode 100644 index 00000000..11ecf3f1 --- /dev/null +++ b/lib/src/response/redirect.rs @@ -0,0 +1,32 @@ +use response::*; + +#[derive(Debug)] +pub struct Redirect(StatusCode, String); + +impl Redirect { + pub fn to(uri: &str) -> Redirect { + Redirect(StatusCode::TemporaryRedirect, String::from(uri)) + } + + pub fn created(uri: &str) -> Redirect { + Redirect(StatusCode::Created, String::from(uri)) + } + + pub fn other(uri: &str) -> Redirect { + Redirect(StatusCode::SeeOther, String::from(uri)) + } + + pub fn permanent(uri: &str) -> Redirect { + Redirect(StatusCode::PermanentRedirect, String::from(uri)) + } +} + +impl<'a> Responder for Redirect { + fn respond<'b>(&mut self, mut res: HypResponse<'b, HypFresh>) { + res.headers_mut().set(header::ContentLength(0)); + res.headers_mut().set(header::Location(self.1.clone())); + *(res.status_mut()) = self.0; + res.send(b"").unwrap(); + } +} + diff --git a/lib/src/response/responder.rs b/lib/src/response/responder.rs new file mode 100644 index 00000000..994fab4f --- /dev/null +++ b/lib/src/response/responder.rs @@ -0,0 +1,70 @@ +use response::*; +use std::io::{Read, Write}; +use std::fs::File; +use std::fmt; + +pub trait Responder { + fn respond<'a>(&mut self, mut res: HypResponse<'a, HypFresh>); +} + +impl<'a> Responder for &'a str { + fn respond<'b>(&mut self, res: HypResponse<'b, HypFresh>) { + res.send(self.as_bytes()).unwrap(); + } +} + +impl Responder for String { + fn respond<'b>(&mut self, res: HypResponse<'b, HypFresh>) { + res.send(self.as_bytes()).unwrap(); + } +} + +// FIXME: Should we set a content-type here? Safari needs text/html to render. +impl Responder for File { + fn respond<'b>(&mut self, mut res: HypResponse<'b, HypFresh>) { + let size = self.metadata().unwrap().len(); + + res.headers_mut().set(header::ContentLength(size)); + *(res.status_mut()) = StatusCode::Ok; + + let mut v = Vec::new(); + self.read_to_end(&mut v).unwrap(); + + let mut stream = res.start().unwrap(); + stream.write_all(&v).unwrap(); + } +} + +impl Responder for Option { + fn respond<'b>(&mut self, res: HypResponse<'b, HypFresh>) { + if self.is_none() { + println!("Option is none."); + // TODO: Should this be a 404 or 500? + return Empty::new(StatusCode::NotFound).respond(res) + } + + self.as_mut().unwrap().respond(res); + } +} + +impl Responder for Result { + // prepend with `default` when using impl specialization + default fn respond<'b>(&mut self, res: HypResponse<'b, HypFresh>) { + if self.is_err() { + println!("Error: {:?}", self.as_ref().err().unwrap()); + // TODO: Should this be a 404 or 500? + return Empty::new(StatusCode::NotFound).respond(res) + } + + self.as_mut().unwrap().respond(res); + } +} + +impl Responder for Result { + fn respond<'b>(&mut self, res: HypResponse<'b, HypFresh>) { + match self { + &mut Ok(ref mut responder) => responder.respond(res), + &mut Err(ref mut responder) => responder.respond(res) + } + } +} diff --git a/lib/src/response/stream.rs b/lib/src/response/stream.rs new file mode 100644 index 00000000..3e58d7b1 --- /dev/null +++ b/lib/src/response/stream.rs @@ -0,0 +1,17 @@ +// TODO: Allow streamed responses. +// const CHUNK_SIZE: u32 = 4096; +// pub struct Stream(T); +// impl Responder for Stream { +// fn respond<'a>(&self, mut r: HypResponse<'a, HypFresh>) { +// r.headers_mut().set(header::TransferEncoding(vec![Encoding::Chunked])); +// *(r.status_mut()) = StatusCode::Ok; +// let mut stream = r.start(); + +// r.write() +// Response { +// status: StatusCode::Ok, +// headers: headers, +// body: Body::Stream(r) +// } +// } +// } diff --git a/lib/src/router/mod.rs b/lib/src/router/mod.rs index a275dbd8..b636a7ab 100644 --- a/lib/src/router/mod.rs +++ b/lib/src/router/mod.rs @@ -42,7 +42,7 @@ impl Router { let num_components = path.components().count(); if let Some(routes) = self.routes.get(&(method, num_components)) { for route in routes.iter().filter(|r| r.collides_with(uri)) { - println!("Matched {} to: {}", uri, route); + println!("\t=> Matched {} to: {}", uri, route); if let None = matched_route { matched_route = Some(route); } diff --git a/lib/src/router/route.rs b/lib/src/router/route.rs index aa8b62a9..77f8b5d5 100644 --- a/lib/src/router/route.rs +++ b/lib/src/router/route.rs @@ -7,6 +7,7 @@ use super::Collider; // :D use std::path::Component; use Handler; +// TODO: Add ranking to routes. Give static routes higher rank by default. // FIXME: Take in the handler! Or maybe keep that in `Router`? pub struct Route { method: Method, @@ -50,7 +51,7 @@ impl Route { // FIXME: This is dirty (the comp_to_str and the RootDir thing). Might need // to have my own wrapper arround path strings. // FIXME: Decide whether a component has to be fully variable or not. That - // is, whether you can have: /ab/ + // is, whether you can have: /ab/ or even /:/ // TODO: Don't return a Vec...take in an &mut [&'a str] (no alloc!) pub fn get_params<'a>(&self, uri: &'a str) -> Vec<&'a str> { let mut result = Vec::with_capacity(self.component_count()); diff --git a/macros/src/route_decorator.rs b/macros/src/route_decorator.rs index 8a4d2ce9..81a2fd70 100644 --- a/macros/src/route_decorator.rs +++ b/macros/src/route_decorator.rs @@ -58,7 +58,7 @@ fn get_route_params(ecx: &mut ExtCtxt, meta_item: &MetaItem) -> Params { let params: &Vec> = match meta_item.node { MetaItemKind::List(_, ref params) => params, _ => ecx.span_fatal(meta_item.span, - "incorrect use of macro. correct form is: #[demo(...)]"), + "Incorrect use of macro. correct form is: #[route(...)]"), }; // Ensure we can unwrap the k = v params. @@ -117,7 +117,6 @@ fn method_variant_to_expr(ecx: &ExtCtxt, method: Method) -> P { pub fn get_fn_params(ecx: &ExtCtxt, sp: Span, path: &str, fn_decl: &FnDecl) -> Vec { - debug!("FUNCTION: {:?}", fn_decl); let mut seen = HashSet::new(); @@ -125,6 +124,7 @@ pub fn get_fn_params(ecx: &ExtCtxt, sp: Span, path: &str, let mut matching = false; // Collect all of the params in the path and insert into HashSet. + // TODO: Move this logic into main library. let mut start = 0; for (i, c) in path.char_indices() { match c { @@ -147,6 +147,7 @@ pub fn get_fn_params(ecx: &ExtCtxt, sp: Span, path: &str, } } + // TODO: This should stay here, though. // Ensure every param in the function declaration is in `path`. Also add // each param name in the declaration to the result vector. let mut result = vec![];