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/config",
"examples/hello_alt_methods",
"examples/raw_upload",
]

View File

@ -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<Expr> {
// 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,
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()

View File

@ -52,7 +52,7 @@ trait RouteGenerateExt {
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 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_param_statements(&self, ecx: &ExtCtxt) -> Vec<Stmt>;
fn generate_fn_arguments(&self, ecx: &ExtCtxt) -> Vec<TokenTree>;
@ -89,20 +89,31 @@ impl RouteGenerateExt for RouteParams {
).expect("form statement"))
}
fn generate_form_statement(&self, ecx: &ExtCtxt) -> Option<Stmt> {
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<Stmt> {
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, &param.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<Stmt> {
@ -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<Spanned<Method>>,
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<Spanned<Method>>,
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)
}

View File

@ -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<Method>,
pub path: Spanned<String>,
pub form_param: Option<KVSpanned<Ident>>,
pub data_param: Option<KVSpanned<Ident>>,
pub query_param: Option<Spanned<Ident>>,
pub format: Option<KVSpanned<ContentType>>,
pub rank: Option<KVSpanned<isize>>,
@ -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(&param);
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<Ident> {
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<O, T, F>(ecx: &ExtCtxt, kv: &KVSpanned<T>, f: F) -> Option<KVSpanne
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() {
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: "<name>"`"#;
let err_string = r#"`data` value must be a parameter, e.g: "<name>"`"#;
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 = "<login>""#)
.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 = "<user_form>""#)
.emit();
str_to_ident("")
str_to_ident(str_name)
}
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> {
vec![]
}
fn fold_lifetimes(&mut self, _: Vec<Lifetime>) -> Vec<Lifetime> {
vec![]
}
}
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
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 main() { }

View File

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

View File

@ -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<T>` 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<T>`, 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 = "<user>")]
/// fn new_user(user: JSON<User>) {
/// ...
/// }
/// ```
/// 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<T>` 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/<id>")]
@ -63,10 +63,21 @@ impl<T> JSON<T> {
}
}
impl<'r, T: Deserialize> FromRequest<'r> for JSON<T> {
type Error = JSONError;
fn from_request(request: &'r Request) -> Result<Self, Self::Error> {
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<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 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 = "<message>")]
fn submit(cookies: &Cookies, message: Message) -> Redirect {
cookies.add(Cookie::new("message".into(), message.message));
#[post("/submit", data = "<message>")]
fn submit(cookies: &Cookies, message: Form<Message>) -> Redirect {
cookies.add(Cookie::new("message".into(), message.into_inner().message));
Redirect::to("/")
}

View File

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

View File

@ -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 = "<user>")]
fn login(user: UserLogin) -> Result<Redirect, String> {
if user.age.is_err() {
return Err(String::from(user.age.unwrap_err()));
#[post("/login", data = "<user_form>")]
fn login<'a>(user_form: Form<'a, UserLogin<'a>>) -> Result<Redirect, String> {
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))
}
}

View File

@ -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 = "<sink>")]
fn sink(sink: FormInput) -> String {
format!("{:?}", sink)
#[post("/", data = "<sink>")]
fn sink(sink: Result<Form<FormInput>, Option<String>>) -> String {
match sink {
Ok(form) => format!("{:?}", form.get()),
Err(Some(f)) => format!("Invalid form input: {}", f),
Err(None) => format!("Form input was invalid UTF8."),
}
#[post("/", rank = 2)]
fn sink2(request: &Request) -> &'static str {
println!("form: {:?}", std::str::from_utf8(request.data.as_slice()));
"Sorry, the form is invalid."
}
#[get("/")]
@ -56,6 +54,6 @@ fn index() -> io::Result<NamedFile> {
fn main() {
rocket::ignite()
.mount("/", routes![index, sink, sink2])
.mount("/", routes![index, sink])
.launch();
}

View File

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

View File

@ -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<isize, &'r str>,
age: Result<usize, &'r str>,
}
#[post("/login", form = "<user>")]
fn login(user: UserLogin) -> Result<Redirect, String> {
if user.age.is_err() {
let input = user.age.unwrap_err();
return Err(format!("'{}' is not a valid age integer.", input));
}
#[post("/login", data = "<user_form>")]
fn login<'a>(user_form: Form<'a, UserLogin<'a>>) -> Result<Redirect, String> {
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/<username>")]
#[get("/user/<username>")]
fn user_page(username: &str) -> String {
format!("This is {}'s page.", username)
}

View File

@ -40,7 +40,7 @@ struct Message {
// None
// }
#[post("/<id>", format = "application/json")]
#[post("/<id>", format = "application/json", data = "<message>")]
fn new(id: ID, message: JSON<Message>) -> JSON<SimpleMap> {
let mut hashmap = MAP.lock().unwrap();
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>> {
let mut hashmap = MAP.lock().unwrap();
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 {
if !req.content_type().is_text() {
println!(" => Content-Type of upload must be data. Ignoring.");
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,6 +11,7 @@ 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;
@ -28,14 +29,15 @@ impl<'a, 'b> Context<'a, 'b> {
}
}
#[post("/", form = "<todo>")]
fn new(todo: Task) -> Result<Flash<Redirect>, Template> {
#[post("/", data = "<todo_form>")]
fn new(todo_form: Form<Task>) -> Flash<Redirect> {
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.")
}
}

View File

@ -1,6 +1,7 @@
#![feature(question_mark)]
#![feature(specialization)]
#![feature(conservative_impl_trait)]
#![feature(associated_type_defaults)]
//! # 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};
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<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 {
buffer: Vec<u8>,
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<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
// 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<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::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};

View File

@ -31,7 +31,6 @@ pub struct Request {
/// </div>
///
/// The data in the request.
pub data: Vec<u8>, // FIXME: Don't read this! (bad Hyper.)
uri: URIBuf, // FIXME: Should be URI (without Hyper).
params: RefCell<Vec<&'static str>>,
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,
};

View File

@ -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> {

View File

@ -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)]

View File

@ -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<Catcher>) -> 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);
}

View File

@ -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
}