From ad08fe1d0444d488d250c872ca081227e1a8eeb2 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 14 Mar 2016 20:43:52 -0700 Subject: [PATCH] Major changes. FN params are now being used! Woo! Subset of list of changes: * Split up decorator and macro into their own files. * Fully parsing the path parameter and verifying against the function's args. * Actually calling methods to fetch and convert the request parameters. * Actually calling methods to convert the handler's return type. * Sketched out more of the Request/Response structures. Pretty close to having a fully working MVP. --- examples/hello/src/main.rs | 23 +-- lib/src/error.rs | 4 +- lib/src/lib.rs | 14 +- lib/src/method.rs | 2 +- lib/src/request.rs | 16 ++ lib/src/response.rs | 20 ++ macros/src/lib.rs | 170 +---------------- macros/src/route_decorator.rs | 233 ++++++++++++++++++++++++ macros/src/routes_macro.rs | 48 +++++ macros/src/{macro_utils.rs => utils.rs} | 0 10 files changed, 343 insertions(+), 187 deletions(-) create mode 100644 lib/src/request.rs create mode 100644 lib/src/response.rs create mode 100644 macros/src/route_decorator.rs create mode 100644 macros/src/routes_macro.rs rename macros/src/{macro_utils.rs => utils.rs} (100%) diff --git a/examples/hello/src/main.rs b/examples/hello/src/main.rs index a1fb27f2..dd22479a 100644 --- a/examples/hello/src/main.rs +++ b/examples/hello/src/main.rs @@ -2,24 +2,19 @@ #![plugin(rocket_macros)] extern crate rocket; -use rocket::{Rocket, Request, Response, Method, Route}; +use rocket::Rocket; -#[route(GET, path = "/hello")] -fn hello() -> &'static str { - "Hello, world!" +#[route(GET, path = "/")] +fn hello(name: String) -> String { + format!("Hello, {}!", name) } -mod test { - use rocket::{Request, Response, Method, Route}; - - #[route(GET, path = "")] - pub fn hello() -> &'static str { - "Hello, world!" - } +#[route(PUT, path = "//")] +fn bye(x: usize, y: usize) -> String { + format!("{} + {} = {}", x, y, x + y) } fn main() { - let mut rocket = Rocket::new("localhost", 8000); - rocket.mount("/test", routes![test::hello]); - rocket.mount_and_launch("/", routes![hello]); + let rocket = Rocket::new("localhost", 8000); + rocket.mount_and_launch("/", routes![hello, bye]); } diff --git a/lib/src/error.rs b/lib/src/error.rs index 6343403b..8ce28607 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -1,3 +1,5 @@ pub enum Error { - BadMethod + BadMethod, + BadParse, + NoKey } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 1e1d4299..f3d2d299 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -2,9 +2,13 @@ extern crate hyper; mod method; mod error; +mod response; +mod request; pub use method::Method; pub use error::Error; +pub use response::Response; +pub use request::Request; use hyper::server::Handler as HypHandler; use hyper::server::Request as HypRequest; @@ -14,9 +18,6 @@ use hyper::Server; pub type Handler = fn(Request) -> Response; -pub struct Request; -pub struct Response; - #[allow(dead_code)] pub struct Route<'a> { pub method: Method, @@ -48,10 +49,9 @@ impl Rocket { } pub fn mount(&mut self, base: &str, routes: &[&Route]) -> &mut Self { - println!("Mounting at {}", base); + println!("🛰 Mounting '{}':", base); for route in routes { - println!(" - Found {} route to {}", route.method, route.path); - (route.handler)(Request); + println!("\t* {} '{}'", route.method, route.path); } self @@ -64,7 +64,7 @@ impl Rocket { pub fn launch(self) { let full_addr = format!("{}:{}", self.address, self.port); - println!("🚀 Rocket is launching ({})...", full_addr); + println!("🚀 Rocket has launched from {}...", full_addr); let _ = Server::http(full_addr.as_str()).unwrap().handle(self); } } diff --git a/lib/src/method.rs b/lib/src/method.rs index b1a62985..80680141 100644 --- a/lib/src/method.rs +++ b/lib/src/method.rs @@ -1,4 +1,4 @@ -use self::Method::{Options, Get, Post, Put, Delete, Head, Trace, Connect, Patch}; +use self::Method::*; use std::str::FromStr; use std::fmt::{self, Display}; use error::Error; diff --git a/lib/src/request.rs b/lib/src/request.rs new file mode 100644 index 00000000..1648c5ed --- /dev/null +++ b/lib/src/request.rs @@ -0,0 +1,16 @@ +use std::str::FromStr; +use error::Error; + +pub struct Request; + +impl Request { + pub fn get_param_str(&self, name: &str) -> Result<&str, Error> { + Err(Error::NoKey) + } + + pub fn get_param(&self, name: &str) -> Result { + self.get_param_str(name).and_then(|s| { + T::from_str(s).map_err(|_| Error::BadParse) + }) + } +} diff --git a/lib/src/response.rs b/lib/src/response.rs new file mode 100644 index 00000000..245b9523 --- /dev/null +++ b/lib/src/response.rs @@ -0,0 +1,20 @@ +pub struct Response; + +impl<'a> From<&'a str> for Response { + fn from(_s: &'a str) -> Self { + Response + } +} + +impl From for Response { + fn from(_s: String) -> Self { + Response + } +} + +impl Response { + pub fn error(number: usize) -> Response { + println!("ERROR {}!", number); + Response + } +} diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 00076bdc..629d9fcc 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -6,178 +6,20 @@ extern crate rustc; extern crate rustc_plugin; extern crate rocket; -#[macro_use] mod macro_utils; +#[macro_use] mod utils; +mod routes_macro; +mod route_decorator; -use macro_utils::{prepend_ident, get_key_values}; - -use std::str::FromStr; use rustc_plugin::Registry; - -use syntax::parse::token::{intern}; use syntax::ext::base::SyntaxExtension; -use syntax::ext::quote::rt::ToTokens; -use syntax::codemap::Span; -use syntax::ast::{Item, ItemKind, MetaItem, MetaItemKind, FnDecl, LitKind}; -use syntax::ext::base::{Annotatable, ExtCtxt}; -use syntax::ptr::P; +use syntax::parse::token::intern; -use syntax::ast::{Path, PathSegment, Expr, ExprKind, TokenTree}; -use syntax::ext::base::{DummyResult, MacResult, MacEager}; -use syntax::ext::build::AstBuilder; // trait for expr_usize -use syntax::parse::parser::{Parser, PathParsingMode}; -use syntax::parse::PResult; -use syntax::parse::token::Token; - -const DEBUG: bool = true; +use routes_macro::routes_macro; +use route_decorator::route_decorator; const STRUCT_PREFIX: &'static str = "ROCKET_ROUTE_STRUCT_"; const FN_PREFIX: &'static str = "rocket_route_fn_"; -use rocket::Method; - -struct Params { - method: Method, - path: String -} - -fn bad_item_fatal(ecx: &mut ExtCtxt, dec_sp: Span, i_sp: Span) -> ! { - ecx.span_err(dec_sp, "This decorator cannot be used on non-functions..."); - ecx.span_fatal(i_sp, "...but an attempt to use it on the item below was made.") -} - -fn bad_method_err(ecx: &mut ExtCtxt, dec_sp: Span, message: &str) -> Method { - let message = format!("{} Valid methods are: [GET, PUT, POST, DELETE, \ - OPTIONS, HEAD, TRACE, CONNECT, PATCH]", message); - ecx.span_err(dec_sp, message.as_str()); - Method::Get -} - -fn get_fn_decl<'a>(ecx: &mut ExtCtxt, sp: Span, annotated: &'a Annotatable) - -> (&'a P, &'a P) { - // `annotated` is the AST object for the annotated item. - let item: &P = match annotated { - &Annotatable::Item(ref item) => item, - &Annotatable::TraitItem(ref item) => bad_item_fatal(ecx, sp, item.span), - &Annotatable::ImplItem(ref item) => bad_item_fatal(ecx, sp, item.span) - }; - - let fn_decl: &P = match item.node { - ItemKind::Fn(ref decl, _, _, _, _, _) => decl, - _ => bad_item_fatal(ecx, sp, item.span) - }; - - (item, fn_decl) -} - -fn get_route_params(ecx: &mut ExtCtxt, meta_item: &MetaItem) -> Params { - // First, check that the macro was used in the #[route(a, b, ..)] form. - let params: &Vec> = match meta_item.node { - MetaItemKind::List(_, ref params) => params, - _ => ecx.span_fatal(meta_item.span, - "incorrect use of macro. correct form is: #[demo(...)]"), - }; - - // Ensure we can unwrap the k = v params. - if params.len() < 1 { - bad_method_err(ecx, meta_item.span, "HTTP method parameter is missing."); - 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. - let (method_param, kv_params) = params.split_first().unwrap(); - let method = if let MetaItemKind::Word(ref word) = method_param.node { - 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()) - }) - } else { - bad_method_err(ecx, method_param.span, "Invalid parameter. Expected a - valid HTTP method at this position.") - }; - - // Now grab all of the required and optional parameters. - let req: [&'static str; 1] = ["path"]; - let opt: [&'static str; 0] = []; - 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) - }); - - Params { - method: method, - path: path - } -} - -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 route_fn_name = prepend_ident(FN_PREFIX, &item.ident); - let fn_name = item.ident; - push(Annotatable::Item(quote_item!(ecx, - fn $route_fn_name(_req: Request) -> Response { - let result = $fn_name(); - println!("Routing function. Result: {}", result); - Response - } - ).unwrap())); - - let struct_name = prepend_ident(STRUCT_PREFIX, &item.ident); - let path = route_params.path; - let struct_item = quote_item!(ecx, - #[allow(non_upper_case_globals)] - pub static $struct_name: Route<'static> = Route { - method: Method::Get, // FIXME - path: $path, - handler: $route_fn_name - }; - ).unwrap(); - push(Annotatable::Item(struct_item)); -} - -fn get_paths<'a>(parser: &mut Parser<'a>) -> PResult<'a, Vec> { - if parser.eat(&Token::Eof) { - return Ok(vec![]); - } - - let mut results = Vec::new(); - loop { - results.push(try!(parser.parse_path(PathParsingMode::NoTypesAllowed))); - if !parser.eat(&Token::Comma) { - try!(parser.expect(&Token::Eof)); - break; - } - } - - Ok(results) -} - -fn routes_macro(ecx: &mut ExtCtxt, sp: Span, args: &[TokenTree]) - -> Box { - let mut parser = ecx.new_parser_from_tts(args); - let mut paths = get_paths(&mut parser).unwrap_or_else(|mut e| { - e.emit(); - vec![] - }); - - // Prefix each path terminator with STRUCT_PREFIX. - for p in &mut paths { - let last = p.segments.len() - 1; - let last_seg = &mut p.segments[last]; - let new_ident = prepend_ident(STRUCT_PREFIX, &last_seg.identifier); - last_seg.identifier = new_ident; - } - - // Build up the P for each &path. - let path_exprs = paths.iter().map(|p| { quote_expr!(ecx, &$p) }).collect(); - MacEager::expr(ecx.expr_vec_slice(sp, path_exprs)) -} - #[plugin_registrar] pub fn plugin_registrar(reg: &mut Registry) { reg.register_syntax_extension(intern("route"), diff --git a/macros/src/route_decorator.rs b/macros/src/route_decorator.rs new file mode 100644 index 00000000..83d594ae --- /dev/null +++ b/macros/src/route_decorator.rs @@ -0,0 +1,233 @@ +use super::{STRUCT_PREFIX, FN_PREFIX}; +use utils::{prepend_ident, get_key_values}; + +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::ext::base::{Annotatable, ExtCtxt}; +use syntax::ptr::P; +use syntax::ext::build::AstBuilder; +use syntax::print::pprust::item_to_string; +use syntax::parse::token::{self, str_to_ident}; + +use rocket::Method; + +#[allow(dead_code)] +const DEBUG: bool = true; + +struct Params { + method: Method, + path: String +} + +fn bad_item_fatal(ecx: &mut ExtCtxt, dec_sp: Span, i_sp: Span) -> ! { + ecx.span_err(dec_sp, "This decorator cannot be used on non-functions..."); + ecx.span_fatal(i_sp, "...but it was used on the item below.") +} + +fn bad_method_err(ecx: &mut ExtCtxt, dec_sp: Span, message: &str) -> Method { + let message = format!("{} Valid methods are: [GET, PUT, POST, DELETE, \ + OPTIONS, HEAD, TRACE, CONNECT, PATCH]", message); + ecx.span_err(dec_sp, message.as_str()); + Method::Get +} + +fn get_fn_decl<'a>(ecx: &mut ExtCtxt, sp: Span, annotated: &'a Annotatable) + -> (&'a P, &'a P) { + // `annotated` is the AST object for the annotated item. + let item: &P = match annotated { + &Annotatable::Item(ref item) => item, + &Annotatable::TraitItem(ref item) => bad_item_fatal(ecx, sp, item.span), + &Annotatable::ImplItem(ref item) => bad_item_fatal(ecx, sp, item.span) + }; + + let fn_decl: &P = match item.node { + ItemKind::Fn(ref decl, _, _, _, _, _) => decl, + _ => bad_item_fatal(ecx, sp, item.span) + }; + + (item, fn_decl) +} + +fn get_route_params(ecx: &mut ExtCtxt, meta_item: &MetaItem) -> Params { + // First, check that the macro was used in the #[route(a, b, ..)] form. + let params: &Vec> = match meta_item.node { + MetaItemKind::List(_, ref params) => params, + _ => ecx.span_fatal(meta_item.span, + "incorrect use of macro. correct form is: #[demo(...)]"), + }; + + // Ensure we can unwrap the k = v params. + if params.len() < 1 { + bad_method_err(ecx, meta_item.span, "HTTP method parameter is missing."); + 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. + let (method_param, kv_params) = params.split_first().unwrap(); + let method = if let MetaItemKind::Word(ref word) = method_param.node { + 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()) + }) + } else { + bad_method_err(ecx, method_param.span, "Invalid parameter. Expected a + valid HTTP method at this position.") + }; + + // Now grab all of the required and optional parameters. + let req: [&'static str; 1] = ["path"]; + let opt: [&'static str; 0] = []; + 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) + }); + + Params { + method: method, + path: path + } +} + +// Is there a better way to do this? I need something with ToTokens for the +// quote_expr macro that builds the route struct. I tried using +// str_to_ident("rocket::Method::Options"), but this seems to miss the context, +// and you get an 'ident not found' on compile. I also tried using the path expr +// builder from ASTBuilder: same thing. +fn method_variant_to_expr(ecx: &ExtCtxt, method: Method) -> P { + match method { + Method::Options => quote_expr!(ecx, rocket::Method::Options), + Method::Get => quote_expr!(ecx, rocket::Method::Get), + Method::Post => quote_expr!(ecx, rocket::Method::Post), + Method::Put => quote_expr!(ecx, rocket::Method::Put), + Method::Delete => quote_expr!(ecx, rocket::Method::Delete), + Method::Head => quote_expr!(ecx, rocket::Method::Head), + Method::Trace => quote_expr!(ecx, rocket::Method::Trace), + Method::Connect => quote_expr!(ecx, rocket::Method::Connect), + Method::Patch => quote_expr!(ecx, rocket::Method::Patch), + } +} + +pub fn get_fn_params(ecx: &ExtCtxt, sp: Span, path: &str, + fn_decl: &FnDecl) -> Vec { + let mut seen = HashSet::new(); + let bad_match_err = "Path string is malformed."; + let mut matching = false; + + // Collect all of the params in the path and insert into HashSet. + let mut start = 0; + for (i, c) in path.char_indices() { + match c { + '<' if !matching => { + matching = true; + start = i; + }, + '>' if matching => { + matching = false; + if start + 1 < i { + let param_name = &path[(start + 1)..i]; + seen.insert(param_name); + } else { + ecx.span_err(sp, "Parameter cannot be empty."); + } + }, + '<' if matching => ecx.span_err(sp, bad_match_err), + '>' if !matching => ecx.span_err(sp, bad_match_err), + _ => { /* ... */ } + } + } + + // 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 { + let ident: &Ident = match arg.pat.node { + PatKind::Ident(_, ref ident, _) => &ident.node, + _ => { + ecx.span_err(sp, "Expected an identifier."); // FIXME: fn span. + 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()); + } + + result.push(name); + } + + // 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()); + } + + result +} + +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 param in &fn_params { + let param_ident = str_to_ident(param.as_str()); + fn_param_exprs.push(quote_stmt!(ecx, + let $param_ident = match req.get_param($param) { + Ok(v) => v, + Err(_) => return rocket::Response::error(200) + }; + ).unwrap()); + } + + 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); + fn_param_idents.extend(tokens); + if i < fn_params.len() - 1 { + fn_param_idents.push(TokenTree::Token(DUMMY_SP, token::Comma)); + } + } + + 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(req: rocket::Request) -> rocket::Response { + $fn_param_exprs + let result = $fn_name($fn_param_idents); + rocket::Response::from(result) + } + ).unwrap(); + debug!("{}", item_to_string(&route_fn_item)); + 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); + push(Annotatable::Item(quote_item!(ecx, + #[allow(non_upper_case_globals)] + pub static $struct_name: rocket::Route<'static> = rocket::Route { + method: $method, + path: $path, + handler: $route_fn_name + }; + ).unwrap())); +} + diff --git a/macros/src/routes_macro.rs b/macros/src/routes_macro.rs new file mode 100644 index 00000000..ee766b77 --- /dev/null +++ b/macros/src/routes_macro.rs @@ -0,0 +1,48 @@ +use super::{STRUCT_PREFIX}; +use utils::prepend_ident; + +use syntax::codemap::Span; +use syntax::ast::{Path, TokenTree}; +use syntax::ext::base::{ExtCtxt, MacResult, MacEager}; +use syntax::ext::build::AstBuilder; +use syntax::parse::parser::{Parser, PathParsingMode}; +use syntax::parse::PResult; +use syntax::parse::token::Token; + +fn get_paths<'a>(parser: &mut Parser<'a>) -> PResult<'a, Vec> { + if parser.eat(&Token::Eof) { + return Ok(vec![]); + } + + let mut results = Vec::new(); + loop { + results.push(try!(parser.parse_path(PathParsingMode::NoTypesAllowed))); + if !parser.eat(&Token::Comma) { + try!(parser.expect(&Token::Eof)); + break; + } + } + + Ok(results) +} + +pub fn routes_macro(ecx: &mut ExtCtxt, sp: Span, args: &[TokenTree]) + -> Box { + let mut parser = ecx.new_parser_from_tts(args); + let mut paths = get_paths(&mut parser).unwrap_or_else(|mut e| { + e.emit(); + vec![] + }); + + // Prefix each path terminator with STRUCT_PREFIX. + for p in &mut paths { + let last = p.segments.len() - 1; + let last_seg = &mut p.segments[last]; + let new_ident = prepend_ident(STRUCT_PREFIX, &last_seg.identifier); + last_seg.identifier = new_ident; + } + + // Build up the P for each &path. + let path_exprs = paths.iter().map(|p| { quote_expr!(ecx, &$p) }).collect(); + MacEager::expr(ecx.expr_vec_slice(sp, path_exprs)) +} diff --git a/macros/src/macro_utils.rs b/macros/src/utils.rs similarity index 100% rename from macros/src/macro_utils.rs rename to macros/src/utils.rs