From d477c180628fed8942fc0fd782083287a334db8b Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 30 Mar 2016 01:02:21 -0700 Subject: [PATCH] Major progress towards form support. --- examples/forms/src/files.rs | 13 ++ examples/forms/src/main.rs | 54 +++-- examples/optional_result/src/main.rs | 2 +- lib/src/lib.rs | 7 +- macros/src/route_decorator.rs | 284 ++++++++++++++++++++------- macros/src/utils.rs | 72 ++++++- 6 files changed, 337 insertions(+), 95 deletions(-) create mode 100644 examples/forms/src/files.rs diff --git a/examples/forms/src/files.rs b/examples/forms/src/files.rs new file mode 100644 index 00000000..63c0c3d9 --- /dev/null +++ b/examples/forms/src/files.rs @@ -0,0 +1,13 @@ +use rocket; +use std::fs::File; +use std::io::Error as IOError; + +#[route(GET, path = "/")] +pub fn index() -> File { + File::open("static/index.html").unwrap() +} + +#[route(GET, path = "/")] +pub fn files(file: &str) -> Result { + File::open(format!("static/{}", file)) +} diff --git a/examples/forms/src/main.rs b/examples/forms/src/main.rs index 5e71405c..2dc0f610 100644 --- a/examples/forms/src/main.rs +++ b/examples/forms/src/main.rs @@ -3,37 +3,51 @@ extern crate rocket; +mod files; + use rocket::Rocket; -use std::fs::File; -use std::io::Error as IOError; use rocket::response::Redirect; - -#[route(GET, path = "/")] -fn index() -> File { - File::open("static/index.html").unwrap() -} - -#[route(GET, path = "/")] -fn files(file: &str) -> Result { - File::open(format!("static/{}", file)) -} +use rocket::error::Error; #[route(GET, path = "/user/")] fn user_page(username: &str) -> String { format!("This is {}'s page.", username) } +// #[derive(FormItem)] // FIXME: Make that happen. +struct UserLogin<'a> { + username: &'a str, + password: &'a str +} + +trait FormItem: Sized { + fn from_form_string(s: &str) -> Result; +} + +impl<'a> FormItem for UserLogin<'a> { + fn from_form_string(s: &str) -> Result { + Ok(UserLogin { + username: "this", + password: "that" + }) + } +} + // TODO: Actually look at form parameters. -#[route(POST, path = "/login")] -fn login() -> Result { - if true { - Ok(Redirect::other("/user/some_name")) - } else { - Err("Sorry, the username and password are invalid.") +// FIXME: fn login<'a>(user: UserLogin<'a>) +#[route(POST, path = "/login", form = "")] +fn login(user: UserLogin) -> Result { + match user.username { + "Sergio" => match user.password { + "password" => Ok(Redirect::other("/user/some_name")), + _ => Err("Wrong password!".to_string()) + }, + _ => Err(format!("Unrecognized user, '{}'.", user.username)) } } fn main() { - let rocket = Rocket::new("localhost", 8000); - rocket.mount_and_launch("/", routes![index, files, user_page, login]); + let mut rocket = Rocket::new("localhost", 8000); + rocket.mount("/", routes![files::index, files::files, user_page, login]); + rocket.launch(); } diff --git a/examples/optional_result/src/main.rs b/examples/optional_result/src/main.rs index 2b9ae840..bbd0bd05 100644 --- a/examples/optional_result/src/main.rs +++ b/examples/optional_result/src/main.rs @@ -5,7 +5,7 @@ extern crate rocket; use rocket::Rocket; #[route(GET, path = "/users/")] -fn user(name: &str) -> Option<&'static str> { +fn user(name: &str, other: i8) -> Option<&'static str> { if name == "Sergio" { Some("Hello, Sergio!") } else { diff --git a/lib/src/lib.rs b/lib/src/lib.rs index d7445779..6ebd739c 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -19,6 +19,7 @@ pub use router::Router; pub use response::{Response, HypResponse, Responder, HypFresh}; use std::fmt; +use std::io::Read; use term_painter::ToStyle; use term_painter::Color::*; use hyper::uri::RequestUri; @@ -60,12 +61,16 @@ pub struct Rocket { } impl HypHandler for Rocket { - fn handle<'a, 'k>(&'a self, req: HypRequest<'a, 'k>, + fn handle<'a, 'k>(&'a self, mut req: HypRequest<'a, 'k>, res: HypResponse<'a, HypFresh>) { println!("{} {:?} {:?}", White.paint("Incoming:"), Green.paint(&req.method), Blue.paint(&req.uri)); + let mut buf = vec![]; + req.read_to_end(&mut buf); // FIXME: Simple DOS attack here. + if let RequestUri::AbsolutePath(uri_string) = req.uri { if let Some(method) = Method::from_hyp(req.method) { + let uri_str = uri_string.as_str(); let route = self.router.route(method, uri_str); let mut response = route.map_or(Response::not_found(), |route| { diff --git a/macros/src/route_decorator.rs b/macros/src/route_decorator.rs index 81a2fd70..4253fc62 100644 --- a/macros/src/route_decorator.rs +++ b/macros/src/route_decorator.rs @@ -1,13 +1,13 @@ use super::{STRUCT_PREFIX, FN_PREFIX}; -use utils::{prepend_ident, get_key_values}; +use utils::*; use std::str::FromStr; use std::collections::HashSet; use syntax::ext::quote::rt::ToTokens; -use syntax::codemap::{Span, DUMMY_SP}; -use syntax::ast::{Ident, TokenTree, PatKind}; -use syntax::ast::{Item, Expr, ItemKind, MetaItem, MetaItemKind, FnDecl}; +use syntax::codemap::{Span, BytePos, DUMMY_SP, Spanned}; +use syntax::ast::{self, Ident, TokenTree, PatKind, Stmt}; +use syntax::ast::{Item, Expr, ItemKind, MetaItem, MetaItemKind, FnDecl, Ty}; use syntax::ext::base::{Annotatable, ExtCtxt}; use syntax::ptr::P; use syntax::ext::build::AstBuilder; @@ -20,8 +20,9 @@ use rocket::Method; const DEBUG: bool = true; struct Params { - method: Method, - path: String + method: Spanned, + path: KVSpanned, + form: Option>, } fn bad_item_fatal(ecx: &mut ExtCtxt, dec_sp: Span, i_sp: Span) -> ! { @@ -37,7 +38,7 @@ fn bad_method_err(ecx: &mut ExtCtxt, dec_sp: Span, message: &str) -> Method { } fn get_fn_decl<'a>(ecx: &mut ExtCtxt, sp: Span, annotated: &'a Annotatable) - -> (&'a P, &'a P) { + -> (&'a P, Spanned<&'a FnDecl>) { // `annotated` is the AST object for the annotated item. let item: &P = match annotated { &Annotatable::Item(ref item) => item, @@ -46,11 +47,11 @@ fn get_fn_decl<'a>(ecx: &mut ExtCtxt, sp: Span, annotated: &'a Annotatable) }; let fn_decl: &P = match item.node { - ItemKind::Fn(ref decl, _, _, _, _, _) => decl, - _ => bad_item_fatal(ecx, sp, item.span) - }; + ItemKind::Fn(ref decl, _, _, _, _, _) => decl, + _ => bad_item_fatal(ecx, sp, item.span) + }; - (item, fn_decl) + (item, wrap_span(&*fn_decl, item.span)) } fn get_route_params(ecx: &mut ExtCtxt, meta_item: &MetaItem) -> Params { @@ -67,32 +68,59 @@ fn get_route_params(ecx: &mut ExtCtxt, meta_item: &MetaItem) -> Params { ecx.span_fatal(meta_item.span, "At least 2 arguments are required."); } - // Get the method and the rest of the k = v params. Ensure method parameter - // is valid. If it's not, issue an error but use "GET" to continue parsing. + // Get the method and the rest of the k = v params. let (method_param, kv_params) = params.split_first().unwrap(); + + // Ensure method parameter is valid. If it's not, issue an error but use + // "GET" to continue parsing. method :: Spanned. let method = if let MetaItemKind::Word(ref word) = method_param.node { - Method::from_str(word).unwrap_or_else(|_| { + let method = Method::from_str(word).unwrap_or_else(|_| { let message = format!("{} is not a valid method.", word); bad_method_err(ecx, method_param.span, message.as_str()) - }) + }); + + Spanned { span: method_param.span, node: method } } else { - bad_method_err(ecx, method_param.span, "Invalid parameter. Expected a - valid HTTP method at this position.") + let method = bad_method_err(ecx, method_param.span, "Invalid parameter. \ + Expected a valid HTTP method at this position."); + dummy_span(method) }; // Now grab all of the required and optional parameters. let req: [&'static str; 1] = ["path"]; - let opt: [&'static str; 0] = []; + let opt: [&'static str; 1] = ["form"]; let kv_pairs = get_key_values(ecx, meta_item.span, &req, &opt, kv_params); // Ensure we have a path, just to keep parsing and generating errors. - let path = kv_pairs.get("path").map_or(String::from("/"), |s| { - String::from(*s) + let path = kv_pairs.get("path").map_or(dummy_kvspan("/".to_string()), |s| { + s.clone().map(|str_string| String::from(str_string)) + }); + + // If there's a form parameter, ensure method is POST. + let form = kv_pairs.get("form").map_or(None, |f| { + if method.node != Method::Post { + ecx.span_err(f.p_span, "Use of `form` requires a POST method..."); + let message = format!("...but {} was found instead.", method.node); + ecx.span_err(method_param.span, message.as_str()); + } + + if !(f.node.starts_with('<') && f.node.ends_with('>')) { + ecx.struct_span_err(f.p_span, "`form` cannot contain arbitrary text") + .help("`form` must be exactly one parameter: \"\"") + .emit(); + } + + if f.node.chars().filter(|c| *c == '<').count() != 1 { + ecx.span_err(f.p_span, "`form` must contain exactly one parameter"); + } + + Some(f.clone().map(|str_string| String::from(str_string))) }); Params { method: method, - path: path + path: path, + form: form } } @@ -115,18 +143,15 @@ fn method_variant_to_expr(ecx: &ExtCtxt, method: Method) -> P { } } -pub fn get_fn_params(ecx: &ExtCtxt, sp: Span, path: &str, - fn_decl: &FnDecl) -> Vec { - debug!("FUNCTION: {:?}", fn_decl); - +// TODO: Put something like this in the library. Maybe as an iterator? +pub fn gen_params_hashset<'a>(ecx: &ExtCtxt, params: &Spanned<&'a str>) + -> HashSet<&'a str> { let mut seen = HashSet::new(); - let bad_match_err = "Path string is malformed."; - let mut matching = false; + let bad_match_err = "Parameter string is malformed."; - // Collect all of the params in the path and insert into HashSet. - // TODO: Move this logic into main library. let mut start = 0; - for (i, c) in path.char_indices() { + let mut matching = false; + for (i, c) in params.node.char_indices() { match c { '<' if !matching => { matching = true; @@ -134,75 +159,177 @@ pub fn get_fn_params(ecx: &ExtCtxt, sp: Span, path: &str, }, '>' if matching => { matching = false; - if start + 1 < i { - let param_name = &path[(start + 1)..i]; + + let mut param_span = params.span.clone(); + param_span.lo = params.span.lo + BytePos(start as u32); + param_span.hi = params.span.lo + BytePos((i + 1) as u32); + + if i > start + 1 { + let param_name = ¶ms.node[(start + 1)..i]; + if seen.contains(param_name) { + let msg = format!("\"{}\" appears more than once in \ + the parameter string.", param_name); + ecx.span_err(param_span, msg.as_str()); + } + seen.insert(param_name); } else { - ecx.span_err(sp, "Parameter cannot be empty."); + ecx.span_err(param_span, "Parameter names cannot be empty."); } }, - '<' if matching => ecx.span_err(sp, bad_match_err), - '>' if !matching => ecx.span_err(sp, bad_match_err), + '<' if matching => ecx.span_err(params.span, bad_match_err), + '>' if !matching => ecx.span_err(params.span, bad_match_err), _ => { /* ... */ } } } - // TODO: This should stay here, though. + seen +} + +#[derive(Debug)] +struct SimpleArg { + name: String, + ty: P, + span: Span +} + +pub fn gen_kv_string_hashset<'a>(ecx: &ExtCtxt, params: &'a KVSpanned) + -> HashSet<&'a str> { + let mut param_span = params.v_span.clone(); + param_span.lo = params.v_span.lo + BytePos(1); + let params = Spanned { + span: param_span, + node: &*params.node + }; + + gen_params_hashset(ecx, ¶ms) +} + +impl SimpleArg { + fn new(name: T, ty: P, sp: Span) -> SimpleArg { + SimpleArg { name: name.to_string(), ty: ty, span: sp } + } + + fn as_str(&self) -> &str { + self.name.as_str() + } +} + +fn get_fn_params<'a>(ecx: &ExtCtxt, dec_span: Span, path: &'a KVSpanned, + fn_decl: &Spanned<&FnDecl>, mut external: HashSet<&'a str>) + -> Vec { + debug!("FUNCTION: {:?}", fn_decl); + let mut path_params = gen_kv_string_hashset(ecx, &path); + + // Ensure that there are no collisions between path parameters and external + // params. If there are, get rid of one of them so we don't double error. + let new_external = external.clone(); + for param in path_params.intersection(&new_external) { + let msg = format!("'{}' appears as a parameter more than once.", param); + external.remove(param); + ecx.span_err(dec_span, msg.as_str()); + } + // Ensure every param in the function declaration is in `path`. Also add // each param name in the declaration to the result vector. let mut result = vec![]; - for arg in &fn_decl.inputs { + for arg in &fn_decl.node.inputs { let ident: &Ident = match arg.pat.node { PatKind::Ident(_, ref ident, _) => &ident.node, _ => { - ecx.span_err(sp, "Expected an identifier."); // FIXME: fn span. + ecx.span_err(arg.pat.span, "Expected an identifier."); return result } }; let name = ident.to_string(); - if !seen.remove(name.as_str()) { - let msg = format!("'{}' appears in the function declaration but \ - not in the path string.", name); - ecx.span_err(sp, msg.as_str()); + if !path_params.remove(name.as_str()) && !external.remove(name.as_str()) { + let msg1 = format!("'{}' appears in the function declaration...", name); + let msg2 = format!("...but does not appear as a parameter \ + (e.g., <{}>).", name); + ecx.span_err(arg.pat.span, msg1.as_str()); + ecx.span_err(dec_span, msg2.as_str()); } - result.push(name); + result.push(SimpleArg::new(name, arg.ty.clone(), arg.pat.span.clone())); } - // Ensure every param in `path` is in the function declaration. - for item in seen { - let msg = format!("'{}' appears in the path string but not in the \ - function declaration.", item); - ecx.span_err(sp, msg.as_str()); + // Ensure every param in `path` and `exclude` is in the function declaration. + for item in path_params { + let msg = format!("'{}' appears in the path string...", item); + ecx.span_err(path.v_span, msg.as_str()); + ecx.span_err(fn_decl.span, "...but does not appear in the function \ + declration."); + } + + // FIXME: need the spans for the external params + for item in external { + let msg = format!("'{}' appears as a parameter...", item); + ecx.span_err(dec_span, msg.as_str()); + ecx.span_err(fn_decl.span, "...but does not appear in the function \ + declaration."); } result } +fn get_form_stmt(ecx: &ExtCtxt, fn_args: &mut Vec, + form_params: &HashSet<&str>) -> Option { + if form_params.len() < 1 { + return None + } else if form_params.len() > 1 { + panic!("Allowed more than 1 form parameter!"); + } + + + let param_ty; + let param_ident; + let param_name = form_params.iter().next().unwrap(); + + { + // Get the first item in the hashset, i.e., the form params variable name. + let fn_arg = fn_args.iter().filter(|a| &&*a.name == param_name).next(); + if fn_arg.is_none() { + // This happens when a form parameter doesn't appear in the function. + return None; + } + + param_ty = fn_arg.unwrap().ty.clone(); + param_ident = str_to_ident(param_name); + } + + debug!("Form parameter variable: {}: {:?}", param_name, param_ty); + let fn_arg_index = fn_args.iter().position(|a| &&*a.name == param_name).unwrap(); + fn_args.remove(fn_arg_index); + quote_stmt!(ecx, + // TODO: Actually get the form parameters to pass into from_form_string. + // Alternatively, pass in some already parsed thing. + let $param_ident: $param_ty = match FormItem::from_form_string("test string") { + Ok(v) => v, + Err(_) => return rocket::Response::not_found() + }; + ) +} + +// FIXME: Compilation fails when parameters have the same name as the function! pub fn route_decorator(ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem, annotated: &Annotatable, push: &mut FnMut(Annotatable)) { let (item, fn_decl) = get_fn_decl(ecx, sp, annotated); let route_params = get_route_params(ecx, meta_item); - let fn_params = get_fn_params(ecx, sp, &route_params.path, &fn_decl); - debug!("Path: {:?}", route_params.path); - debug!("Function Declaration: {:?}", fn_decl); - - let mut fn_param_exprs = vec![]; - for (i, param) in fn_params.iter().enumerate() { - let param_ident = str_to_ident(param.as_str()); - let param_fn_item = quote_stmt!(ecx, - let $param_ident = match _req.get_param($i) { - Ok(v) => v, - Err(_) => return rocket::Response::not_found() - }; - ).unwrap(); - - debug!("Param FN: {:?}", stmt_to_string(¶m_fn_item)); - fn_param_exprs.push(param_fn_item); + // TODO: move this elsewhere + let mut external_params = HashSet::new(); + let mut form_param_hashset = HashSet::new(); + if let Some(ref form) = route_params.form { + form_param_hashset = gen_kv_string_hashset(ecx, form); + external_params.extend(&form_param_hashset); } + let mut fn_params = get_fn_params(ecx, sp, &route_params.path, &fn_decl, + external_params.clone()); + + // Create a comma seperated list (token tree) of the function parameters + // We pass this in to the user's function that we're wrapping. let mut fn_param_idents: Vec = vec![]; for i in 0..fn_params.len() { let tokens = str_to_ident(fn_params[i].as_str()).to_tokens(ecx); @@ -212,11 +339,34 @@ pub fn route_decorator(ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem, } } + // Generate the statements that will attempt to parse forms during run-time. + // let form_span = route_params.form.map_or(DUMMY_SP, |f| f.span.clone()); + let form_stmt = get_form_stmt(ecx, &mut fn_params, &mut form_param_hashset); + form_stmt.as_ref().map(|s| debug!("Form stmt: {:?}", stmt_to_string(s))); + + // Generate the statements that will attempt to parse the paramaters during + // run-time. + let mut fn_param_exprs = vec![]; + for (i, param) in fn_params.iter().enumerate() { + let param_ident = str_to_ident(param.as_str()); + let param_ty = ¶m.ty; + let param_fn_item = quote_stmt!(ecx, + let $param_ident: $param_ty = match _req.get_param($i) { + Ok(v) => v, + Err(_) => return rocket::Response::not_found() + }; + ).unwrap(); + + debug!("Param FN: {:?}", stmt_to_string(¶m_fn_item)); + fn_param_exprs.push(param_fn_item); + } + debug!("Final Params: {:?}", fn_params); let route_fn_name = prepend_ident(FN_PREFIX, &item.ident); let fn_name = item.ident; let route_fn_item = quote_item!(ecx, - fn $route_fn_name<'a>(_req: rocket::Request) -> rocket::Response<'a> { + fn $route_fn_name<'rocket>(_req: rocket::Request) -> rocket::Response<'rocket> { + $form_stmt $fn_param_exprs let result = $fn_name($fn_param_idents); rocket::Response::new(result) @@ -227,8 +377,8 @@ pub fn route_decorator(ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem, push(Annotatable::Item(route_fn_item)); let struct_name = prepend_ident(STRUCT_PREFIX, &item.ident); - let path = route_params.path; - let method = method_variant_to_expr(ecx, route_params.method); + let path = route_params.path.node; + let method = method_variant_to_expr(ecx, route_params.method.node); push(Annotatable::Item(quote_item!(ecx, #[allow(non_upper_case_globals)] pub static $struct_name: rocket::Route = rocket::Route { diff --git a/macros/src/utils.rs b/macros/src/utils.rs index 3a1ac126..73785f5f 100644 --- a/macros/src/utils.rs +++ b/macros/src/utils.rs @@ -1,7 +1,8 @@ use syntax::parse::{token}; -use syntax::ast::{Ident, MetaItem, MetaItemKind, LitKind}; +use syntax::ast::{Ident, MetaItem, MetaItemKind, LitKind, TokenTree}; use syntax::ext::base::{ExtCtxt}; -use syntax::codemap::Span; +use syntax::codemap::{Span, Spanned, BytePos, DUMMY_SP}; +use syntax::ext::quote::rt::ToTokens; use syntax::ptr::P; use std::collections::{HashSet, HashMap}; @@ -20,6 +21,7 @@ macro_rules! debug { if DEBUG { println!("{}:{}", file!(), line!()); println!($($message)*); + println!(""); } }) } @@ -37,9 +39,60 @@ pub fn append_ident(ident: &Ident, other: T) -> Ident { token::str_to_ident(new_ident.as_str()) } +#[inline] +pub fn wrap_span(t: T, span: Span) -> Spanned { + Spanned { + span: span, + node: t, + } +} + +#[inline] +pub fn dummy_span(t: T) -> Spanned { + Spanned { + span: DUMMY_SP, + node: t, + } +} + +#[inline] +pub fn dummy_kvspan(t: T) -> KVSpanned { + KVSpanned { + k_span: DUMMY_SP, + v_span: DUMMY_SP, + p_span: DUMMY_SP, + node: t, + } +} + +#[derive(Debug, Clone)] +pub struct KVSpanned { + pub k_span: Span, + pub v_span: Span, + pub p_span: Span, + pub node: T +} + +impl ToTokens for KVSpanned { + fn to_tokens(&self, cx: &ExtCtxt) -> Vec { + self.node.to_tokens(cx) + } +} + +impl KVSpanned { + pub fn map U>(self, f: F) -> KVSpanned { + KVSpanned { + k_span: self.k_span, + v_span: self.v_span, + p_span: self.p_span, + node: f(self.node), + } + } +} + pub fn get_key_values<'b>(ecx: &mut ExtCtxt, sp: Span, required: &[&str], optional: &[&str], kv_params: &'b [P]) - -> HashMap<&'b str, &'b str> { + -> HashMap<&'b str, KVSpanned<&'b str>> { let mut seen = HashSet::new(); let mut kv_pairs = HashMap::new(); @@ -48,16 +101,23 @@ pub fn get_key_values<'b>(ecx: &mut ExtCtxt, sp: Span, required: &[&str], if let MetaItemKind::NameValue(ref name, ref value) = param.node { if required.contains(&&**name) || optional.contains(&&**name) { if seen.contains(&**name) { - let msg = format!("'{}' cannot be set twice.", &**name); + let msg = format!("'{}' parameter appears twice.", &**name); ecx.span_err(param.span, &msg); continue; } seen.insert(&**name); if let LitKind::Str(ref string, _) = value.node { - kv_pairs.insert(&**name, &**string); + let mut k_span = param.span; + k_span.hi = k_span.lo + BytePos(name.len() as u32); + kv_pairs.insert(&**name, KVSpanned { + node: &**string, + k_span: k_span, + p_span: param.span, + v_span: value.span, + }); } else { - ecx.span_err(param.span, "Value must be a string."); + ecx.span_err(value.span, "Value must be a string."); } } else { let msg = format!("'{}' is not a valid parameter.", &**name);