Remove non-streaming requests. Use streaming requests everywhere.

This commit includes the following important API changes:

  * The `form` route parameter has been removed.
  * The `data` route parameter has been added.
  * Forms are not handled via the `data` parameter and `Form` type.
  * Removed the `data` parameter from `Request`.
  * Added `FromData` conversion trate and default implementation.
  * Added `DataOutcome` enum, which is the return type of `from_data`.
  * 'FromData' is now used to automatically derive the `data` parameter.
  * Moved `form` into `request` module.
  * Removed `Failure::new` in favor of direct value construction.

This commit includes the following important package additions:

  * Added a 'raw_upload' example.
  * `manual_routes` example uses `Data` parameter.
  * Now building and running tests with `--all-features` flag.
  * All exmaples have been updated to latest API.
  * Now using upstream Tera.

This commit includes the following important fixes:

  * Any valid ident is now allowed in single-parameter route parameters.
  * Lifetimes are now properly stripped in code generation.
  * `FromForm` derive now works on empty structs.
This commit is contained in:
Sergio Benitez 2016-10-12 00:14:42 -07:00
parent a688f59ca0
commit 2f35b23514
36 changed files with 926 additions and 504 deletions

View File

@ -26,4 +26,5 @@ members = [
"examples/form_kitchen_sink", "examples/form_kitchen_sink",
"examples/config", "examples/config",
"examples/hello_alt_methods", "examples/hello_alt_methods",
"examples/raw_upload",
] ]

View File

@ -1,5 +1,7 @@
#![allow(unused_imports)] // FIXME: Why is this coming from quote_tokens? #![allow(unused_imports)] // FIXME: Why is this coming from quote_tokens?
use std::mem::transmute;
use syntax::ext::base::{Annotatable, ExtCtxt}; use syntax::ext::base::{Annotatable, ExtCtxt};
use syntax::print::pprust::{stmt_to_string}; use syntax::print::pprust::{stmt_to_string};
use syntax::parse::token::{str_to_ident}; use syntax::parse::token::{str_to_ident};
@ -7,12 +9,13 @@ use syntax::ast::{ItemKind, Expr, MetaItem, Mutability, VariantData};
use syntax::codemap::Span; use syntax::codemap::Span;
use syntax::ext::build::AstBuilder; use syntax::ext::build::AstBuilder;
use syntax::ptr::P; use syntax::ptr::P;
use std::mem::transmute;
use syntax_ext::deriving::generic::MethodDef; use syntax_ext::deriving::generic::MethodDef;
use syntax_ext::deriving::generic::{StaticStruct, Substructure, TraitDef, ty}; use syntax_ext::deriving::generic::{StaticStruct, Substructure, TraitDef, ty};
use syntax_ext::deriving::generic::combine_substructure as c_s; use syntax_ext::deriving::generic::combine_substructure as c_s;
use utils::strip_ty_lifetimes;
static ONLY_STRUCTS_ERR: &'static str = "`FromForm` can only be derived for \ static ONLY_STRUCTS_ERR: &'static str = "`FromForm` can only be derived for \
structures with named fields."; structures with named fields.";
static PRIVATE_LIFETIME: &'static str = "'rocket"; static PRIVATE_LIFETIME: &'static str = "'rocket";
@ -113,11 +116,12 @@ pub fn from_form_derive(ecx: &mut ExtCtxt, span: Span, meta_item: &MetaItem,
fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substructure) -> P<Expr> { fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substructure) -> P<Expr> {
// Check that we specified the methods to the argument correctly. // Check that we specified the methods to the argument correctly.
let arg = if substr.nonself_args.len() == 1 { const EXPECTED_ARGS: usize = 1;
let arg = if substr.nonself_args.len() == EXPECTED_ARGS {
&substr.nonself_args[0] &substr.nonself_args[0]
} else { } else {
let msg = format!("incorrect number of arguments in `from_form_string`: \ let msg = format!("incorrect number of arguments in `from_form_string`: \
expected {}, found {}", 1, substr.nonself_args.len()); expected {}, found {}", EXPECTED_ARGS, substr.nonself_args.len());
cx.span_bug(trait_span, msg.as_str()); cx.span_bug(trait_span, msg.as_str());
}; };
@ -140,7 +144,8 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct
None => cx.span_fatal(trait_span, ONLY_STRUCTS_ERR) None => cx.span_fatal(trait_span, ONLY_STRUCTS_ERR)
}; };
fields_and_types.push((ident, &field.ty)); let stripped_ty = strip_ty_lifetimes(field.ty.clone());
fields_and_types.push((ident, stripped_ty));
} }
debug!("Fields and types: {:?}", fields_and_types); debug!("Fields and types: {:?}", fields_and_types);
@ -155,7 +160,7 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct
// placed into the final struct. They start out as `None` and are changed // placed into the final struct. They start out as `None` and are changed
// to Some when a parse completes, or some default value if the parse was // to Some when a parse completes, or some default value if the parse was
// unsuccessful and default() returns Some. // unsuccessful and default() returns Some.
for &(ref ident, ty) in &fields_and_types { for &(ref ident, ref ty) in &fields_and_types {
stmts.push(quote_stmt!(cx, stmts.push(quote_stmt!(cx,
let mut $ident: ::std::option::Option<$ty> = None; let mut $ident: ::std::option::Option<$ty> = None;
).unwrap()); ).unwrap());
@ -199,11 +204,13 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct
// This looks complicated but just generates the boolean condition checking // This looks complicated but just generates the boolean condition checking
// that each parameter actually is Some() or has a default value. // that each parameter actually is Some() or has a default value.
let mut failure_conditions = vec![]; let mut failure_conditions = vec![];
for (i, &(ref ident, ty)) in (&fields_and_types).iter().enumerate() {
// Start with `false` in case there are no fields.
failure_conditions.push(quote_tokens!(cx, false));
for &(ref ident, ref ty) in (&fields_and_types).iter() {
// Pushing an "||" (or) between every condition. // Pushing an "||" (or) between every condition.
if i > 0 { failure_conditions.push(quote_tokens!(cx, ||));
failure_conditions.push(quote_tokens!(cx, ||));
}
failure_conditions.push(quote_tokens!(cx, failure_conditions.push(quote_tokens!(cx,
if $ident.is_none() && if $ident.is_none() &&
@ -217,7 +224,7 @@ fn from_form_substructure(cx: &mut ExtCtxt, trait_span: Span, substr: &Substruct
// The fields of the struct, which are just the let bindings declared above // The fields of the struct, which are just the let bindings declared above
// or the default value. // or the default value.
let mut result_fields = vec![]; let mut result_fields = vec![];
for &(ref ident, ty) in &fields_and_types { for &(ref ident, ref ty) in &fields_and_types {
result_fields.push(quote_tokens!(cx, result_fields.push(quote_tokens!(cx,
$ident: $ident.unwrap_or_else(|| $ident: $ident.unwrap_or_else(||
<$ty as ::rocket::request::FromFormValue>::default().unwrap() <$ty as ::rocket::request::FromFormValue>::default().unwrap()

View File

@ -52,7 +52,7 @@ trait RouteGenerateExt {
fn gen_form(&self, &ExtCtxt, Option<&Spanned<Ident>>, P<Expr>) -> Option<Stmt>; fn gen_form(&self, &ExtCtxt, Option<&Spanned<Ident>>, P<Expr>) -> Option<Stmt>;
fn missing_declared_err<T: Display>(&self, ecx: &ExtCtxt, arg: &Spanned<T>); fn missing_declared_err<T: Display>(&self, ecx: &ExtCtxt, arg: &Spanned<T>);
fn generate_form_statement(&self, ecx: &ExtCtxt) -> Option<Stmt>; fn generate_data_statement(&self, ecx: &ExtCtxt) -> Option<Stmt>;
fn generate_query_statement(&self, ecx: &ExtCtxt) -> Option<Stmt>; fn generate_query_statement(&self, ecx: &ExtCtxt) -> Option<Stmt>;
fn generate_param_statements(&self, ecx: &ExtCtxt) -> Vec<Stmt>; fn generate_param_statements(&self, ecx: &ExtCtxt) -> Vec<Stmt>;
fn generate_fn_arguments(&self, ecx: &ExtCtxt) -> Vec<TokenTree>; fn generate_fn_arguments(&self, ecx: &ExtCtxt) -> Vec<TokenTree>;
@ -89,20 +89,31 @@ impl RouteGenerateExt for RouteParams {
).expect("form statement")) ).expect("form statement"))
} }
fn generate_form_statement(&self, ecx: &ExtCtxt) -> Option<Stmt> { fn generate_data_statement(&self, ecx: &ExtCtxt) -> Option<Stmt> {
let param = self.form_param.as_ref().map(|p| &p.value); let param = self.data_param.as_ref().map(|p| &p.value);
let expr = quote_expr!(ecx, let arg = param.and_then(|p| self.annotated_fn.find_input(&p.node.name));
match ::std::str::from_utf8(_req.data.as_slice()) { if param.is_none() {
Ok(s) => s, return None;
Err(_) => { } else if arg.is_none() {
println!(" => Form is not valid UTF8."); self.missing_declared_err(ecx, &param.unwrap());
return ::rocket::Response::failed( return None;
::rocket::http::StatusCode::BadRequest); }
}
}
);
self.gen_form(ecx, param, expr) let arg = arg.unwrap();
let name = arg.ident().expect("form param identifier").prepend(PARAM_PREFIX);
let ty = strip_ty_lifetimes(arg.ty.clone());
Some(quote_stmt!(ecx,
let $name: $ty =
match ::rocket::request::FromData::from_data(&_req, _data) {
::rocket::request::DataOutcome::Success(d) => d,
::rocket::request::DataOutcome::Forward(d) =>
return ::rocket::Response::forward(d),
::rocket::request::DataOutcome::Failure(_) => {
let code = ::rocket::http::StatusCode::BadRequest;
return ::rocket::Response::failed(code);
}
};
).expect("data statement"))
} }
fn generate_query_statement(&self, ecx: &ExtCtxt) -> Option<Stmt> { fn generate_query_statement(&self, ecx: &ExtCtxt) -> Option<Stmt> {
@ -148,11 +159,11 @@ impl RouteGenerateExt for RouteParams {
).expect("declared param parsing statement")); ).expect("declared param parsing statement"));
} }
// A from_request parameter is one that isn't declared, form, or query. // A from_request parameter is one that isn't declared, data, or query.
let from_request = |a: &&Arg| { let from_request = |a: &&Arg| {
if let Some(name) = a.name() { if let Some(name) = a.name() {
!declared_set.contains(name) !declared_set.contains(name)
&& self.form_param.as_ref().map_or(true, |p| { && self.data_param.as_ref().map_or(true, |p| {
!a.named(&p.value().name) !a.named(&p.value().name)
}) && self.query_param.as_ref().map_or(true, |p| { }) && self.query_param.as_ref().map_or(true, |p| {
!a.named(&p.node.name) !a.named(&p.node.name)
@ -163,7 +174,7 @@ impl RouteGenerateExt for RouteParams {
} }
}; };
// Generate the code for `form_request` parameters. // Generate the code for `from_request` parameters.
let all = &self.annotated_fn.decl().inputs; let all = &self.annotated_fn.decl().inputs;
for arg in all.iter().filter(from_request) { for arg in all.iter().filter(from_request) {
let ident = arg.ident().unwrap().prepend(PARAM_PREFIX); let ident = arg.ident().unwrap().prepend(PARAM_PREFIX);
@ -214,9 +225,9 @@ fn generic_route_decorator(known_method: Option<Spanned<Method>>,
let route = RouteParams::from(ecx, sp, known_method, meta_item, annotated); let route = RouteParams::from(ecx, sp, known_method, meta_item, annotated);
debug!("Route params: {:?}", route); debug!("Route params: {:?}", route);
let form_statement = route.generate_form_statement(ecx);
let query_statement = route.generate_query_statement(ecx);
let param_statements = route.generate_param_statements(ecx); let param_statements = route.generate_param_statements(ecx);
let query_statement = route.generate_query_statement(ecx);
let data_statement = route.generate_data_statement(ecx);
let fn_arguments = route.generate_fn_arguments(ecx); let fn_arguments = route.generate_fn_arguments(ecx);
// Generate and emit the wrapping function with the Rocket handler signature. // Generate and emit the wrapping function with the Rocket handler signature.
@ -225,9 +236,9 @@ fn generic_route_decorator(known_method: Option<Spanned<Method>>,
emit_item(push, quote_item!(ecx, emit_item(push, quote_item!(ecx,
fn $route_fn_name<'_b>(_req: &'_b ::rocket::Request, _data: ::rocket::Data) fn $route_fn_name<'_b>(_req: &'_b ::rocket::Request, _data: ::rocket::Data)
-> ::rocket::Response<'_b> { -> ::rocket::Response<'_b> {
$form_statement
$query_statement
$param_statements $param_statements
$query_statement
$data_statement
let result = $user_fn_name($fn_arguments); let result = $user_fn_name($fn_arguments);
::rocket::Response::complete(result) ::rocket::Response::complete(result)
} }

View File

@ -6,7 +6,7 @@ use syntax::ext::base::{ExtCtxt, Annotatable};
use syntax::codemap::{Span, Spanned, dummy_spanned}; use syntax::codemap::{Span, Spanned, dummy_spanned};
use syntax::parse::token::str_to_ident; use syntax::parse::token::str_to_ident;
use utils::{span, MetaItemExt, SpanExt}; use utils::{span, MetaItemExt, SpanExt, is_valid_ident};
use super::{Function, ParamIter}; use super::{Function, ParamIter};
use super::keyvalue::KVSpanned; use super::keyvalue::KVSpanned;
use rocket::http::{Method, ContentType}; use rocket::http::{Method, ContentType};
@ -22,7 +22,7 @@ pub struct RouteParams {
pub annotated_fn: Function, pub annotated_fn: Function,
pub method: Spanned<Method>, pub method: Spanned<Method>,
pub path: Spanned<String>, pub path: Spanned<String>,
pub form_param: Option<KVSpanned<Ident>>, pub data_param: Option<KVSpanned<Ident>>,
pub query_param: Option<Spanned<Ident>>, pub query_param: Option<Spanned<Ident>>,
pub format: Option<KVSpanned<ContentType>>, pub format: Option<KVSpanned<ContentType>>,
pub rank: Option<KVSpanned<isize>>, pub rank: Option<KVSpanned<isize>>,
@ -74,7 +74,7 @@ impl RouteParams {
// Parse all of the optional parameters. // Parse all of the optional parameters.
let mut seen_keys = HashSet::new(); let mut seen_keys = HashSet::new();
let (mut rank, mut form, mut format) = Default::default(); let (mut rank, mut data, mut format) = Default::default();
for param in &attr_params[1..] { for param in &attr_params[1..] {
let kv_opt = kv_from_nested(&param); let kv_opt = kv_from_nested(&param);
if kv_opt.is_none() { if kv_opt.is_none() {
@ -85,7 +85,7 @@ impl RouteParams {
let kv = kv_opt.unwrap(); let kv = kv_opt.unwrap();
match kv.key().as_str() { match kv.key().as_str() {
"rank" => rank = parse_opt(ecx, &kv, parse_rank), "rank" => rank = parse_opt(ecx, &kv, parse_rank),
"form" => form = parse_opt(ecx, &kv, parse_form), "data" => data = parse_opt(ecx, &kv, parse_data),
"format" => format = parse_opt(ecx, &kv, parse_format), "format" => format = parse_opt(ecx, &kv, parse_format),
_ => { _ => {
let msg = format!("'{}' is not a known parameter", kv.key()); let msg = format!("'{}' is not a known parameter", kv.key());
@ -107,7 +107,7 @@ impl RouteParams {
RouteParams { RouteParams {
method: method, method: method,
path: path, path: path,
form_param: form, data_param: data,
query_param: query, query_param: query,
format: format, format: format,
rank: rank, rank: rank,
@ -145,7 +145,7 @@ fn param_string_to_ident(ecx: &ExtCtxt, s: Spanned<&str>) -> Option<Ident> {
let string = s.node; let string = s.node;
if string.starts_with('<') && string.ends_with('>') { if string.starts_with('<') && string.ends_with('>') {
let param = &string[1..(string.len() - 1)]; let param = &string[1..(string.len() - 1)];
if param.chars().all(char::is_alphanumeric) { if is_valid_ident(param) {
return Some(str_to_ident(param)); return Some(str_to_ident(param));
} }
@ -217,21 +217,23 @@ fn parse_opt<O, T, F>(ecx: &ExtCtxt, kv: &KVSpanned<T>, f: F) -> Option<KVSpanne
Some(kv.map_ref(|_| f(ecx, kv))) Some(kv.map_ref(|_| f(ecx, kv)))
} }
fn parse_form(ecx: &ExtCtxt, kv: &KVSpanned<LitKind>) -> Ident { fn parse_data(ecx: &ExtCtxt, kv: &KVSpanned<LitKind>) -> Ident {
let mut str_name = "unknown";
if let LitKind::Str(ref s, _) = *kv.value() { if let LitKind::Str(ref s, _) = *kv.value() {
str_name = s;
if let Some(ident) = param_string_to_ident(ecx, span(s, kv.value.span)) { if let Some(ident) = param_string_to_ident(ecx, span(s, kv.value.span)) {
return ident; return ident;
} }
} }
let err_string = r#"`form` value must be a parameter, e.g: "<name>"`"#; let err_string = r#"`data` value must be a parameter, e.g: "<name>"`"#;
ecx.struct_span_fatal(kv.span, err_string) ecx.struct_span_fatal(kv.span, err_string)
.help(r#"form, if specified, must be a key-value pair where .help(r#"data, if specified, must be a key-value pair where
the key is `form` and the value is a string with a single the key is `data` and the value is a string with a single
parameter inside '<' '>'. e.g: form = "<login>""#) parameter inside '<' '>'. e.g: data = "<user_form>""#)
.emit(); .emit();
str_to_ident("") str_to_ident(str_name)
} }
fn parse_rank(ecx: &ExtCtxt, kv: &KVSpanned<LitKind>) -> isize { fn parse_rank(ecx: &ExtCtxt, kv: &KVSpanned<LitKind>) -> isize {

View File

@ -87,6 +87,10 @@ impl Folder for TyLifetimeRemover {
fn fold_lifetime_defs(&mut self, _: Vec<LifetimeDef>) -> Vec<LifetimeDef> { fn fold_lifetime_defs(&mut self, _: Vec<LifetimeDef>) -> Vec<LifetimeDef> {
vec![] vec![]
} }
fn fold_lifetimes(&mut self, _: Vec<Lifetime>) -> Vec<Lifetime> {
vec![]
}
} }
pub fn strip_ty_lifetimes(ty: P<Ty>) -> P<Ty> { pub fn strip_ty_lifetimes(ty: P<Ty>) -> P<Ty> {

View File

@ -7,7 +7,7 @@ fn get(other: &str) -> &'static str { "hi" } //~ ERROR isn't in the function
#[get("/a?<r>")] //~ ERROR 'r' is declared #[get("/a?<r>")] //~ ERROR 'r' is declared
fn get1() -> &'static str { "hi" } //~ ERROR isn't in the function fn get1() -> &'static str { "hi" } //~ ERROR isn't in the function
#[get("/a", form = "<test>")] //~ ERROR 'test' is declared #[get("/a", data = "<test>")] //~ ERROR 'test' is declared
fn get2() -> &'static str { "hi" } //~ ERROR isn't in the function fn get2() -> &'static str { "hi" } //~ ERROR isn't in the function
fn main() { } fn main() { }

View File

@ -4,14 +4,15 @@
extern crate rocket; extern crate rocket;
use rocket::http::Cookies; use rocket::http::Cookies;
use rocket::request::Form;
#[derive(FromForm)] #[derive(FromForm)]
struct User { struct User {
name: String name: String
} }
#[post("/<name>?<query>", format = "application/json", form = "<user>", rank = 2)] #[post("/<name>?<query>", format = "application/json", data = "<user>", rank = 2)]
fn get(name: &str, query: User, user: User, cookies: &Cookies) -> &'static str { "hi" } fn get(name: &str, query: User, user: Form<User>, cookies: &Cookies) -> &'static str { "hi" }
fn main() { fn main() {
let _ = routes![get]; let _ = routes![get];

View File

@ -0,0 +1,15 @@
#![feature(plugin, custom_derive)]
#![plugin(rocket_codegen)]
extern crate rocket;
use rocket::request::FromForm;
#[derive(PartialEq, Debug, FromForm)]
struct Form { }
fn main() {
// Same number of arguments: simple case.
let task = Form::from_form_string("");
assert_eq!(task, Ok(Form { }));
}

View File

@ -25,8 +25,4 @@ serde_json = { version = "^0.8", optional = true }
handlebars = { version = "^0.21", optional = true, features = ["serde_type"] } handlebars = { version = "^0.21", optional = true, features = ["serde_type"] }
glob = { version = "^0.2", optional = true } glob = { version = "^0.2", optional = true }
lazy_static = { version = "^0.2", optional = true } lazy_static = { version = "^0.2", optional = true }
tera = { version = "^0.3", optional = true }
# Tera dependency
[dependencies.tera]
git = "https://github.com/SergioBenitez/tera"
optional = true

View File

@ -2,39 +2,39 @@ extern crate serde;
extern crate serde_json; extern crate serde_json;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::io::Read;
use rocket::request::{Request, FromRequest}; use rocket::request::{Request, Data, FromData, DataOutcome};
use rocket::response::{Responder, Outcome, ResponseOutcome, data}; use rocket::response::{Responder, Outcome, ResponseOutcome, data};
use rocket::http::StatusCode; use rocket::http::StatusCode;
use rocket::http::hyper::FreshHyperResponse; use rocket::http::hyper::FreshHyperResponse;
use self::serde::{Serialize, Deserialize}; use self::serde::{Serialize, Deserialize};
use self::serde_json::Error as JSONError; use self::serde_json::error::Error as SerdeError;
/// The JSON type, which implements both `FromRequest` and `Responder`. This /// The JSON type, which implements `FromData` and `Responder`. This type allows
/// type allows you to trivially consume and respond with JSON in your Rocket /// you to trivially consume and respond with JSON in your Rocket application.
/// application.
/// ///
/// If you're receiving JSON data, simple add a `JSON<T>` type to your function /// If you're receiving JSON data, simple add a `data` parameter to your route
/// signature where `T` is some type you'd like to parse from JSON. `T` must /// arguments and ensure the type o the parameter is a `JSON<T>`, where `T` is
/// implement `Deserialize` from [Serde](https://github.com/serde-rs/json). The /// some type you'd like to parse from JSON. `T` must implement `Deserialize`
/// data is parsed from the HTTP request body. /// from [Serde](https://github.com/serde-rs/json). The data is parsed from the
/// HTTP request body.
/// ///
/// ```rust,ignore /// ```rust,ignore
/// #[post("/users/", format = "application/json")] /// #[post("/users/", format = "application/json", data = "<user>")]
/// fn new_user(user: JSON<User>) { /// fn new_user(user: JSON<User>) {
/// ... /// ...
/// } /// }
/// ``` /// ```
/// You don't _need_ to use `format = "application/json"`, but it _may_ be what /// You don't _need_ to use `format = "application/json"`, but it _may_ be what
/// you want. Using `format = application/json` means that any request that /// you want. Using `format = application/json` means that any request that
/// doesn't specify "application/json" as its first `Accept:` header parameter /// doesn't specify "application/json" as its first `Content-Type:` header
/// will not be routed to this handler. /// parameter will not be routed to this handler.
/// ///
/// If you're responding with JSON data, return a `JSON<T>` type, where `T` /// If you're responding with JSON data, return a `JSON<T>` type, where `T`
/// implements implements `Serialize` from /// implements `Serialize` from [Serde](https://github.com/serde-rs/json). The
/// [Serde](https://github.com/serde-rs/json). The content type is set to /// content type of the response is set to `application/json` automatically.
/// `application/json` automatically.
/// ///
/// ```rust,ignore /// ```rust,ignore
/// #[get("/users/<id>")] /// #[get("/users/<id>")]
@ -63,10 +63,21 @@ impl<T> JSON<T> {
} }
} }
impl<'r, T: Deserialize> FromRequest<'r> for JSON<T> { /// Maximum size of JSON is 1MB.
type Error = JSONError; /// TODO: Determine this size from some configuration parameter.
fn from_request(request: &'r Request) -> Result<Self, Self::Error> { const MAX_SIZE: u64 = 1048576;
Ok(JSON(serde_json::from_slice(request.data.as_slice())?))
impl<T: Deserialize> FromData for JSON<T> {
type Error = SerdeError;
fn from_data(request: &Request, data: Data) -> DataOutcome<Self, SerdeError> {
if !request.content_type().is_json() {
error_!("Content-Type is not JSON.");
return DataOutcome::Forward(data);
}
let reader = data.open().take(MAX_SIZE);
DataOutcome::from(serde_json::from_reader(reader).map(|val| JSON(val)))
} }
} }

View File

@ -8,6 +8,7 @@ extern crate rocket;
use std::collections::HashMap; use std::collections::HashMap;
use rocket::request::Form;
use rocket::response::Redirect; use rocket::response::Redirect;
use rocket::http::{Cookie, Cookies}; use rocket::http::{Cookie, Cookies};
use rocket_contrib::Template; use rocket_contrib::Template;
@ -17,9 +18,9 @@ struct Message {
message: String message: String
} }
#[post("/submit", form = "<message>")] #[post("/submit", data = "<message>")]
fn submit(cookies: &Cookies, message: Message) -> Redirect { fn submit(cookies: &Cookies, message: Form<Message>) -> Redirect {
cookies.add(Cookie::new("message".into(), message.message)); cookies.add(Cookie::new("message".into(), message.into_inner().message));
Redirect::to("/") Redirect::to("/")
} }

View File

@ -8,7 +8,7 @@ fn index() -> io::Result<NamedFile> {
NamedFile::open("static/index.html") NamedFile::open("static/index.html")
} }
#[get("/<file..>")] #[get("/<file..>", rank = 2)]
fn files(file: PathBuf) -> io::Result<NamedFile> { fn files(file: PathBuf) -> io::Result<NamedFile> {
NamedFile::open(Path::new("static/").join(file)) NamedFile::open(Path::new("static/").join(file))
} }

View File

@ -6,7 +6,7 @@ extern crate rocket;
mod files; mod files;
use rocket::response::Redirect; use rocket::response::Redirect;
use rocket::request::FromFormValue; use rocket::request::{Form, FromFormValue};
#[derive(Debug)] #[derive(Debug)]
struct StrongPassword<'r>(&'r str); struct StrongPassword<'r>(&'r str);
@ -49,24 +49,26 @@ impl<'v> FromFormValue<'v> for AdultAge {
} }
} }
#[post("/login", form = "<user>")] #[post("/login", data = "<user_form>")]
fn login(user: UserLogin) -> Result<Redirect, String> { fn login<'a>(user_form: Form<'a, UserLogin<'a>>) -> Result<Redirect, String> {
if user.age.is_err() { let user = user_form.get();
return Err(String::from(user.age.unwrap_err()));
if let Err(e) = user.age {
return Err(format!("Age is invalid: {}", e));
} }
if user.password.is_err() { if let Err(e) = user.password {
return Err(String::from(user.password.unwrap_err())); return Err(format!("Password is invalid: {}", e));
} }
match user.username { if user.username == "Sergio" {
"Sergio" => { if let Ok(StrongPassword("password")) = user.password {
match user.password.unwrap().0 { Ok(Redirect::other("/user/Sergio"))
"password" => Ok(Redirect::other("/user/Sergio")), } else {
_ => Err("Wrong password!".to_string()), Err("Wrong password!".to_string())
}
} }
_ => Err(format!("Unrecognized user, '{}'.", user.username)), } else {
Err(format!("Unrecognized user, '{}'.", user.username))
} }
} }

View File

@ -3,7 +3,7 @@
extern crate rocket; extern crate rocket;
use rocket::request::{Request, FromFormValue}; use rocket::request::{Form, FromFormValue};
use rocket::response::NamedFile; use rocket::response::NamedFile;
use std::io; use std::io;
@ -29,24 +29,22 @@ impl<'v> FromFormValue<'v> for FormOption {
} }
#[derive(Debug, FromForm)] #[derive(Debug, FromForm)]
struct FormInput<'r> { struct FormInput {
checkbox: bool, checkbox: bool,
number: usize, number: usize,
radio: FormOption, radio: FormOption,
password: &'r str, password: String,
textarea: String, textarea: String,
select: FormOption, select: FormOption,
} }
#[post("/", form = "<sink>")] #[post("/", data = "<sink>")]
fn sink(sink: FormInput) -> String { fn sink(sink: Result<Form<FormInput>, Option<String>>) -> String {
format!("{:?}", sink) match sink {
} Ok(form) => format!("{:?}", form.get()),
Err(Some(f)) => format!("Invalid form input: {}", f),
#[post("/", rank = 2)] Err(None) => format!("Form input was invalid UTF8."),
fn sink2(request: &Request) -> &'static str { }
println!("form: {:?}", std::str::from_utf8(request.data.as_slice()));
"Sorry, the form is invalid."
} }
#[get("/")] #[get("/")]
@ -56,6 +54,6 @@ fn index() -> io::Result<NamedFile> {
fn main() { fn main() {
rocket::ignite() rocket::ignite()
.mount("/", routes![index, sink, sink2]) .mount("/", routes![index, sink])
.launch(); .launch();
} }

View File

@ -8,7 +8,7 @@ fn index() -> io::Result<NamedFile> {
NamedFile::open("static/index.html") NamedFile::open("static/index.html")
} }
#[get("/<file..>")] #[get("/<file..>", rank = 5)]
fn files(file: PathBuf) -> io::Result<NamedFile> { fn files(file: PathBuf) -> io::Result<NamedFile> {
NamedFile::open(Path::new("static/").join(file)) NamedFile::open(Path::new("static/").join(file))
} }

View File

@ -5,37 +5,38 @@ extern crate rocket;
mod files; mod files;
use rocket::request::Form;
use rocket::response::Redirect; use rocket::response::Redirect;
#[derive(FromForm)] #[derive(FromForm)]
struct UserLogin<'r> { struct UserLogin<'r> {
username: &'r str, username: &'r str,
password: &'r str, password: &'r str,
age: Result<isize, &'r str>, age: Result<usize, &'r str>,
} }
#[post("/login", form = "<user>")] #[post("/login", data = "<user_form>")]
fn login(user: UserLogin) -> Result<Redirect, String> { fn login<'a>(user_form: Form<'a, UserLogin<'a>>) -> Result<Redirect, String> {
if user.age.is_err() { let user = user_form.get();
let input = user.age.unwrap_err(); match user.age {
return Err(format!("'{}' is not a valid age integer.", input)); Ok(age) if age < 21 => return Err(format!("Sorry, {} is too young!", age)),
} Ok(age) if age > 120 => return Err(format!("Are you sure you're {}?", age)),
Err(e) => return Err(format!("'{}' is not a valid integer.", e)),
Ok(_) => { /* Move along, adult. */ }
};
let age = user.age.unwrap(); if user.username == "Sergio" {
if age < 20 { match user.password {
return Err(format!("Sorry, {} is too young!", age));
}
match user.username {
"Sergio" => match user.password {
"password" => Ok(Redirect::other("/user/Sergio")), "password" => Ok(Redirect::other("/user/Sergio")),
_ => Err("Wrong password!".to_string()) _ => Err("Wrong password!".to_string())
}, }
_ => Err(format!("Unrecognized user, '{}'.", user.username)) } else {
Err(format!("Unrecognized user, '{}'.", user.username))
} }
} }
#[post("/user/<username>")]
#[get("/user/<username>")]
fn user_page(username: &str) -> String { fn user_page(username: &str) -> String {
format!("This is {}'s page.", username) format!("This is {}'s page.", username)
} }

View File

@ -40,7 +40,7 @@ struct Message {
// None // None
// } // }
#[post("/<id>", format = "application/json")] #[post("/<id>", format = "application/json", data = "<message>")]
fn new(id: ID, message: JSON<Message>) -> JSON<SimpleMap> { fn new(id: ID, message: JSON<Message>) -> JSON<SimpleMap> {
let mut hashmap = MAP.lock().unwrap(); let mut hashmap = MAP.lock().unwrap();
if hashmap.contains_key(&id) { if hashmap.contains_key(&id) {
@ -54,7 +54,7 @@ fn new(id: ID, message: JSON<Message>) -> JSON<SimpleMap> {
} }
} }
#[put("/<id>", format = "application/json")] #[put("/<id>", format = "application/json", data = "<message>")]
fn update(id: ID, message: JSON<Message>) -> Option<JSON<SimpleMap>> { fn update(id: ID, message: JSON<Message>) -> Option<JSON<SimpleMap>> {
let mut hashmap = MAP.lock().unwrap(); let mut hashmap = MAP.lock().unwrap();
if hashmap.contains_key(&id) { if hashmap.contains_key(&id) {

View File

@ -27,6 +27,7 @@ fn echo_url<'a>(req: &'a Request, _: Data) -> Response<'a> {
fn upload(req: &Request, data: Data) -> Response { fn upload(req: &Request, data: Data) -> Response {
if !req.content_type().is_text() { if !req.content_type().is_text() {
println!(" => Content-Type of upload must be data. Ignoring.");
return Response::failed(StatusCode::BadRequest); return Response::failed(StatusCode::BadRequest);
} }

View File

@ -0,0 +1,9 @@
[package]
name = "raw_upload"
version = "0.0.1"
authors = ["Sergio Benitez <sb@sergio.bz>"]
workspace = "../../"
[dependencies]
rocket = { path = "../../lib" }
rocket_codegen = { path = "../../codegen" }

View File

@ -0,0 +1,33 @@
#![feature(plugin)]
#![plugin(rocket_codegen)]
extern crate rocket;
use rocket::request::Data;
use rocket::response::Failure;
use rocket::http::{StatusCode, ContentType};
#[post("/upload", data = "<data>")]
fn upload(content_type: ContentType, data: Data) -> Result<String, Failure> {
if !content_type.is_text() {
println!(" => Content-Type of upload must be text. Ignoring.");
return Err(Failure(StatusCode::BadRequest));
}
match data.stream_to_file("/tmp/upload.txt") {
Ok(n) => Ok(format!("OK: {} bytes uploaded.", n)),
Err(e) => {
println!(" => Failed writing to file: {:?}.", e);
return Err(Failure(StatusCode::InternalServerError));
}
}
}
#[get("/")]
fn index() -> &'static str {
"Upload your text files by POSTing them to /upload."
}
fn main() {
rocket::ignite().mount("/", routes![index, upload]).launch();
}

View File

@ -11,12 +11,13 @@ extern crate serde_json;
mod static_files; mod static_files;
mod task; mod task;
use rocket::request::Form;
use rocket::response::{Flash, Redirect}; use rocket::response::{Flash, Redirect};
use rocket_contrib::Template; use rocket_contrib::Template;
use task::Task; use task::Task;
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct Context<'a, 'b>{msg: Option<(&'a str, &'b str)>, tasks: Vec<Task>} struct Context<'a, 'b>{ msg: Option<(&'a str, &'b str)>, tasks: Vec<Task> }
impl<'a, 'b> Context<'a, 'b> { impl<'a, 'b> Context<'a, 'b> {
pub fn err(msg: &'a str) -> Context<'static, 'a> { pub fn err(msg: &'a str) -> Context<'static, 'a> {
@ -28,14 +29,15 @@ impl<'a, 'b> Context<'a, 'b> {
} }
} }
#[post("/", form = "<todo>")] #[post("/", data = "<todo_form>")]
fn new(todo: Task) -> Result<Flash<Redirect>, Template> { fn new(todo_form: Form<Task>) -> Flash<Redirect> {
let todo = todo_form.into_inner();
if todo.description.is_empty() { if todo.description.is_empty() {
Err(Template::render("index", &Context::err("Description cannot be empty."))) Flash::error(Redirect::to("/"), "Description cannot be empty.")
} else if todo.insert() { } else if todo.insert() {
Ok(Flash::success(Redirect::to("/"), "Todo successfully added.")) Flash::success(Redirect::to("/"), "Todo successfully added.")
} else { } else {
Err(Template::render("index", &Context::err("Whoops! The server failed."))) Flash::error(Redirect::to("/"), "Whoops! The server failed.")
} }
} }

View File

@ -1,6 +1,7 @@
#![feature(question_mark)] #![feature(question_mark)]
#![feature(specialization)] #![feature(specialization)]
#![feature(conservative_impl_trait)] #![feature(conservative_impl_trait)]
#![feature(associated_type_defaults)]
//! # Rocket - Core API Documentation //! # Rocket - Core API Documentation
//! //!

View File

@ -0,0 +1,52 @@
use std::io::{self, BufRead, Read, Cursor, BufReader, Chain, Take};
use std::net::Shutdown;
use http::hyper::{HyperHttpStream, HyperHttpReader};
use http::hyper::HyperNetworkStream;
pub type StreamReader = HyperHttpReader<HyperHttpStream>;
pub type InnerStream = Chain<Take<Cursor<Vec<u8>>>, BufReader<StreamReader>>;
pub struct DataStream {
pub stream: InnerStream,
pub network: HyperHttpStream,
}
impl Read for DataStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.stream.read(buf)
}
}
impl BufRead for DataStream {
fn fill_buf(&mut self) -> io::Result<&[u8]> {
self.stream.fill_buf()
}
fn consume(&mut self, amt: usize) {
self.stream.consume(amt)
}
}
pub fn kill_stream<S: Read, N: HyperNetworkStream>(stream: &mut S, network: &mut N) {
io::copy(&mut stream.take(1024), &mut io::sink()).expect("sink");
// If there are any more bytes, kill it.
let mut buf = [0];
if let Ok(n) = stream.read(&mut buf) {
if n > 0 {
warn_!("Data left unread. Force closing network stream.");
if let Err(e) = network.close(Shutdown::Both) {
error_!("Failed to close network stream: {:?}", e);
}
}
}
}
impl Drop for DataStream {
// Be a bad citizen and close the TCP stream if there's unread data.
fn drop(&mut self) {
kill_stream(&mut self.stream, &mut self.network);
}
}

View File

@ -0,0 +1,63 @@
use std::fmt::Debug;
use request::{Request, Data};
/// Trait used to derive an object from incoming request data.
pub trait FromData: Sized {
type Error = ();
fn from_data(request: &Request, data: Data) -> DataOutcome<Self, Self::Error>;
}
impl<T: FromData> FromData for Result<T, T::Error> {
fn from_data(request: &Request, data: Data) -> DataOutcome<Self, Self::Error> {
match T::from_data(request, data) {
DataOutcome::Success(val) => DataOutcome::Success(Ok(val)),
DataOutcome::Failure(val) => DataOutcome::Success(Err(val)),
DataOutcome::Forward(data) => DataOutcome::Forward(data)
}
}
}
impl<T: FromData> FromData for Option<T> {
fn from_data(request: &Request, data: Data) -> DataOutcome<Self, Self::Error> {
match T::from_data(request, data) {
DataOutcome::Success(val) => DataOutcome::Success(Some(val)),
DataOutcome::Failure(_) => DataOutcome::Success(None),
DataOutcome::Forward(data) => DataOutcome::Forward(data)
}
}
}
#[must_use]
pub enum DataOutcome<T, E> {
/// Signifies that all processing completed successfully.
Success(T),
/// Signifies that some processing occurred that ultimately resulted in
/// failure. As a result, no further processing can occur.
Failure(E),
/// Signifies that no processing occured and as such, processing can be
/// forwarded to the next available target.
Forward(Data),
}
impl<T, E: Debug> From<Result<T, E>> for DataOutcome<T, E> {
fn from(result: Result<T, E>) -> Self {
match result {
Ok(val) => DataOutcome::Success(val),
Err(e) => {
error_!("{:?}", e);
DataOutcome::Failure(e)
}
}
}
}
impl<T> From<Option<T>> for DataOutcome<T, ()> {
fn from(result: Option<T>) -> Self {
match result {
Some(val) => DataOutcome::Success(val),
None => DataOutcome::Failure(())
}
}
}

View File

@ -1,71 +1,30 @@
use std::io::{self, BufRead, Read, Cursor, BufReader, Chain, Take}; //! Talk about the data thing.
use std::time::Duration;
use std::net::Shutdown;
use http::hyper::{HyperBodyReader, HyperHttpStream, HyperHttpReader}; mod from_data;
mod data_stream;
pub use self::from_data::{FromData, DataOutcome};
use std::io::{self, BufRead, Read, Write, Cursor, BufReader};
use std::path::Path;
use std::fs::File;
use std::time::Duration;
use std::mem::transmute;
use self::data_stream::{DataStream, StreamReader, kill_stream};
use request::Request;
use http::hyper::{HyperBodyReader, HyperHttpStream};
use http::hyper::HyperNetworkStream; use http::hyper::HyperNetworkStream;
use http::hyper::HyperHttpReader::*; use http::hyper::HyperHttpReader::*;
type StreamReader = HyperHttpReader<HyperHttpStream>;
pub struct DataStream {
stream: Chain<Take<Cursor<Vec<u8>>>, BufReader<StreamReader>>,
network: HyperHttpStream,
}
impl Read for DataStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.stream.read(buf)
}
}
impl BufRead for DataStream {
fn fill_buf(&mut self) -> io::Result<&[u8]> {
self.stream.fill_buf()
}
fn consume(&mut self, amt: usize) {
self.stream.consume(amt)
}
}
fn try_sinking<N: HyperNetworkStream>(net: &mut N) -> bool {
warn_!("Data left unread. Sinking 1k bytes.");
io::copy(&mut net.take(1024), &mut io::sink()).expect("sink");
// If there are any more bytes, kill it.
let mut buf = [0];
if let Ok(n) = net.read(&mut buf) {
if n > 0 {
warn_!("Data still remains. Force closing network stream.");
return net.close(Shutdown::Both).is_ok();
}
}
false
}
impl Drop for DataStream {
// Be a bad citizen and close the TCP stream if there's unread data.
// Unfortunately, Hyper forces us to do this.
fn drop(&mut self) {
try_sinking(&mut self.network);
}
}
pub struct Data { pub struct Data {
buffer: Vec<u8>, buffer: Vec<u8>,
is_done: bool,
stream: StreamReader, stream: StreamReader,
position: usize, position: usize,
capacity: usize, capacity: usize,
} }
impl Drop for Data {
fn drop(&mut self) {
try_sinking(self.stream.get_mut());
}
}
impl Data { impl Data {
pub fn open(mut self) -> impl BufRead { pub fn open(mut self) -> impl BufRead {
// Swap out the buffer and stream for empty ones so we can move. // Swap out the buffer and stream for empty ones so we can move.
@ -115,19 +74,88 @@ impl Data {
Ok(Data::new(vec, pos, cap, stream)) Ok(Data::new(vec, pos, cap, stream))
} }
#[inline(always)]
pub fn peek(&self) -> &[u8] { pub fn peek(&self) -> &[u8] {
&self.buffer[self.position..self.capacity] &self.buffer[self.position..self.capacity]
} }
pub fn new(buf: Vec<u8>, pos: usize, cap: usize, stream: StreamReader) -> Data { #[inline(always)]
pub fn peek_complete(&self) -> bool {
self.is_done
}
#[inline(always)]
pub fn stream_to<W: Write>(self, writer: &mut W) -> io::Result<u64> {
io::copy(&mut self.open(), writer)
}
#[inline(always)]
pub fn stream_to_file<P: AsRef<Path>>(self, path: P) -> io::Result<u64> {
io::copy(&mut self.open(), &mut File::create(path)?)
}
pub fn new(mut buf: Vec<u8>,
pos: usize,
mut cap: usize,
mut stream: StreamReader)
-> Data {
// TODO: Make sure we always try to get some number of bytes in the // TODO: Make sure we always try to get some number of bytes in the
// buffer so that peek actually does something. // buffer so that peek actually does something.
// const PEEK_BYTES: usize = 4096;
// Make sure the buffer is large enough for the bytes we want to peek.
const PEEK_BYTES: usize = 4096;
if buf.len() < PEEK_BYTES {
trace!("Resizing peek buffer from {} to {}.", buf.len(), PEEK_BYTES);
buf.resize(PEEK_BYTES, 0);
}
trace!("Init buffer cap: {}", cap);
let buf_len = buf.len();
let eof = match stream.read(&mut buf[cap..(buf_len - 1)]) {
Ok(n) if n == 0 => true,
Ok(n) => {
trace!("Filled peek buf with {} bytes.", n);
cap += n;
match stream.read(&mut buf[cap..(cap + 1)]) {
Ok(n) => {
cap += n;
n == 0
}
Err(e) => {
error_!("Failed to check stream EOF status: {:?}", e);
false
}
}
}
Err(e) => {
error_!("Failed to read into peek buffer: {:?}", e);
false
}
};
trace!("Peek buffer size: {}, remaining: {}", buf_len, buf_len - cap);
Data { Data {
buffer: buf, buffer: buf,
stream: stream, stream: stream,
is_done: eof,
position: pos, position: pos,
capacity: cap, capacity: cap,
} }
} }
} }
impl Drop for Data {
fn drop(&mut self) {
// This is okay since the network stream expects to be shared mutably.
unsafe {
let stream: &mut StreamReader = transmute(self.stream.by_ref());
kill_stream(stream, self.stream.get_mut());
}
}
}
impl FromData for Data {
fn from_data(_: &Request, data: Data) -> DataOutcome<Self, Self::Error> {
DataOutcome::Success(data)
}
}

View File

@ -1,298 +0,0 @@
//! Types and traits to handle form processing.
//!
//! In general, you will deal with forms in Rocket via the `form` parameter in
//! routes:
//!
//! ```rust,ignore
//! #[post("/", form = <my_form>)]
//! fn form_submit(my_form: MyType) -> ...
//! ```
//!
//! Form parameter types must implement the [FromForm](trait.FromForm.html)
//! trait, which is automatically derivable. Automatically deriving `FromForm`
//! for a structure requires that all of its fields implement
//! [FromFormValue](trait.FormFormValue.html). See the
//! [codegen](/rocket_codegen/) documentation or the [forms guide](/guide/forms)
//! for more information on forms and on deriving `FromForm`.
use url;
use error::Error;
use std::str::FromStr;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr};
/// Trait to create instance of some type from an HTTP form; used by code
/// generation for `form` route parameters.
///
/// This trait can be automatically derived via the
/// [rocket_codegen](/rocket_codegen) plugin:
///
/// ```rust,ignore
/// #![feature(plugin, custom_derive)]
/// #![plugin(rocket_codegen)]
///
/// extern crate rocket;
///
/// #[derive(FromForm)]
/// struct TodoTask {
/// description: String,
/// completed: bool
/// }
/// ```
///
/// When deriving `FromForm`, every field in the structure must implement
/// [FromFormValue](trait.FromFormValue.html). If you implement `FormForm`
/// yourself, use the [FormItems](struct.FormItems.html) iterator to iterate
/// through the form key/value pairs.
pub trait FromForm<'f>: Sized {
/// The associated error which can be returned from parsing.
type Error;
/// Parses an instance of `Self` from a raw HTTP form
/// (`application/x-www-form-urlencoded data`) or returns an `Error` if one
/// cannot be parsed.
fn from_form_string(form_string: &'f str) -> Result<Self, Self::Error>;
}
/// This implementation should only be used during debugging!
impl<'f> FromForm<'f> for &'f str {
type Error = Error;
fn from_form_string(s: &'f str) -> Result<Self, Error> {
Ok(s)
}
}
/// Trait to create instance of some type from a form value; expected from field
/// types in structs deriving `FromForm`.
///
/// # Examples
///
/// This trait is generally implemented when verifying form inputs. For example,
/// if you'd like to verify that some user is over some age in a form, then you
/// might define a new type and implement `FromFormValue` as follows:
///
/// ```rust
/// use rocket::request::FromFormValue;
/// use rocket::Error;
///
/// struct AdultAge(usize);
///
/// impl<'v> FromFormValue<'v> for AdultAge {
/// type Error = &'v str;
///
/// fn from_form_value(form_value: &'v str) -> Result<AdultAge, &'v str> {
/// match usize::from_form_value(form_value) {
/// Ok(age) if age >= 21 => Ok(AdultAge(age)),
/// _ => Err(form_value),
/// }
/// }
/// }
/// ```
///
/// This type can then be used in a `FromForm` struct as follows:
///
/// ```rust,ignore
/// #[derive(FromForm)]
/// struct User {
/// age: AdultAge,
/// ...
/// }
/// ```
pub trait FromFormValue<'v>: Sized {
/// The associated error which can be returned from parsing. It is a good
/// idea to have the return type be or contain an `&'v str` so that the
/// unparseable string can be examined after a bad parse.
type Error;
/// Parses an instance of `Self` from an HTTP form field value or returns an
/// `Error` if one cannot be parsed.
fn from_form_value(form_value: &'v str) -> Result<Self, Self::Error>;
/// Returns a default value to be used when the form field does not exist.
/// If this returns None, then the field is required. Otherwise, this should
/// return Some(default_value).
fn default() -> Option<Self> {
None
}
}
impl<'v> FromFormValue<'v> for &'v str {
type Error = Error;
// This just gives the raw string.
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
Ok(v)
}
}
impl<'v> FromFormValue<'v> for String {
type Error = &'v str;
// This actually parses the value according to the standard.
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
let decoder = url::percent_encoding::percent_decode(v.as_bytes());
let res = decoder.decode_utf8().map_err(|_| v).map(|s| s.into_owned());
match res {
e@Err(_) => e,
Ok(mut string) => Ok({
unsafe {
for c in string.as_mut_vec() {
if *c == b'+' {
*c = b' ';
}
}
}
string
})
}
}
}
impl<'v> FromFormValue<'v> for bool {
type Error = &'v str;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
match v {
"on" | "true" => Ok(true),
"off" | "false" => Ok(false),
_ => Err(v),
}
}
}
macro_rules! impl_with_fromstr {
($($T:ident),+) => ($(
impl<'v> FromFormValue<'v> for $T {
type Error = &'v str;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
$T::from_str(v).map_err(|_| v)
}
}
)+)
}
impl_with_fromstr!(f32, f64, isize, i8, i16, i32, i64, usize, u8, u16, u32, u64,
IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr);
impl<'v, T: FromFormValue<'v>> FromFormValue<'v> for Option<T> {
type Error = Error;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
match T::from_form_value(v) {
Ok(v) => Ok(Some(v)),
Err(_) => Ok(None),
}
}
fn default() -> Option<Option<T>> {
Some(None)
}
}
// TODO: Add more useful implementations (range, regex, etc.).
impl<'v, T: FromFormValue<'v>> FromFormValue<'v> for Result<T, T::Error> {
type Error = Error;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
match T::from_form_value(v) {
ok@Ok(_) => Ok(ok),
e@Err(_) => Ok(e),
}
}
}
/// Iterator over the key/value pairs of a given HTTP form string. You'll likely
/// want to use this if you're implementing [FromForm](trait.FromForm.html)
/// manually, for whatever reason, by iterating over the items in `form_string`.
///
/// # Examples
///
/// `FormItems` can be used directly as an iterator:
///
/// ```rust
/// use rocket::request::FormItems;
///
/// // prints "greeting = hello" then "username = jake"
/// let form_string = "greeting=hello&username=jake";
/// for (key, value) in FormItems(form_string) {
/// println!("{} = {}", key, value);
/// }
/// ```
///
/// This is the same example as above, but the iterator is used explicitly.
///
/// ```rust
/// use rocket::request::FormItems;
///
/// let form_string = "greeting=hello&username=jake";
/// let mut items = FormItems(form_string);
/// assert_eq!(items.next(), Some(("greeting", "hello")));
/// assert_eq!(items.next(), Some(("username", "jake")));
/// assert_eq!(items.next(), None);
/// ```
pub struct FormItems<'f>(pub &'f str);
impl<'f> Iterator for FormItems<'f> {
type Item = (&'f str, &'f str);
fn next(&mut self) -> Option<Self::Item> {
let string = self.0;
let (key, rest) = match string.find('=') {
Some(index) => (&string[..index], &string[(index + 1)..]),
None => return None,
};
let (value, remainder) = match rest.find('&') {
Some(index) => (&rest[..index], &rest[(index + 1)..]),
None => (rest, ""),
};
self.0 = remainder;
Some((key, value))
}
}
#[cfg(test)]
mod test {
use super::FormItems;
macro_rules! check_form {
($string:expr, $expected: expr) => ({
let results: Vec<(&str, &str)> = FormItems($string).collect();
assert_eq!($expected.len(), results.len());
for i in 0..results.len() {
let (expected_key, actual_key) = ($expected[i].0, results[i].0);
let (expected_val, actual_val) = ($expected[i].1, results[i].1);
assert!(expected_key == actual_key,
"key [{}] mismatch: expected {}, got {}",
i, expected_key, actual_key);
assert!(expected_val == actual_val,
"val [{}] mismatch: expected {}, got {}",
i, expected_val, actual_val);
}
})
}
#[test]
fn test_form_string() {
check_form!("username=user&password=pass",
&[("username", "user"), ("password", "pass")]);
check_form!("user=user&user=pass",
&[("user", "user"), ("user", "pass")]);
check_form!("user=&password=pass",
&[("user", ""), ("password", "pass")]);
check_form!("=&=", &[("", ""), ("", "")]);
check_form!("a=b", &[("a", "b")]);
check_form!("a=b&a", &[("a", "b")]);
check_form!("a=b&a=", &[("a", "b"), ("a", "")]);
}
}

View File

@ -0,0 +1,95 @@
/// Iterator over the key/value pairs of a given HTTP form string. You'll likely
/// want to use this if you're implementing [FromForm](trait.FromForm.html)
/// manually, for whatever reason, by iterating over the items in `form_string`.
///
/// # Examples
///
/// `FormItems` can be used directly as an iterator:
///
/// ```rust
/// use rocket::request::FormItems;
///
/// // prints "greeting = hello" then "username = jake"
/// let form_string = "greeting=hello&username=jake";
/// for (key, value) in FormItems(form_string) {
/// println!("{} = {}", key, value);
/// }
/// ```
///
/// This is the same example as above, but the iterator is used explicitly.
///
/// ```rust
/// use rocket::request::FormItems;
///
/// let form_string = "greeting=hello&username=jake";
/// let mut items = FormItems(form_string);
/// assert_eq!(items.next(), Some(("greeting", "hello")));
/// assert_eq!(items.next(), Some(("username", "jake")));
/// assert_eq!(items.next(), None);
/// ```
pub struct FormItems<'f>(pub &'f str);
impl<'f> Iterator for FormItems<'f> {
type Item = (&'f str, &'f str);
fn next(&mut self) -> Option<Self::Item> {
let string = self.0;
let (key, rest) = match string.find('=') {
Some(index) => (&string[..index], &string[(index + 1)..]),
None => return None,
};
let (value, remainder) = match rest.find('&') {
Some(index) => (&rest[..index], &rest[(index + 1)..]),
None => (rest, ""),
};
self.0 = remainder;
Some((key, value))
}
}
#[cfg(test)]
mod test {
use super::FormItems;
macro_rules! check_form {
($string:expr, $expected: expr) => ({
let results: Vec<(&str, &str)> = FormItems($string).collect();
assert_eq!($expected.len(), results.len());
for i in 0..results.len() {
let (expected_key, actual_key) = ($expected[i].0, results[i].0);
let (expected_val, actual_val) = ($expected[i].1, results[i].1);
assert!(expected_key == actual_key,
"key [{}] mismatch: expected {}, got {}",
i, expected_key, actual_key);
assert!(expected_val == actual_val,
"val [{}] mismatch: expected {}, got {}",
i, expected_val, actual_val);
}
})
}
#[test]
fn test_form_string() {
check_form!("username=user&password=pass",
&[("username", "user"), ("password", "pass")]);
check_form!("user=user&user=pass",
&[("user", "user"), ("user", "pass")]);
check_form!("user=&password=pass",
&[("user", ""), ("password", "pass")]);
check_form!("=&=", &[("", ""), ("", "")]);
check_form!("a=b", &[("a", "b")]);
check_form!("a=b&a", &[("a", "b")]);
check_form!("a=b&a=", &[("a", "b"), ("a", "")]);
}
}

View File

@ -0,0 +1,43 @@
use error::Error;
/// Trait to create instance of some type from an HTTP form; used by code
/// generation for `form` route parameters.
///
/// This trait can be automatically derived via the
/// [rocket_codegen](/rocket_codegen) plugin:
///
/// ```rust,ignore
/// #![feature(plugin, custom_derive)]
/// #![plugin(rocket_codegen)]
///
/// extern crate rocket;
///
/// #[derive(FromForm)]
/// struct TodoTask {
/// description: String,
/// completed: bool
/// }
/// ```
///
/// When deriving `FromForm`, every field in the structure must implement
/// [FromFormValue](trait.FromFormValue.html). If you implement `FormForm`
/// yourself, use the [FormItems](struct.FormItems.html) iterator to iterate
/// through the form key/value pairs.
pub trait FromForm<'f>: Sized {
/// The associated error which can be returned from parsing.
type Error;
/// Parses an instance of `Self` from a raw HTTP form
/// (`application/x-www-form-urlencoded data`) or returns an `Error` if one
/// cannot be parsed.
fn from_form_string(form_string: &'f str) -> Result<Self, Self::Error>;
}
/// This implementation should only be used during debugging!
impl<'f> FromForm<'f> for &'f str {
type Error = Error;
fn from_form_string(s: &'f str) -> Result<Self, Error> {
Ok(s)
}
}

View File

@ -0,0 +1,146 @@
use url;
use error::Error;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr};
use std::str::FromStr;
/// Trait to create instance of some type from a form value; expected from field
/// types in structs deriving `FromForm`.
///
/// # Examples
///
/// This trait is generally implemented when verifying form inputs. For example,
/// if you'd like to verify that some user is over some age in a form, then you
/// might define a new type and implement `FromFormValue` as follows:
///
/// ```rust
/// use rocket::request::FromFormValue;
/// use rocket::Error;
///
/// struct AdultAge(usize);
///
/// impl<'v> FromFormValue<'v> for AdultAge {
/// type Error = &'v str;
///
/// fn from_form_value(form_value: &'v str) -> Result<AdultAge, &'v str> {
/// match usize::from_form_value(form_value) {
/// Ok(age) if age >= 21 => Ok(AdultAge(age)),
/// _ => Err(form_value),
/// }
/// }
/// }
/// ```
///
/// This type can then be used in a `FromForm` struct as follows:
///
/// ```rust,ignore
/// #[derive(FromForm)]
/// struct User {
/// age: AdultAge,
/// ...
/// }
/// ```
pub trait FromFormValue<'v>: Sized {
/// The associated error which can be returned from parsing. It is a good
/// idea to have the return type be or contain an `&'v str` so that the
/// unparseable string can be examined after a bad parse.
type Error;
/// Parses an instance of `Self` from an HTTP form field value or returns an
/// `Error` if one cannot be parsed.
fn from_form_value(form_value: &'v str) -> Result<Self, Self::Error>;
/// Returns a default value to be used when the form field does not exist.
/// If this returns None, then the field is required. Otherwise, this should
/// return Some(default_value).
fn default() -> Option<Self> {
None
}
}
impl<'v> FromFormValue<'v> for &'v str {
type Error = Error;
// This just gives the raw string.
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
Ok(v)
}
}
impl<'v> FromFormValue<'v> for String {
type Error = &'v str;
// This actually parses the value according to the standard.
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
let decoder = url::percent_encoding::percent_decode(v.as_bytes());
let res = decoder.decode_utf8().map_err(|_| v).map(|s| s.into_owned());
match res {
e@Err(_) => e,
Ok(mut string) => Ok({
unsafe {
for c in string.as_mut_vec() {
if *c == b'+' {
*c = b' ';
}
}
}
string
})
}
}
}
impl<'v> FromFormValue<'v> for bool {
type Error = &'v str;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
match v {
"on" | "true" => Ok(true),
"off" | "false" => Ok(false),
_ => Err(v),
}
}
}
macro_rules! impl_with_fromstr {
($($T:ident),+) => ($(
impl<'v> FromFormValue<'v> for $T {
type Error = &'v str;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
$T::from_str(v).map_err(|_| v)
}
}
)+)
}
impl_with_fromstr!(f32, f64, isize, i8, i16, i32, i64, usize, u8, u16, u32, u64,
IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr);
impl<'v, T: FromFormValue<'v>> FromFormValue<'v> for Option<T> {
type Error = Error;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
match T::from_form_value(v) {
Ok(v) => Ok(Some(v)),
Err(_) => Ok(None),
}
}
fn default() -> Option<Option<T>> {
Some(None)
}
}
// TODO: Add more useful implementations (range, regex, etc.).
impl<'v, T: FromFormValue<'v>> FromFormValue<'v> for Result<T, T::Error> {
type Error = Error;
fn from_form_value(v: &'v str) -> Result<Self, Self::Error> {
match T::from_form_value(v) {
ok@Ok(_) => Ok(ok),
e@Err(_) => Ok(e),
}
}
}

206
lib/src/request/form/mod.rs Normal file
View File

@ -0,0 +1,206 @@
//! Types and traits to handle form processing.
//!
//! In general, you will deal with forms in Rocket via the `form` parameter in
//! routes:
//!
//! ```rust,ignore
//! #[post("/", form = <my_form>)]
//! fn form_submit(my_form: MyType) -> ...
//! ```
//!
//! Form parameter types must implement the [FromForm](trait.FromForm.html)
//! trait, which is automatically derivable. Automatically deriving `FromForm`
//! for a structure requires that all of its fields implement
//! [FromFormValue](trait.FormFormValue.html). See the
//! [codegen](/rocket_codegen/) documentation or the [forms guide](/guide/forms)
//! for more information on forms and on deriving `FromForm`.
mod form_items;
mod from_form;
mod from_form_value;
pub use self::form_items::FormItems;
pub use self::from_form::FromForm;
pub use self::from_form_value::FromFormValue;
use std::marker::PhantomData;
use std::fmt::{self, Debug};
use std::io::Read;
use request::{Request, FromData, Data, DataOutcome};
// This works, and it's safe, but it sucks to have the lifetime appear twice.
pub struct Form<'f, T: FromForm<'f> + 'f> {
object: T,
form_string: String,
_phantom: PhantomData<&'f T>,
}
impl<'f, T: FromForm<'f> + 'f> Form<'f, T> {
pub fn get(&'f self) -> &'f T {
&self.object
}
pub fn get_mut(&'f mut self) -> &'f mut T {
&mut self.object
}
pub fn raw_form_string(&self) -> &str {
&self.form_string
}
// Alright, so here's what's going on here. We'd like to have form
// objects have pointers directly to the form string. This means that
// the form string has to live at least as long as the form object. So,
// to enforce this, we store the form_string along with the form object.
//
// So far so good. Now, this means that the form_string can never be
// deallocated while the object is alive. That implies that the
// `form_string` value should never be moved away. We can enforce that
// easily by 1) not making `form_string` public, and 2) not exposing any
// `&mut self` methods that could modify `form_string`.
//
// Okay, we do all of these things. Now, we still need to give a
// lifetime to `FromForm`. Which one do we choose? The danger is that
// references inside `object` may be copied out, and we have to ensure
// that they don't outlive this structure. So we would really like
// something like `self` and then to transmute to that. But this doesn't
// exist. So we do the next best: we use the first lifetime supplied by the
// caller via `get()` and contrain everything to that lifetime. This is, in
// reality a little coarser than necessary, but the user can simply move the
// call to right after the creation of a Form object to get the same effect.
fn new(form_string: String) -> Result<Self, (String, T::Error)> {
let long_lived_string: &'f str = unsafe {
::std::mem::transmute(form_string.as_str())
};
match T::from_form_string(long_lived_string) {
Ok(obj) => Ok(Form {
form_string: form_string,
object: obj,
_phantom: PhantomData
}),
Err(e) => Err((form_string, e))
}
}
}
impl<'f, T: FromForm<'f> + 'static> Form<'f, T> {
pub fn into_inner(self) -> T {
self.object
}
}
impl<'f, T: FromForm<'f> + Debug + 'f> Debug for Form<'f, T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?} from form string: {:?}", self.object, self.form_string)
}
}
impl<'f, T: FromForm<'f>> FromData for Form<'f, T> where T::Error: Debug {
type Error = Option<String>;
fn from_data(request: &Request, data: Data) -> DataOutcome<Self, Self::Error> {
if !request.content_type().is_form() {
warn_!("Form data does not have form content type.");
return DataOutcome::Forward(data);
}
let mut form_string = String::with_capacity(4096);
let mut stream = data.open().take(32768);
if let Err(e) = stream.read_to_string(&mut form_string) {
error_!("IO Error: {:?}", e);
DataOutcome::Failure(None)
} else {
match Form::new(form_string) {
Ok(form) => DataOutcome::Success(form),
Err((form_string, e)) => {
error_!("Failed to parse value from form: {:?}", e);
DataOutcome::Failure(Some(form_string))
}
}
}
}
}
#[cfg(test)]
mod test {
use super::Form;
use ::request::FromForm;
struct Simple<'s> {
value: &'s str
}
struct Other {
value: String
}
impl<'s> FromForm<'s> for Simple<'s> {
type Error = &'s str;
fn from_form_string(fs: &'s str) -> Result<Simple<'s>, &'s str> {
Ok(Simple { value: fs })
}
}
impl<'s> FromForm<'s> for Other {
type Error = &'s str;
fn from_form_string(fs: &'s str) -> Result<Other, &'s str> {
Ok(Other { value: fs.to_string() })
}
}
#[test]
fn test_lifetime() {
let form_string = "hello=world".to_string();
let form: Form<Simple> = Form::new(form_string).unwrap();
let string: &str = form.get().value;
assert_eq!(string, "hello=world");
}
#[test]
fn test_lifetime_2() {
let form_string = "hello=world".to_string();
let mut _y = "hi";
let _form: Form<Simple> = Form::new(form_string).unwrap();
// _y = form.get().value;
// fn should_not_compile<'f>(form: Form<'f, &'f str>) -> &'f str {
// form.get()
// }
// fn should_not_compile_2<'f>(form: Form<'f, Simple<'f>>) -> &'f str {
// form.into_inner().value
// }
// assert_eq!(should_not_compile(form), "hello=world");
}
#[test]
fn test_lifetime_3() {
let form_string = "hello=world".to_string();
let form: Form<Other> = Form::new(form_string).unwrap();
// Not bad.
fn should_compile(form: Form<Other>) -> String {
form.into_inner().value
}
assert_eq!(should_compile(form), "hello=world".to_string());
}
#[test]
fn test_lifetime_4() {
let form_string = "hello=world".to_string();
let form: Form<Simple> = Form::new(form_string).unwrap();
fn should_compile<'f>(_form: Form<'f, Simple<'f>>) { }
should_compile(form)
// assert_eq!(should_not_compile(form), "hello=world");
}
}

View File

@ -9,5 +9,5 @@ mod from_request;
pub use self::request::Request; pub use self::request::Request;
pub use self::from_request::FromRequest; pub use self::from_request::FromRequest;
pub use self::param::{FromParam, FromSegments}; pub use self::param::{FromParam, FromSegments};
pub use self::form::{FromForm, FromFormValue, FormItems}; pub use self::form::{Form, FromForm, FromFormValue, FormItems};
pub use self::data::{Data}; pub use self::data::{Data, FromData, DataOutcome};

View File

@ -31,7 +31,6 @@ pub struct Request {
/// </div> /// </div>
/// ///
/// The data in the request. /// The data in the request.
pub data: Vec<u8>, // FIXME: Don't read this! (bad Hyper.)
uri: URIBuf, // FIXME: Should be URI (without Hyper). uri: URIBuf, // FIXME: Should be URI (without Hyper).
params: RefCell<Vec<&'static str>>, params: RefCell<Vec<&'static str>>,
cookies: Cookies, cookies: Cookies,
@ -104,7 +103,6 @@ impl Request {
method: method, method: method,
cookies: Cookies::new(&[]), cookies: Cookies::new(&[]),
uri: URIBuf::from(uri), uri: URIBuf::from(uri),
data: vec![],
headers: HyperHeaders::new(), headers: HyperHeaders::new(),
} }
} }
@ -211,7 +209,6 @@ impl Request {
method: method, method: method,
cookies: cookies, cookies: cookies,
uri: uri, uri: uri,
data: vec![], // TODO: Remove me.
headers: h_headers, headers: h_headers,
}; };

View File

@ -1,14 +1,8 @@
use response::{ResponseOutcome, Outcome, Responder}; use response::{ResponseOutcome, Outcome, Responder};
use http::hyper::{FreshHyperResponse, StatusCode}; use http::hyper::{FreshHyperResponse, StatusCode};
pub struct Failure(StatusCode); #[derive(Debug)]
pub struct Failure(pub StatusCode);
impl Failure {
#[inline(always)]
pub fn new(status: StatusCode) -> Failure {
Failure(status)
}
}
impl Responder for Failure { impl Responder for Failure {
fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> ResponseOutcome<'a> { fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> ResponseOutcome<'a> {

View File

@ -44,7 +44,7 @@ impl<'a> Response<'a> {
#[inline(always)] #[inline(always)]
pub fn failed(code: StatusCode) -> Response<'static> { pub fn failed(code: StatusCode) -> Response<'static> {
Response::complete(Failure::new(code)) Response::complete(Failure(code))
} }
#[inline(always)] #[inline(always)]

View File

@ -72,7 +72,7 @@ impl Rocket {
self.preprocess_request(&mut request, &data); self.preprocess_request(&mut request, &data);
info!("{}:", request); info!("{}:", request);
info_!("Peek size: {} bytes", data.peek().len()); trace_!("Peek size: {} bytes", data.peek().len());
let matches = self.router.route(&request); let matches = self.router.route(&request);
for route in matches { for route in matches {
// Retrieve and set the requests parameters. // Retrieve and set the requests parameters.
@ -180,9 +180,9 @@ impl Rocket {
pub fn catch(mut self, catchers: Vec<Catcher>) -> Self { pub fn catch(mut self, catchers: Vec<Catcher>) -> Self {
info!("👾 {}:", Magenta.paint("Catchers")); info!("👾 {}:", Magenta.paint("Catchers"));
for c in catchers { for c in catchers {
if self.catchers.get(&c.code).map_or(false, |e| e.is_default()) { if self.catchers.get(&c.code).map_or(false, |e| !e.is_default()) {
let msg = format!("warning: overrides {} catcher!", c.code); let msg = "(warning: duplicate catcher!)";
warn!("{} ({})", c, Yellow.paint(msg.as_str())); info_!("{} {}", c, Yellow.paint(msg));
} else { } else {
info_!("{}", c); info_!("{}", c);
} }

View File

@ -18,10 +18,10 @@ function build_and_test() {
pushd ${dir} pushd ${dir}
echo ":: Building '${PWD}'..." echo ":: Building '${PWD}'..."
RUST_BACKTRACE=1 cargo build RUST_BACKTRACE=1 cargo build --all-features
echo ":: Running unit tests in '${PWD}'..." echo ":: Running unit tests in '${PWD}'..."
RUST_BACKTRACE=1 cargo test RUST_BACKTRACE=1 cargo test --all-features
popd popd
} }