Major progress towards form support.

This commit is contained in:
Sergio Benitez 2016-03-30 01:02:21 -07:00
parent fb8fdc3bc2
commit d477c18062
6 changed files with 337 additions and 95 deletions

View File

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

View File

@ -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 = "/<file>")]
fn files(file: &str) -> Result<File, IOError> {
File::open(format!("static/{}", file))
}
use rocket::error::Error;
#[route(GET, path = "/user/<username>")]
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<Self, Error>;
}
impl<'a> FormItem for UserLogin<'a> {
fn from_form_string(s: &str) -> Result<Self, Error> {
Ok(UserLogin {
username: "this",
password: "that"
})
}
}
// TODO: Actually look at form parameters.
#[route(POST, path = "/login")]
fn login() -> Result<Redirect, &'static str> {
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 = "<user>")]
fn login(user: UserLogin) -> Result<Redirect, String> {
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();
}

View File

@ -5,7 +5,7 @@ extern crate rocket;
use rocket::Rocket;
#[route(GET, path = "/users/<name>")]
fn user(name: &str) -> Option<&'static str> {
fn user(name: &str, other: i8) -> Option<&'static str> {
if name == "Sergio" {
Some("Hello, Sergio!")
} else {

View File

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

View File

@ -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<Method>,
path: KVSpanned<String>,
form: Option<KVSpanned<String>>,
}
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<Item>, &'a P<FnDecl>) {
-> (&'a P<Item>, Spanned<&'a FnDecl>) {
// `annotated` is the AST object for the annotated item.
let item: &P<Item> = 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<FnDecl> = 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<Method>.
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: \"<param>\"")
.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<Expr> {
}
}
pub fn get_fn_params(ecx: &ExtCtxt, sp: Span, path: &str,
fn_decl: &FnDecl) -> Vec<String> {
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 = &params.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<Ty>,
span: Span
}
pub fn gen_kv_string_hashset<'a>(ecx: &ExtCtxt, params: &'a KVSpanned<String>)
-> 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, &params)
}
impl SimpleArg {
fn new<T: ToString>(name: T, ty: P<Ty>, 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<String>,
fn_decl: &Spanned<&FnDecl>, mut external: HashSet<&'a str>)
-> Vec<SimpleArg> {
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<SimpleArg>,
form_params: &HashSet<&str>) -> Option<Stmt> {
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(&param_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<TokenTree> = 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 = &param.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(&param_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 {

View File

@ -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<T: ToString>(ident: &Ident, other: T) -> Ident {
token::str_to_ident(new_ident.as_str())
}
#[inline]
pub fn wrap_span<T>(t: T, span: Span) -> Spanned<T> {
Spanned {
span: span,
node: t,
}
}
#[inline]
pub fn dummy_span<T>(t: T) -> Spanned<T> {
Spanned {
span: DUMMY_SP,
node: t,
}
}
#[inline]
pub fn dummy_kvspan<T>(t: T) -> KVSpanned<T> {
KVSpanned {
k_span: DUMMY_SP,
v_span: DUMMY_SP,
p_span: DUMMY_SP,
node: t,
}
}
#[derive(Debug, Clone)]
pub struct KVSpanned<T> {
pub k_span: Span,
pub v_span: Span,
pub p_span: Span,
pub node: T
}
impl<T: ToTokens> ToTokens for KVSpanned<T> {
fn to_tokens(&self, cx: &ExtCtxt) -> Vec<TokenTree> {
self.node.to_tokens(cx)
}
}
impl<T> KVSpanned<T> {
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> KVSpanned<U> {
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<MetaItem>])
-> 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);