Progress on errors. Started Todo example.

The error function now takes in a "RoutingError" structure. The idea is that the
structure includes all of the information necessary for a user to processor the
error as they wish. This interface is very incomplete and may change. At a
minimum, the error structure should include:

  1) The request that failed.
  2) Why the request failed.
  3) The chain of attempted route matches, if any.
  4) Something else?
This commit is contained in:
Sergio Benitez 2016-07-15 21:09:08 -07:00
parent 5870c2fe92
commit 26b7b814f4
13 changed files with 230 additions and 24 deletions

View File

@ -2,7 +2,7 @@
#![plugin(rocket_macros)]
extern crate rocket;
use rocket::Rocket;
use rocket::{Rocket, RoutingError};
#[route(GET, path = "/hello/<name>/<age>")]
fn hello(name: &str, age: i8) -> String {
@ -10,8 +10,10 @@ fn hello(name: &str, age: i8) -> String {
}
#[error(code = "404")]
fn not_found() -> &'static str {
"<h1>Sorry pal.</h1><p>Go to '/hello/&lt;name&gt;/&ltage&gt;' instead.</p>"
fn not_found(error: RoutingError) -> String {
format!("<p>Sorry, but '{}' is not a valid path!</p>
<p>Try visiting /hello/&lt;name&gt;/&lt;age&gt; instead.</p>",
error.request.uri)
}
fn main() {

View File

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

View File

@ -0,0 +1,13 @@
use rocket;
use std::fs::File;
use std::io::Error as IOError;
#[route(GET, path = "/")]
pub fn index() -> File {
File::open("static/index.html").unwrap()
}
#[route(GET, path = "/<file>")]
pub fn files(file: &str) -> Result<File, IOError> {
File::open(format!("static/{}", file))
}

View File

@ -0,0 +1,81 @@
#![feature(plugin, custom_derive)]
#![plugin(rocket_macros)]
extern crate rocket;
mod files;
use rocket::Rocket;
use rocket::response::Redirect;
use rocket::form::FromFormValue;
#[derive(Debug)]
struct StrongPassword<'r>(&'r str);
#[derive(Debug)]
struct AdultAge(isize);
#[derive(FromForm)]
struct UserLogin<'r> {
username: &'r str,
password: Result<StrongPassword<'r>, &'r str>,
age: Result<AdultAge, &'r str>,
}
impl<'v> FromFormValue<'v> for StrongPassword<'v> {
type Error = &'static str;
fn parse(v: &'v str) -> Result<Self, Self::Error> {
if v.len() < 8 {
Err("Too short!")
} else {
Ok(StrongPassword(v))
}
}
}
impl<'v> FromFormValue<'v> for AdultAge {
type Error = &'static str;
fn parse(v: &'v str) -> Result<Self, Self::Error> {
let age = match isize::parse(v) {
Ok(v) => v,
Err(_) => return Err("Age value is not a number.")
};
match age > 20 {
true => Ok(AdultAge(age)),
false => Err("Must be at least 21.")
}
}
}
#[route(POST, path = "/login", form = "<user>")]
fn login(user: UserLogin) -> Result<Redirect, String> {
if user.age.is_err() {
return Err(String::from(user.age.unwrap_err()));
}
if user.password.is_err() {
return Err(String::from(user.password.unwrap_err()));
}
match user.username {
"Sergio" => match user.password.unwrap().0 {
"password" => Ok(Redirect::other("/user/Sergio")),
_ => Err("Wrong password!".to_string())
},
_ => Err(format!("Unrecognized user, '{}'.", user.username))
}
}
#[route(GET, path = "/user/<username>")]
fn user_page(username: &str) -> String {
format!("This is {}'s page.", username)
}
fn main() {
let mut rocket = Rocket::new("localhost", 8000);
rocket.mount("/", routes![files::index, files::files, user_page, login]);
rocket.launch();
}

View File

@ -0,0 +1,8 @@
<h1>Login</h1>
<form action="/login" method="post" accept-charset="utf-8">
Username:<input type="text" name="username">
Password:<input type="password" name="password">
Age:<input type="number" name="age">
<input type="submit" value="Login">
</form>

8
examples/todo/Cargo.toml Normal file
View File

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

39
examples/todo/src/main.rs Normal file
View File

@ -0,0 +1,39 @@
#![feature(plugin, custom_derive)]
#![plugin(rocket_macros)]
extern crate rocket;
use rocket::Rocket;
use rocket::response::Redirect;
#[derive(FromForm)]
struct Todo<'r> {
description: &'r str,
}
#[route(POST, path = "/todo", form = "<todo>")]
fn new_todo(todo: Todo) -> Result<Redirect, &'static str> {
// if todos.add(todo).is_ok() {
// Ok(Redirect::to("/"))
// } else {
// Err("Could not add todo to list.")
// }
Ok(Redirect::to("/"))
}
#[route(GET, path = "/todos")]
fn list_todos() -> &'static str {
"List all of the todos here!"
}
#[route(GET, path = "/")]
fn index() -> Redirect {
Redirect::to("/todos")
}
fn main() {
let mut rocket = Rocket::new("localhost", 8000);
rocket.mount("/", routes![index, list_todos, new_todo]);
rocket.launch();
}

View File

@ -1,4 +1,8 @@
use handler::Handler;
use error::Error;
use response::Response;
use request::Request;
use error::RoutingError;
use codegen::StaticCatchInfo;
use std::fmt;
@ -7,15 +11,23 @@ use term_painter::Color::*;
pub struct Catcher {
pub code: u16,
pub handler: Handler,
handler: Handler,
is_default: bool
}
// TODO: Should `Catcher` be an interface? Should there be an `ErrorHandler`
// that takes in a `RoutingError` and returns a `Response`? What's the right
// interface here?
impl Catcher {
pub fn new(code: u16, handler: Handler) -> Catcher {
Catcher::new_with_default(code, handler, false)
}
pub fn handle<'a>(&'a self, error: RoutingError<'a>) -> Response {
(self.handler)(error.request)
}
fn new_with_default(code: u16, handler: Handler, default: bool) -> Catcher {
Catcher {
code: code,
@ -47,13 +59,6 @@ pub mod defaults {
use super::Catcher;
use std::collections::HashMap;
pub fn get() -> HashMap<u16, Catcher> {
let mut map = HashMap::new();
map.insert(404, Catcher::new_with_default(404, not_found, true));
map.insert(500, Catcher::new_with_default(500, internal_error, true));
map
}
pub fn not_found(_request: Request) -> Response {
Response::with_status(StatusCode::NotFound, "\
<head>\
@ -82,5 +87,11 @@ pub mod defaults {
<small>Rocket</small>\
")
}
}
pub fn get() -> HashMap<u16, Catcher> {
let mut map = HashMap::new();
map.insert(404, Catcher::new_with_default(404, not_found, true));
map.insert(500, Catcher::new_with_default(500, internal_error, true));
map
}
}

View File

@ -1,3 +1,5 @@
use request::Request;
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Error {
BadMethod,
@ -5,3 +7,29 @@ pub enum Error {
NoRoute,
NoKey
}
pub struct RoutingError<'r> {
pub error: Error,
pub request: Request<'r>,
pub chain: Option<&'r [&'r str]>
}
impl<'a> RoutingError<'a> {
pub fn unchained(request: Request<'a>)
-> RoutingError<'a> {
RoutingError {
error: Error::NoRoute,
request: request,
chain: None,
}
}
pub fn new(error: Error, request: Request<'a>, chain: &'a [&'a str])
-> RoutingError<'a> {
RoutingError {
error: error,
request: request,
chain: Some(chain)
}
}
}

View File

@ -26,7 +26,7 @@ pub use codegen::{StaticRouteInfo, StaticCatchInfo};
pub use request::Request;
pub use method::Method;
pub use response::{Response, Responder};
pub use error::Error;
pub use error::{Error, RoutingError};
pub use param::FromParam;
pub use router::{Router, Route};
pub use catcher::Catcher;

View File

@ -1,18 +1,22 @@
use error::Error;
use param::FromParam;
use method::Method;
pub use hyper::server::Request as HyperRequest;
#[derive(Clone)]
pub struct Request<'a> {
params: Vec<&'a str>,
params: Option<Vec<&'a str>>,
pub method: Method,
pub uri: &'a str,
pub data: &'a [u8]
}
impl<'a> Request<'a> {
pub fn new(params: Vec<&'a str>, uri: &'a str, data: &'a [u8]) -> Request<'a> {
pub fn new(method: Method, uri: &'a str, params: Option<Vec<&'a str>>,
data: &'a [u8]) -> Request<'a> {
Request {
method: method,
params: params,
uri: uri,
data: data
@ -24,10 +28,10 @@ impl<'a> Request<'a> {
}
pub fn get_param<T: FromParam<'a>>(&'a self, n: usize) -> Result<T, Error> {
if n >= self.params.len() {
if self.params.is_none() || n >= self.params.as_ref().unwrap().len() {
Err(Error::NoKey)
} else {
T::from_param(self.params[n])
T::from_param(self.params.as_ref().unwrap()[n])
}
}
}

View File

@ -80,10 +80,11 @@ impl Rocket {
// A closure which we call when we know there is no route.
let handle_not_found = |response: FreshHyperResponse| {
let request = Request::new(vec![], uri, &buf);
let handler_404 = self.catchers.get(&404).unwrap().handler;
println!("{}", Red.paint("\t<= Dispatch failed. Returning 404."));
handler_404(request).respond(response);
let request = Request::new(method, uri, None, &buf);
let catcher = self.catchers.get(&404).unwrap();
catcher.handle(RoutingError::unchained(request)).respond(response);
};
// No route found. Handle the not_found error and return.
@ -93,17 +94,20 @@ impl Rocket {
}
// Okay, we've got a route. Unwrap it, generate a request, and try to
// dispatch. TODO: keep trying lower ranked routes before dispatching a
// not found error.
// dispatch.
println!("\t=> {}", Magenta.paint("Dispatching request."));
let route = route.unwrap();
let params = route.get_params(uri);
let request = Request::new(params, uri, &buf);
let request = Request::new(method, uri, Some(params), &buf);
let outcome = (route.handler)(request).respond(res);
// TODO: keep trying lower ranked routes before dispatching a not found
// error.
println!("\t=> {} {}", White.paint("Outcome:"), outcome);
outcome.map_forward(|res| {
println!("{}", Red.paint("\t=> No further matching routes."));
// TODO: Have some way to know why this was failed forward. Use that
// instead of always using an unchained error.
handle_not_found(res);
});
}

View File

@ -62,7 +62,7 @@ pub fn error_decorator(ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem,
fn $catch_fn_name<'rocket>(_req: rocket::Request<'rocket>)
-> rocket::Response<'rocket> {
// TODO: Figure out what type signature of catcher should be.
let result = $fn_name();
let result = $fn_name(RoutingError::unchained(_req));
rocket::Response::with_raw_status($catch_code, result)
}
).unwrap();