From 2f35b235149bfea3f02b42e9331fa0498edf60df Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 12 Oct 2016 00:14:42 -0700 Subject: [PATCH] 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. --- Cargo.toml | 1 + codegen/src/decorators/derive_form.rs | 27 +- codegen/src/decorators/route.rs | 53 ++-- codegen/src/parser/route.rs | 26 +- codegen/src/utils/mod.rs | 4 + codegen/tests/compile-fail/ignored_params.rs | 2 +- codegen/tests/run-pass/complete-decorator.rs | 5 +- codegen/tests/run-pass/empty_form.rs | 15 + contrib/Cargo.toml | 6 +- contrib/src/json/mod.rs | 49 +-- examples/cookies/src/main.rs | 7 +- examples/extended_validation/src/files.rs | 2 +- examples/extended_validation/src/main.rs | 30 +- examples/form_kitchen_sink/src/main.rs | 24 +- examples/forms/src/files.rs | 2 +- examples/forms/src/main.rs | 35 +-- examples/json/src/main.rs | 4 +- examples/manual_routes/src/main.rs | 1 + examples/raw_upload/Cargo.toml | 9 + examples/raw_upload/src/main.rs | 33 ++ examples/todo/src/main.rs | 14 +- lib/src/lib.rs | 1 + lib/src/request/data/data_stream.rs | 52 ++++ lib/src/request/data/from_data.rs | 63 ++++ lib/src/request/{data.rs => data/mod.rs} | 146 +++++---- lib/src/request/form.rs | 298 ------------------- lib/src/request/form/form_items.rs | 95 ++++++ lib/src/request/form/from_form.rs | 43 +++ lib/src/request/form/from_form_value.rs | 146 +++++++++ lib/src/request/form/mod.rs | 206 +++++++++++++ lib/src/request/mod.rs | 4 +- lib/src/request/request.rs | 3 - lib/src/response/failure.rs | 10 +- lib/src/response/mod.rs | 2 +- lib/src/rocket.rs | 8 +- scripts/test.sh | 4 +- 36 files changed, 926 insertions(+), 504 deletions(-) create mode 100644 codegen/tests/run-pass/empty_form.rs create mode 100644 examples/raw_upload/Cargo.toml create mode 100644 examples/raw_upload/src/main.rs create mode 100644 lib/src/request/data/data_stream.rs create mode 100644 lib/src/request/data/from_data.rs rename lib/src/request/{data.rs => data/mod.rs} (51%) delete mode 100644 lib/src/request/form.rs create mode 100644 lib/src/request/form/form_items.rs create mode 100644 lib/src/request/form/from_form.rs create mode 100644 lib/src/request/form/from_form_value.rs create mode 100644 lib/src/request/form/mod.rs diff --git a/Cargo.toml b/Cargo.toml index d0ff641f..8e5dbbe3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,4 +26,5 @@ members = [ "examples/form_kitchen_sink", "examples/config", "examples/hello_alt_methods", + "examples/raw_upload", ] diff --git a/codegen/src/decorators/derive_form.rs b/codegen/src/decorators/derive_form.rs index 00182a22..2738a50e 100644 --- a/codegen/src/decorators/derive_form.rs +++ b/codegen/src/decorators/derive_form.rs @@ -1,5 +1,7 @@ #![allow(unused_imports)] // FIXME: Why is this coming from quote_tokens? +use std::mem::transmute; + use syntax::ext::base::{Annotatable, ExtCtxt}; use syntax::print::pprust::{stmt_to_string}; 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::ext::build::AstBuilder; use syntax::ptr::P; -use std::mem::transmute; use syntax_ext::deriving::generic::MethodDef; use syntax_ext::deriving::generic::{StaticStruct, Substructure, TraitDef, ty}; 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 \ structures with named fields."; 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 { // 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] } else { 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()); }; @@ -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) }; - 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); @@ -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 // to Some when a parse completes, or some default value if the parse was // 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, let mut $ident: ::std::option::Option<$ty> = None; ).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 // that each parameter actually is Some() or has a default value. 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. - if i > 0 { - failure_conditions.push(quote_tokens!(cx, ||)); - } + failure_conditions.push(quote_tokens!(cx, ||)); failure_conditions.push(quote_tokens!(cx, 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 // or the default value. 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, $ident: $ident.unwrap_or_else(|| <$ty as ::rocket::request::FromFormValue>::default().unwrap() diff --git a/codegen/src/decorators/route.rs b/codegen/src/decorators/route.rs index 8f2883b0..67844c15 100644 --- a/codegen/src/decorators/route.rs +++ b/codegen/src/decorators/route.rs @@ -52,7 +52,7 @@ trait RouteGenerateExt { fn gen_form(&self, &ExtCtxt, Option<&Spanned>, P) -> Option; fn missing_declared_err(&self, ecx: &ExtCtxt, arg: &Spanned); - fn generate_form_statement(&self, ecx: &ExtCtxt) -> Option; + fn generate_data_statement(&self, ecx: &ExtCtxt) -> Option; fn generate_query_statement(&self, ecx: &ExtCtxt) -> Option; fn generate_param_statements(&self, ecx: &ExtCtxt) -> Vec; fn generate_fn_arguments(&self, ecx: &ExtCtxt) -> Vec; @@ -89,20 +89,31 @@ impl RouteGenerateExt for RouteParams { ).expect("form statement")) } - fn generate_form_statement(&self, ecx: &ExtCtxt) -> Option { - let param = self.form_param.as_ref().map(|p| &p.value); - let expr = quote_expr!(ecx, - match ::std::str::from_utf8(_req.data.as_slice()) { - Ok(s) => s, - Err(_) => { - println!(" => Form is not valid UTF8."); - return ::rocket::Response::failed( - ::rocket::http::StatusCode::BadRequest); - } - } - ); + fn generate_data_statement(&self, ecx: &ExtCtxt) -> Option { + let param = self.data_param.as_ref().map(|p| &p.value); + let arg = param.and_then(|p| self.annotated_fn.find_input(&p.node.name)); + if param.is_none() { + return None; + } else if arg.is_none() { + self.missing_declared_err(ecx, ¶m.unwrap()); + return None; + } - 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 { @@ -148,11 +159,11 @@ impl RouteGenerateExt for RouteParams { ).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| { if let Some(name) = a.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) }) && self.query_param.as_ref().map_or(true, |p| { !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; for arg in all.iter().filter(from_request) { let ident = arg.ident().unwrap().prepend(PARAM_PREFIX); @@ -214,9 +225,9 @@ fn generic_route_decorator(known_method: Option>, let route = RouteParams::from(ecx, sp, known_method, meta_item, annotated); 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 query_statement = route.generate_query_statement(ecx); + let data_statement = route.generate_data_statement(ecx); let fn_arguments = route.generate_fn_arguments(ecx); // Generate and emit the wrapping function with the Rocket handler signature. @@ -225,9 +236,9 @@ fn generic_route_decorator(known_method: Option>, emit_item(push, quote_item!(ecx, fn $route_fn_name<'_b>(_req: &'_b ::rocket::Request, _data: ::rocket::Data) -> ::rocket::Response<'_b> { - $form_statement - $query_statement $param_statements + $query_statement + $data_statement let result = $user_fn_name($fn_arguments); ::rocket::Response::complete(result) } diff --git a/codegen/src/parser/route.rs b/codegen/src/parser/route.rs index afe4e635..9289774a 100644 --- a/codegen/src/parser/route.rs +++ b/codegen/src/parser/route.rs @@ -6,7 +6,7 @@ use syntax::ext::base::{ExtCtxt, Annotatable}; use syntax::codemap::{Span, Spanned, dummy_spanned}; 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::keyvalue::KVSpanned; use rocket::http::{Method, ContentType}; @@ -22,7 +22,7 @@ pub struct RouteParams { pub annotated_fn: Function, pub method: Spanned, pub path: Spanned, - pub form_param: Option>, + pub data_param: Option>, pub query_param: Option>, pub format: Option>, pub rank: Option>, @@ -74,7 +74,7 @@ impl RouteParams { // Parse all of the optional parameters. 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..] { let kv_opt = kv_from_nested(¶m); if kv_opt.is_none() { @@ -85,7 +85,7 @@ impl RouteParams { let kv = kv_opt.unwrap(); match kv.key().as_str() { "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), _ => { let msg = format!("'{}' is not a known parameter", kv.key()); @@ -107,7 +107,7 @@ impl RouteParams { RouteParams { method: method, path: path, - form_param: form, + data_param: data, query_param: query, format: format, rank: rank, @@ -145,7 +145,7 @@ fn param_string_to_ident(ecx: &ExtCtxt, s: Spanned<&str>) -> Option { let string = s.node; if string.starts_with('<') && string.ends_with('>') { 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)); } @@ -217,21 +217,23 @@ fn parse_opt(ecx: &ExtCtxt, kv: &KVSpanned, f: F) -> Option) -> Ident { +fn parse_data(ecx: &ExtCtxt, kv: &KVSpanned) -> Ident { + let mut str_name = "unknown"; 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)) { return ident; } } - let err_string = r#"`form` value must be a parameter, e.g: ""`"#; + let err_string = r#"`data` value must be a parameter, e.g: ""`"#; ecx.struct_span_fatal(kv.span, err_string) - .help(r#"form, if specified, must be a key-value pair where - the key is `form` and the value is a string with a single - parameter inside '<' '>'. e.g: form = """#) + .help(r#"data, if specified, must be a key-value pair where + the key is `data` and the value is a string with a single + parameter inside '<' '>'. e.g: data = """#) .emit(); - str_to_ident("") + str_to_ident(str_name) } fn parse_rank(ecx: &ExtCtxt, kv: &KVSpanned) -> isize { diff --git a/codegen/src/utils/mod.rs b/codegen/src/utils/mod.rs index b89c3bd2..d25f7477 100644 --- a/codegen/src/utils/mod.rs +++ b/codegen/src/utils/mod.rs @@ -87,6 +87,10 @@ impl Folder for TyLifetimeRemover { fn fold_lifetime_defs(&mut self, _: Vec) -> Vec { vec![] } + + fn fold_lifetimes(&mut self, _: Vec) -> Vec { + vec![] + } } pub fn strip_ty_lifetimes(ty: P) -> P { diff --git a/codegen/tests/compile-fail/ignored_params.rs b/codegen/tests/compile-fail/ignored_params.rs index 2bc0a749..8902c897 100644 --- a/codegen/tests/compile-fail/ignored_params.rs +++ b/codegen/tests/compile-fail/ignored_params.rs @@ -7,7 +7,7 @@ fn get(other: &str) -> &'static str { "hi" } //~ ERROR isn't in the function #[get("/a?")] //~ ERROR 'r' is declared fn get1() -> &'static str { "hi" } //~ ERROR isn't in the function -#[get("/a", form = "")] //~ ERROR 'test' is declared +#[get("/a", data = "")] //~ ERROR 'test' is declared fn get2() -> &'static str { "hi" } //~ ERROR isn't in the function fn main() { } diff --git a/codegen/tests/run-pass/complete-decorator.rs b/codegen/tests/run-pass/complete-decorator.rs index 6d086515..a7cf6a86 100644 --- a/codegen/tests/run-pass/complete-decorator.rs +++ b/codegen/tests/run-pass/complete-decorator.rs @@ -4,14 +4,15 @@ extern crate rocket; use rocket::http::Cookies; +use rocket::request::Form; #[derive(FromForm)] struct User { name: String } -#[post("/?", format = "application/json", form = "", rank = 2)] -fn get(name: &str, query: User, user: User, cookies: &Cookies) -> &'static str { "hi" } +#[post("/?", format = "application/json", data = "", rank = 2)] +fn get(name: &str, query: User, user: Form, cookies: &Cookies) -> &'static str { "hi" } fn main() { let _ = routes![get]; diff --git a/codegen/tests/run-pass/empty_form.rs b/codegen/tests/run-pass/empty_form.rs new file mode 100644 index 00000000..5a42c049 --- /dev/null +++ b/codegen/tests/run-pass/empty_form.rs @@ -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 { })); +} diff --git a/contrib/Cargo.toml b/contrib/Cargo.toml index e72334b5..1c974f8f 100644 --- a/contrib/Cargo.toml +++ b/contrib/Cargo.toml @@ -25,8 +25,4 @@ serde_json = { version = "^0.8", optional = true } handlebars = { version = "^0.21", optional = true, features = ["serde_type"] } glob = { version = "^0.2", optional = true } lazy_static = { version = "^0.2", optional = true } - -# Tera dependency -[dependencies.tera] -git = "https://github.com/SergioBenitez/tera" -optional = true +tera = { version = "^0.3", optional = true } diff --git a/contrib/src/json/mod.rs b/contrib/src/json/mod.rs index d9670285..dbab8a77 100644 --- a/contrib/src/json/mod.rs +++ b/contrib/src/json/mod.rs @@ -2,39 +2,39 @@ extern crate serde; extern crate serde_json; 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::http::StatusCode; use rocket::http::hyper::FreshHyperResponse; 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 -/// type allows you to trivially consume and respond with JSON in your Rocket -/// application. +/// The JSON type, which implements `FromData` and `Responder`. This type allows +/// you to trivially consume and respond with JSON in your Rocket application. /// -/// If you're receiving JSON data, simple add a `JSON` type to your function -/// signature where `T` is some type you'd like to parse from JSON. `T` must -/// implement `Deserialize` from [Serde](https://github.com/serde-rs/json). The -/// data is parsed from the HTTP request body. +/// If you're receiving JSON data, simple add a `data` parameter to your route +/// arguments and ensure the type o the parameter is a `JSON`, where `T` is +/// some type you'd like to parse from JSON. `T` must implement `Deserialize` +/// from [Serde](https://github.com/serde-rs/json). The data is parsed from the +/// HTTP request body. /// /// ```rust,ignore -/// #[post("/users/", format = "application/json")] +/// #[post("/users/", format = "application/json", data = "")] /// fn new_user(user: JSON) { /// ... /// } /// ``` /// 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 -/// doesn't specify "application/json" as its first `Accept:` header parameter -/// will not be routed to this handler. +/// doesn't specify "application/json" as its first `Content-Type:` header +/// parameter will not be routed to this handler. /// /// If you're responding with JSON data, return a `JSON` type, where `T` -/// implements implements `Serialize` from -/// [Serde](https://github.com/serde-rs/json). The content type is set to -/// `application/json` automatically. +/// implements `Serialize` from [Serde](https://github.com/serde-rs/json). The +/// content type of the response is set to `application/json` automatically. /// /// ```rust,ignore /// #[get("/users/")] @@ -63,10 +63,21 @@ impl JSON { } } -impl<'r, T: Deserialize> FromRequest<'r> for JSON { - type Error = JSONError; - fn from_request(request: &'r Request) -> Result { - Ok(JSON(serde_json::from_slice(request.data.as_slice())?)) +/// Maximum size of JSON is 1MB. +/// TODO: Determine this size from some configuration parameter. +const MAX_SIZE: u64 = 1048576; + +impl FromData for JSON { + type Error = SerdeError; + + fn from_data(request: &Request, data: Data) -> DataOutcome { + 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))) } } diff --git a/examples/cookies/src/main.rs b/examples/cookies/src/main.rs index 7bb4470e..924e1b73 100644 --- a/examples/cookies/src/main.rs +++ b/examples/cookies/src/main.rs @@ -8,6 +8,7 @@ extern crate rocket; use std::collections::HashMap; +use rocket::request::Form; use rocket::response::Redirect; use rocket::http::{Cookie, Cookies}; use rocket_contrib::Template; @@ -17,9 +18,9 @@ struct Message { message: String } -#[post("/submit", form = "")] -fn submit(cookies: &Cookies, message: Message) -> Redirect { - cookies.add(Cookie::new("message".into(), message.message)); +#[post("/submit", data = "")] +fn submit(cookies: &Cookies, message: Form) -> Redirect { + cookies.add(Cookie::new("message".into(), message.into_inner().message)); Redirect::to("/") } diff --git a/examples/extended_validation/src/files.rs b/examples/extended_validation/src/files.rs index 80e1c752..f7c5efd0 100644 --- a/examples/extended_validation/src/files.rs +++ b/examples/extended_validation/src/files.rs @@ -8,7 +8,7 @@ fn index() -> io::Result { NamedFile::open("static/index.html") } -#[get("/")] +#[get("/", rank = 2)] fn files(file: PathBuf) -> io::Result { NamedFile::open(Path::new("static/").join(file)) } diff --git a/examples/extended_validation/src/main.rs b/examples/extended_validation/src/main.rs index c9d3d192..4614eacc 100644 --- a/examples/extended_validation/src/main.rs +++ b/examples/extended_validation/src/main.rs @@ -6,7 +6,7 @@ extern crate rocket; mod files; use rocket::response::Redirect; -use rocket::request::FromFormValue; +use rocket::request::{Form, FromFormValue}; #[derive(Debug)] struct StrongPassword<'r>(&'r str); @@ -49,24 +49,26 @@ impl<'v> FromFormValue<'v> for AdultAge { } } -#[post("/login", form = "")] -fn login(user: UserLogin) -> Result { - if user.age.is_err() { - return Err(String::from(user.age.unwrap_err())); +#[post("/login", data = "")] +fn login<'a>(user_form: Form<'a, UserLogin<'a>>) -> Result { + let user = user_form.get(); + + if let Err(e) = user.age { + return Err(format!("Age is invalid: {}", e)); } - if user.password.is_err() { - return Err(String::from(user.password.unwrap_err())); + if let Err(e) = user.password { + return Err(format!("Password is invalid: {}", e)); } - match user.username { - "Sergio" => { - match user.password.unwrap().0 { - "password" => Ok(Redirect::other("/user/Sergio")), - _ => Err("Wrong password!".to_string()), - } + if user.username == "Sergio" { + if let Ok(StrongPassword("password")) = user.password { + Ok(Redirect::other("/user/Sergio")) + } else { + Err("Wrong password!".to_string()) } - _ => Err(format!("Unrecognized user, '{}'.", user.username)), + } else { + Err(format!("Unrecognized user, '{}'.", user.username)) } } diff --git a/examples/form_kitchen_sink/src/main.rs b/examples/form_kitchen_sink/src/main.rs index 996846e5..001c137f 100644 --- a/examples/form_kitchen_sink/src/main.rs +++ b/examples/form_kitchen_sink/src/main.rs @@ -3,7 +3,7 @@ extern crate rocket; -use rocket::request::{Request, FromFormValue}; +use rocket::request::{Form, FromFormValue}; use rocket::response::NamedFile; use std::io; @@ -29,24 +29,22 @@ impl<'v> FromFormValue<'v> for FormOption { } #[derive(Debug, FromForm)] -struct FormInput<'r> { +struct FormInput { checkbox: bool, number: usize, radio: FormOption, - password: &'r str, + password: String, textarea: String, select: FormOption, } -#[post("/", form = "")] -fn sink(sink: FormInput) -> String { - format!("{:?}", sink) -} - -#[post("/", rank = 2)] -fn sink2(request: &Request) -> &'static str { - println!("form: {:?}", std::str::from_utf8(request.data.as_slice())); - "Sorry, the form is invalid." +#[post("/", data = "")] +fn sink(sink: Result, Option>) -> String { + match sink { + Ok(form) => format!("{:?}", form.get()), + Err(Some(f)) => format!("Invalid form input: {}", f), + Err(None) => format!("Form input was invalid UTF8."), + } } #[get("/")] @@ -56,6 +54,6 @@ fn index() -> io::Result { fn main() { rocket::ignite() - .mount("/", routes![index, sink, sink2]) + .mount("/", routes![index, sink]) .launch(); } diff --git a/examples/forms/src/files.rs b/examples/forms/src/files.rs index 80e1c752..903760b8 100644 --- a/examples/forms/src/files.rs +++ b/examples/forms/src/files.rs @@ -8,7 +8,7 @@ fn index() -> io::Result { NamedFile::open("static/index.html") } -#[get("/")] +#[get("/", rank = 5)] fn files(file: PathBuf) -> io::Result { NamedFile::open(Path::new("static/").join(file)) } diff --git a/examples/forms/src/main.rs b/examples/forms/src/main.rs index af5e14e4..fbe2519d 100644 --- a/examples/forms/src/main.rs +++ b/examples/forms/src/main.rs @@ -5,37 +5,38 @@ extern crate rocket; mod files; +use rocket::request::Form; use rocket::response::Redirect; #[derive(FromForm)] struct UserLogin<'r> { username: &'r str, password: &'r str, - age: Result, + age: Result, } -#[post("/login", form = "")] -fn login(user: UserLogin) -> Result { - if user.age.is_err() { - let input = user.age.unwrap_err(); - return Err(format!("'{}' is not a valid age integer.", input)); - } +#[post("/login", data = "")] +fn login<'a>(user_form: Form<'a, UserLogin<'a>>) -> Result { + let user = user_form.get(); + match user.age { + 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 age < 20 { - return Err(format!("Sorry, {} is too young!", age)); - } - - match user.username { - "Sergio" => match user.password { + if user.username == "Sergio" { + match user.password { "password" => Ok(Redirect::other("/user/Sergio")), _ => Err("Wrong password!".to_string()) - }, - _ => Err(format!("Unrecognized user, '{}'.", user.username)) + } + } else { + Err(format!("Unrecognized user, '{}'.", user.username)) } } -#[post("/user/")] + +#[get("/user/")] fn user_page(username: &str) -> String { format!("This is {}'s page.", username) } diff --git a/examples/json/src/main.rs b/examples/json/src/main.rs index 9bd829ee..2ffef3b0 100644 --- a/examples/json/src/main.rs +++ b/examples/json/src/main.rs @@ -40,7 +40,7 @@ struct Message { // None // } -#[post("/", format = "application/json")] +#[post("/", format = "application/json", data = "")] fn new(id: ID, message: JSON) -> JSON { let mut hashmap = MAP.lock().unwrap(); if hashmap.contains_key(&id) { @@ -54,7 +54,7 @@ fn new(id: ID, message: JSON) -> JSON { } } -#[put("/", format = "application/json")] +#[put("/", format = "application/json", data = "")] fn update(id: ID, message: JSON) -> Option> { let mut hashmap = MAP.lock().unwrap(); if hashmap.contains_key(&id) { diff --git a/examples/manual_routes/src/main.rs b/examples/manual_routes/src/main.rs index e79be0ea..497d93c0 100644 --- a/examples/manual_routes/src/main.rs +++ b/examples/manual_routes/src/main.rs @@ -27,6 +27,7 @@ fn echo_url<'a>(req: &'a Request, _: Data) -> Response<'a> { fn upload(req: &Request, data: Data) -> Response { if !req.content_type().is_text() { + println!(" => Content-Type of upload must be data. Ignoring."); return Response::failed(StatusCode::BadRequest); } diff --git a/examples/raw_upload/Cargo.toml b/examples/raw_upload/Cargo.toml new file mode 100644 index 00000000..82bb15e9 --- /dev/null +++ b/examples/raw_upload/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "raw_upload" +version = "0.0.1" +authors = ["Sergio Benitez "] +workspace = "../../" + +[dependencies] +rocket = { path = "../../lib" } +rocket_codegen = { path = "../../codegen" } diff --git a/examples/raw_upload/src/main.rs b/examples/raw_upload/src/main.rs new file mode 100644 index 00000000..3363e41e --- /dev/null +++ b/examples/raw_upload/src/main.rs @@ -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 = "")] +fn upload(content_type: ContentType, data: Data) -> Result { + 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(); +} diff --git a/examples/todo/src/main.rs b/examples/todo/src/main.rs index 7d3956ea..f874523f 100644 --- a/examples/todo/src/main.rs +++ b/examples/todo/src/main.rs @@ -11,12 +11,13 @@ extern crate serde_json; mod static_files; mod task; +use rocket::request::Form; use rocket::response::{Flash, Redirect}; use rocket_contrib::Template; use task::Task; #[derive(Debug, Serialize)] -struct Context<'a, 'b>{msg: Option<(&'a str, &'b str)>, tasks: Vec} +struct Context<'a, 'b>{ msg: Option<(&'a str, &'b str)>, tasks: Vec } impl<'a, 'b> Context<'a, 'b> { pub fn err(msg: &'a str) -> Context<'static, 'a> { @@ -28,14 +29,15 @@ impl<'a, 'b> Context<'a, 'b> { } } -#[post("/", form = "")] -fn new(todo: Task) -> Result, Template> { +#[post("/", data = "")] +fn new(todo_form: Form) -> Flash { + let todo = todo_form.into_inner(); 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() { - Ok(Flash::success(Redirect::to("/"), "Todo successfully added.")) + Flash::success(Redirect::to("/"), "Todo successfully added.") } else { - Err(Template::render("index", &Context::err("Whoops! The server failed."))) + Flash::error(Redirect::to("/"), "Whoops! The server failed.") } } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 1f6e8349..83a2c46b 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,6 +1,7 @@ #![feature(question_mark)] #![feature(specialization)] #![feature(conservative_impl_trait)] +#![feature(associated_type_defaults)] //! # Rocket - Core API Documentation //! diff --git a/lib/src/request/data/data_stream.rs b/lib/src/request/data/data_stream.rs new file mode 100644 index 00000000..a04e485a --- /dev/null +++ b/lib/src/request/data/data_stream.rs @@ -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; +pub type InnerStream = Chain>>, BufReader>; + +pub struct DataStream { + pub stream: InnerStream, + pub network: HyperHttpStream, +} + +impl Read for DataStream { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + 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(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); + } +} + diff --git a/lib/src/request/data/from_data.rs b/lib/src/request/data/from_data.rs new file mode 100644 index 00000000..5d643a4b --- /dev/null +++ b/lib/src/request/data/from_data.rs @@ -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; +} + +impl FromData for Result { + fn from_data(request: &Request, data: Data) -> DataOutcome { + 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 FromData for Option { + fn from_data(request: &Request, data: Data) -> DataOutcome { + 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 { + /// 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 From> for DataOutcome { + fn from(result: Result) -> Self { + match result { + Ok(val) => DataOutcome::Success(val), + Err(e) => { + error_!("{:?}", e); + DataOutcome::Failure(e) + } + } + } +} + +impl From> for DataOutcome { + fn from(result: Option) -> Self { + match result { + Some(val) => DataOutcome::Success(val), + None => DataOutcome::Failure(()) + } + } +} diff --git a/lib/src/request/data.rs b/lib/src/request/data/mod.rs similarity index 51% rename from lib/src/request/data.rs rename to lib/src/request/data/mod.rs index 7fd2d9e1..1f0731d0 100644 --- a/lib/src/request/data.rs +++ b/lib/src/request/data/mod.rs @@ -1,71 +1,30 @@ -use std::io::{self, BufRead, Read, Cursor, BufReader, Chain, Take}; -use std::time::Duration; -use std::net::Shutdown; +//! Talk about the data thing. -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::HyperHttpReader::*; -type StreamReader = HyperHttpReader; - -pub struct DataStream { - stream: Chain>>, BufReader>, - network: HyperHttpStream, -} - -impl Read for DataStream { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - 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(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 { buffer: Vec, + is_done: bool, stream: StreamReader, position: usize, capacity: usize, } -impl Drop for Data { - fn drop(&mut self) { - try_sinking(self.stream.get_mut()); - } -} - impl Data { pub fn open(mut self) -> impl BufRead { // 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)) } + #[inline(always)] pub fn peek(&self) -> &[u8] { &self.buffer[self.position..self.capacity] } - pub fn new(buf: Vec, pos: usize, cap: usize, stream: StreamReader) -> Data { + #[inline(always)] + pub fn peek_complete(&self) -> bool { + self.is_done + } + + #[inline(always)] + pub fn stream_to(self, writer: &mut W) -> io::Result { + io::copy(&mut self.open(), writer) + } + + #[inline(always)] + pub fn stream_to_file>(self, path: P) -> io::Result { + io::copy(&mut self.open(), &mut File::create(path)?) + } + + pub fn new(mut buf: Vec, + pos: usize, + mut cap: usize, + mut stream: StreamReader) + -> Data { // TODO: Make sure we always try to get some number of bytes in the // 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 { buffer: buf, stream: stream, + is_done: eof, position: pos, 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 { + DataOutcome::Success(data) + } +} diff --git a/lib/src/request/form.rs b/lib/src/request/form.rs deleted file mode 100644 index 66140bde..00000000 --- a/lib/src/request/form.rs +++ /dev/null @@ -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 = )] -//! 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; -} - -/// 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 { - 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 { -/// 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; - - /// 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 { - 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 { - 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 { - 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 { - 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 { - $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 { - type Error = Error; - - fn from_form_value(v: &'v str) -> Result { - match T::from_form_value(v) { - Ok(v) => Ok(Some(v)), - Err(_) => Ok(None), - } - } - - fn default() -> Option> { - Some(None) - } -} - -// TODO: Add more useful implementations (range, regex, etc.). -impl<'v, T: FromFormValue<'v>> FromFormValue<'v> for Result { - type Error = Error; - - fn from_form_value(v: &'v str) -> Result { - 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 { - 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", "")]); - } -} diff --git a/lib/src/request/form/form_items.rs b/lib/src/request/form/form_items.rs new file mode 100644 index 00000000..4391dbff --- /dev/null +++ b/lib/src/request/form/form_items.rs @@ -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 { + 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", "")]); + } +} diff --git a/lib/src/request/form/from_form.rs b/lib/src/request/form/from_form.rs new file mode 100644 index 00000000..849c2eb7 --- /dev/null +++ b/lib/src/request/form/from_form.rs @@ -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; +} + +/// 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 { + Ok(s) + } +} + diff --git a/lib/src/request/form/from_form_value.rs b/lib/src/request/form/from_form_value.rs new file mode 100644 index 00000000..fe6cbfc2 --- /dev/null +++ b/lib/src/request/form/from_form_value.rs @@ -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 { +/// 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; + + /// 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 { + 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 { + 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 { + 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 { + 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 { + $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 { + type Error = Error; + + fn from_form_value(v: &'v str) -> Result { + match T::from_form_value(v) { + Ok(v) => Ok(Some(v)), + Err(_) => Ok(None), + } + } + + fn default() -> Option> { + Some(None) + } +} + +// TODO: Add more useful implementations (range, regex, etc.). +impl<'v, T: FromFormValue<'v>> FromFormValue<'v> for Result { + type Error = Error; + + fn from_form_value(v: &'v str) -> Result { + match T::from_form_value(v) { + ok@Ok(_) => Ok(ok), + e@Err(_) => Ok(e), + } + } +} + diff --git a/lib/src/request/form/mod.rs b/lib/src/request/form/mod.rs new file mode 100644 index 00000000..9eec3ed2 --- /dev/null +++ b/lib/src/request/form/mod.rs @@ -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 = )] +//! 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 { + 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; + + fn from_data(request: &Request, data: Data) -> DataOutcome { + 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, &'s str> { + Ok(Simple { value: fs }) + } + } + + impl<'s> FromForm<'s> for Other { + type Error = &'s str; + + fn from_form_string(fs: &'s str) -> Result { + Ok(Other { value: fs.to_string() }) + } + } + + #[test] + fn test_lifetime() { + let form_string = "hello=world".to_string(); + let form: Form = 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 = 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 = Form::new(form_string).unwrap(); + + // Not bad. + fn should_compile(form: Form) -> 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 = 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"); + } +} + diff --git a/lib/src/request/mod.rs b/lib/src/request/mod.rs index b8154993..8020add1 100644 --- a/lib/src/request/mod.rs +++ b/lib/src/request/mod.rs @@ -9,5 +9,5 @@ mod from_request; pub use self::request::Request; pub use self::from_request::FromRequest; pub use self::param::{FromParam, FromSegments}; -pub use self::form::{FromForm, FromFormValue, FormItems}; -pub use self::data::{Data}; +pub use self::form::{Form, FromForm, FromFormValue, FormItems}; +pub use self::data::{Data, FromData, DataOutcome}; diff --git a/lib/src/request/request.rs b/lib/src/request/request.rs index 66450b54..eb3f3e38 100644 --- a/lib/src/request/request.rs +++ b/lib/src/request/request.rs @@ -31,7 +31,6 @@ pub struct Request { /// /// /// The data in the request. - pub data: Vec, // FIXME: Don't read this! (bad Hyper.) uri: URIBuf, // FIXME: Should be URI (without Hyper). params: RefCell>, cookies: Cookies, @@ -104,7 +103,6 @@ impl Request { method: method, cookies: Cookies::new(&[]), uri: URIBuf::from(uri), - data: vec![], headers: HyperHeaders::new(), } } @@ -211,7 +209,6 @@ impl Request { method: method, cookies: cookies, uri: uri, - data: vec![], // TODO: Remove me. headers: h_headers, }; diff --git a/lib/src/response/failure.rs b/lib/src/response/failure.rs index d2babd4d..9845936b 100644 --- a/lib/src/response/failure.rs +++ b/lib/src/response/failure.rs @@ -1,14 +1,8 @@ use response::{ResponseOutcome, Outcome, Responder}; use http::hyper::{FreshHyperResponse, StatusCode}; -pub struct Failure(StatusCode); - -impl Failure { - #[inline(always)] - pub fn new(status: StatusCode) -> Failure { - Failure(status) - } -} +#[derive(Debug)] +pub struct Failure(pub StatusCode); impl Responder for Failure { fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> ResponseOutcome<'a> { diff --git a/lib/src/response/mod.rs b/lib/src/response/mod.rs index 5f616862..aaf724fa 100644 --- a/lib/src/response/mod.rs +++ b/lib/src/response/mod.rs @@ -44,7 +44,7 @@ impl<'a> Response<'a> { #[inline(always)] pub fn failed(code: StatusCode) -> Response<'static> { - Response::complete(Failure::new(code)) + Response::complete(Failure(code)) } #[inline(always)] diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index dae242c9..ffa1e3c2 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -72,7 +72,7 @@ impl Rocket { self.preprocess_request(&mut request, &data); info!("{}:", request); - info_!("Peek size: {} bytes", data.peek().len()); + trace_!("Peek size: {} bytes", data.peek().len()); let matches = self.router.route(&request); for route in matches { // Retrieve and set the requests parameters. @@ -180,9 +180,9 @@ impl Rocket { pub fn catch(mut self, catchers: Vec) -> Self { info!("👾 {}:", Magenta.paint("Catchers")); for c in catchers { - if self.catchers.get(&c.code).map_or(false, |e| e.is_default()) { - let msg = format!("warning: overrides {} catcher!", c.code); - warn!("{} ({})", c, Yellow.paint(msg.as_str())); + if self.catchers.get(&c.code).map_or(false, |e| !e.is_default()) { + let msg = "(warning: duplicate catcher!)"; + info_!("{} {}", c, Yellow.paint(msg)); } else { info_!("{}", c); } diff --git a/scripts/test.sh b/scripts/test.sh index e949a9dc..0f4863d6 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -18,10 +18,10 @@ function build_and_test() { pushd ${dir} echo ":: Building '${PWD}'..." - RUST_BACKTRACE=1 cargo build + RUST_BACKTRACE=1 cargo build --all-features echo ":: Running unit tests in '${PWD}'..." - RUST_BACKTRACE=1 cargo test + RUST_BACKTRACE=1 cargo test --all-features popd }